1. Обзор

В этом руководстве мы научимся создавать простое веб-приложение с помощью Grails.

Grails (точнее, его последняя основная версия) — это фреймворк, построенный на основе проекта Spring Boot и использующий язык Apache Groovy для разработки веб-приложений.

Он вдохновлен Rails Framework для Ruby и построен на философии «конвенция над конфигурацией», которая позволяет сократить шаблонный код.

2. Настройка

Прежде всего, давайте перейдем на официальную страницу, чтобы подготовить среду. На момент написания этого руководства последней версией была 3.3.3.

Проще говоря, существует два способа установки Grails: через SDKMAN или путем загрузки дистрибутива и добавления двоичных файлов в переменную окружения PATH.

Мы не будем пошагово описывать настройку, потому что она хорошо задокументирована в Grails Docs.

3. Анатомия приложения Grails

В этом разделе мы лучше понимаем структуру приложения Grails. Как мы упоминали ранее, Grails предпочитает соглашение конфигурации, поэтому расположение файлов определяет их назначение. Давайте посмотрим, что у нас есть в каталоге grails-app:

assets — место, где мы храним файлы статических ресурсов, такие как стили, файлы javascript или изображения. conf — содержит файлы конфигурации проекта: application.yml содержит стандартное веб-приложение. такие параметры, как источник данных, типы mime и другие параметры, связанные с Grails или Spring resources.groovy содержит определения bean-компонентов Spring logback.groovy содержит контроллеры конфигурации регистрации, отвечающие за обработку запросов и генерацию ответов или делегирование их представлениям. По соглашению, когда имя файла заканчивается на *Controller, платформа создает сопоставление URL-адресов по умолчанию для каждого действия, определенного в домене класса контроллера — содержит бизнес-модель приложения Grails. Каждый класс, находящийся здесь, будет сопоставлен с таблицами базы данных с помощью GORM i18n — используется для поддержки интернационализации. По соглашению Grails создаст одноэлементный компонент Spring для каждой библиотеки taglib службы — место для представлений пользовательских библиотек тегов — содержит представления и шаблоны

    4. Простое веб-приложение

В этой главе мы создадим простое веб-приложение для управления студентами. Начнем с вызова команды CLI для создания скелета приложения:

Когда базовая структура проекта сгенерирована, давайте перейдем к реализации реальных компонентов веб-приложения.

grails create-app

4.1. Уровень домена

Поскольку мы реализуем веб-приложение для работы со студентами, давайте начнем с создания класса домена с именем Student:

И, наконец, добавим к нему свойства firstName и lastName:

grails create-domain-class com.baeldung.grails.Student

~~ ~ Grails применяет свои соглашения и устанавливает объектно-реляционное сопоставление для всех классов, расположенных в каталоге grails-app/domain.

class Student {
    String firstName
    String lastName
}

Более того, благодаря трейту GormEntity все классы предметной области будут иметь доступ ко всем операциям CRUD, которые мы будем использовать в следующем разделе для реализации сервисов.

4.2. Сервисный уровень

Наше приложение будет обрабатывать следующие варианты использования:

Просмотр списка студентов Создание новых студентов Удаление существующих студентов

    Давайте реализуем эти варианты использования. Мы начнем с создания класса сервиса:

Давайте перейдем в каталог grails-app/services, найдем наш только что созданный сервис в соответствующем пакете и добавим все необходимые методы:

grails create-service com.baeldung.grails.Student

~~ ~ Обратите внимание, что службы по умолчанию не поддерживают транзакции. Мы можем включить эту функцию, добавив в класс аннотацию @Transactional.

@Transactional
class StudentService {

    def get(id){
        Student.get(id)
    }

    def list() {
        Student.list()
    }

    def save(student){
        student.save()
    }

    def delete(id){
        Student.get(id).delete()
    }
}

4.3. Уровень контроллера

Чтобы сделать бизнес-логику доступной для пользовательского интерфейса, давайте создадим StudentController, вызвав следующую команду:

По умолчанию Grails внедряет bean-компоненты по именам. Это означает, что мы можем легко внедрить одноэлементный экземпляр StudentService в наш контроллер, объявив переменную экземпляра с именем studentService.

grails create-controller com.baeldung.grails.Student

«Теперь мы можем определить действия для чтения, создания и удаления учеников.

По соглашению, действие index() этого контроллера будет отображаться на URI /student/index, действие show() на /student/show и так далее.

class StudentController {

    def studentService

    def index() {
        respond studentService.list()
    }

    def show(Long id) {
        respond studentService.get(id)
    }

    def create() {
        respond new Student(params)
    }

    def save(Student student) {
        studentService.save(student)
        redirect action:"index", method:"GET"
    }

    def delete(Long id) {
        studentService.delete(id)
        redirect action:"index", method:"GET"
    }
}

4.4. View Layer

Настроив действия нашего контроллера, мы можем теперь приступить к созданию представлений пользовательского интерфейса. Мы создадим три страницы Groovy Server для перечисления, создания и удаления студентов.

По соглашению Grails отображает представление на основе имени контроллера и действия. Например, действие index() из StudentController будет преобразовано в /grails-app/views/student/index.gsp

Давайте начнем с реализации представления /grails-app/views/student/index.gsp, которое будет отображать список учеников. Мы будем использовать тег \u003cf:table/\u003e для создания HTML-таблицы, отображающей всех учащихся, возвращенных действием index() в нашем контроллере.

