Перейти к основному содержимому

Создание веб-приложения MVC с помощью Grails

· 8 мин. чтения

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 :

  • ресурсы — место, где мы храним статические файлы ресурсов, такие как стили, файлы javascript или изображения.

  • conf — содержит файлы конфигурации проекта:

  • application.yml содержит стандартные настройки веб-приложения, такие как источник данных, типы mime и другие настройки, связанные с Grails или Spring.

  • resources.groovy содержит определения bean-компонентов Spring.

  • logback.groovy содержит конфигурацию ведения журнала

  • контроллеры — отвечают за обработку запросов и генерацию ответов или делегирование их представлениям. По соглашению, когда имя файла заканчивается на *Controller , платформа создает сопоставление URL-адресов по умолчанию для каждого действия, определенного в классе контроллера.

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

  • i18n — используется для поддержки интернационализации

  • init — точка входа приложения

  • services — здесь будет жить бизнес-логика приложения. По соглашению Grails создаст одноэлементный компонент Spring для каждой службы.

  • taglib — место для пользовательских библиотек тегов

  • представления — содержит представления и шаблоны

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

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

grails create-app

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

4.1. Слой домена

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

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

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

class Student {
String firstName
String lastName
}

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

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

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

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

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

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

grails create-service com.foreach.grails.Student

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

@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()
}
}

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

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

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

grails create-controller com.foreach.grails.Student

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

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

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"
}
}

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

4.4. Просмотр слоя

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

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

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

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

<!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/create.gsp , которое позволяет пользователю создавать новых учащихся. Мы будем использовать встроенный тег <f:all/> , который отображает форму для всех свойств данного компонента: ``

<!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>

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

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

<!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>

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

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

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

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

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'
}

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

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'
}

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

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

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

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

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
}

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

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

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'
}

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

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

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

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

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

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

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

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

grails run-app

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

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

./c7eee9bc9c53520d8f8b325a91717a12.jpg

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

grails war

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

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

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

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