По соглашению, когда мы отвечаем списком объектов, Grails добавит суффикс «List» к имени модели, чтобы мы могли получить доступ к списку объектов ученика с помощью переменной studentList:

Мы Теперь перейдем к представлению /grails-app/views/student/create.gsp, которое позволяет пользователю создавать новых учащихся. Мы будем использовать встроенный тег \u003cf:all/\u003e, который отображает форму для всех свойств данного компонента:

<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
    </head>
    <body>
        <div class="nav" role="navigation">
            <ul>
                <li><g:link class="create" action="create">Create</g:link></li>
            </ul>
        </div>
        <div id="list-student" class="content scaffold-list" role="main">
            <f:table collection="${studentList}" 
                properties="['firstName', 'lastName']" />
        </div>
    </body>
</html>

Наконец, давайте создадим представление /grails-app/views/student/show .gsp для просмотра и последующего удаления учащихся.

<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
    </head>
    <body>
        <div id="create-student" class="content scaffold-create" role="main">
            <g:form resource="${this.student}" method="POST">
                <fieldset class="form">
                    <f:all bean="student"/>
                </fieldset>
                <fieldset class="buttons">
                    <g:submitButton name="create" class="save" value="Create" />
                </fieldset>
            </g:form>
        </div>
    </body>
</html>

Среди других тегов мы воспользуемся тегом \u003cf:display/\u003e, который принимает bean-компонент в качестве аргумента и отображает все его поля:

4.5. Модульные тесты

<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
    </head>
    <body>
        <div class="nav" role="navigation">
            <ul>
                <li><g:link class="list" action="index">Students list</g:link></li>
            </ul>
        </div>
        <div id="show-student" class="content scaffold-show" role="main">
            <f:display bean="student" />
            <g:form resource="${this.student}" method="DELETE">
                <fieldset class="buttons">
                    <input class="delete" type="submit" value="delete" />
                </fieldset>
            </g:form>
        </div>
    </body>
</html>

Grails в основном использует преимущества Spock для целей тестирования. Если вы не знакомы со Spock, мы настоятельно рекомендуем сначала прочитать это руководство.

Давайте начнем с модульного тестирования действия index() нашего StudentController.

Мы смоделируем метод list() из StudentService и проверим, возвращает ли index() ожидаемую модель:

Теперь давайте проверим действие delete(). Мы проверим, был ли вызван метод delete() из StudentService, и проверим перенаправление на индексную страницу:

void "Test the index action returns the correct model"() {
    given:
    controller.studentService = Mock(StudentService) {
        list() >> [new Student(firstName: 'John',lastName: 'Doe')]
    }
 
    when:"The index action is executed"
    controller.index()

    then:"The model is correct"
    model.studentList.size() == 1
    model.studentList[0].firstName == 'John'
    model.studentList[0].lastName == 'Doe'
}

4.6. Интеграционные тесты

void "Test the delete action with an instance"() {
    given:
    controller.studentService = Mock(StudentService) {
      1 * delete(2)
    }

    when:"The domain instance is passed to the delete action"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'DELETE'
    controller.delete(2)

    then:"The user is redirected to index"
    response.redirectedUrl == '/student/index'
}

Далее давайте посмотрим, как создавать интеграционные тесты для сервисного уровня. В основном мы будем тестировать интеграцию с базой данных, настроенной в файле grails-app/conf/application.yml.

По умолчанию Grails использует для этой цели базу данных H2 в памяти.

Прежде всего, давайте начнем с определения вспомогательного метода для создания данных для заполнения базы данных:

Благодаря аннотации @Rollback в нашем классе интеграционного теста каждый метод будет выполняться в отдельной транзакции, что будет откатываться в конце теста.

private Long setupData() {
    new Student(firstName: 'John',lastName: 'Doe')
      .save(flush: true, failOnError: true)
    new Student(firstName: 'Max',lastName: 'Foo')
      .save(flush: true, failOnError: true)
    Student student = new Student(firstName: 'Alex',lastName: 'Bar')
      .save(flush: true, failOnError: true)
    student.id
}

Посмотрите, как мы реализовали интеграционный тест для нашего метода list():

Также давайте протестируем метод delete() и проверим, уменьшилось ли общее количество студентов на единицу: ~~ ~

void "test list"() {
    setupData()

    when:
    List<Student> studentList = studentService.list()

    then:
    studentList.size() == 3
    studentList[0].lastName == 'Doe'
    studentList[1].lastName == 'Foo'
    studentList[2].lastName == 'Bar'
}

5. Запуск и развертывание

void "test delete"() {
    Long id = setupData()

    expect:
    studentService.list().size() == 3

    when:
    studentService.delete(id)
    sessionFactory.currentSession.flush()

    then:
    studentService.list().size() == 2
}

Запуск и развертывание приложений можно выполнить с помощью одной команды через интерфейс командной строки Grails.

Для запуска приложения используйте:

По умолчанию Grails настроит Tomcat на порт 8080.

grails run-app

Давайте перейдем по адресу http://localhost:8080/student/index, чтобы посмотреть, как выглядит наше веб-приложение. например:

Если вы хотите развернуть свое приложение в контейнере сервлетов, используйте:

для создания готового к развертыванию военного артефакта.

grails war

6. Заключение

В этой статье мы сосредоточились на том, как создать веб-приложение Grails, используя философию соглашения вместо конфигурации. Мы также увидели, как выполнять модульные и интеграционные тесты с помощью инфраструктуры Spock.

Как всегда, весь используемый здесь код можно найти на GitHub.

«