Разработка приложений со Spring. Базовое веб-приложение.

В данной статье я рассмотрю процесс разработки простого веб-приложения с использованием Spring и Thymeleaf. Данный проект будет использоваться в последующих статьях, в рамках которых приложение будет описано дальнейшее развитие приложения. Исходный код проекта доступен по этой ссылке.

Содержание

  1. О проекте, его структура и зависимости
  2. Классы-сущности и репозитории
  3. Модульное и интеграционное тестирование
  4. Контроллеры
  5. Шаблоны Thymeleaf
  6. Интернационализация

О проекте

Разрабатываемый проект — простой хелпдеск, основная цель которого — предоставить пользователям возможность уведомлять службу технической поддержки о возникающих проблемах. Кроме возможности создания самих заявок (или обращений), должна быть реализована возможность оставлять комментарии к заявкам.

Для данного проекта потребуются следующие зависимости:

  • Spring Data JPA для работы с базой данных
  • Thymeleaf для описания шаблонов
  • СУБД H2 в качестве хранилища данных
  • Lombok для минимизации рутинного кода вроде get/set-методов и конструкторов

Project Lombok

Данный инструмент значительно упрощает процесс разработки, добавляя перед компиляцией необходимые рутинные элементы, такие как get/set-методы, конструкторы, логгеры и многое другое. Поддержка данного инструмента есть во всех главных Java IDE. Более подробно с Lombok можно ознакомиться на официальном сайе проекта.

В итоге pom.xml будет выглядеть следующим образом:

Поскольку проект основан на Spring Boot, можно сгеренировать заготовку проекта при помощи Spring Initializr.

Классы-сущности и репозитории

Как уже было сказано, основные возможности нашего проекта — создание и обсуждение заявок. Следовательно, нам потребуется всего два класса-сущностей: для описания заявки и для описания комментария. Авторизация будет рассматриваться в следующей статье, так что в классах-сущностях информации об авторах записей не будет.

В классе, описывающем заявку или обращение, понадобится четыре свойства:

  • Идентификатор
  • Краткое описание проблемы
  • Подробное описание проблемы
  • Дата создания

В классе, описывающем комментарий к заявке, понадобятся также четыре свойства:

  • Идентификатор
  • Текст комментария
  • Дата создания
  • Ссылка на заявку

Немножко задержимся на аннотациях, фигурирующих в наших классах-сущностях.

  • @Entity — JPA-аннотация уровня типа, обозначающая, что данный класс проецируется на таблицу в базе данных.
  • @Data — аннотация Lombok, добавляющая все рутинные методы классов сущностей, среди которых get/set-методы, hashCode, equals и toString.
  • @NoArgsConstructor добавляет конструктор без аргументов.
  • @AllArgsConstructor добавляет конструктор со всеми аргументами.
  • @Id означает, что помеченное свойство является первичным ключом.
  • @GeneratedValue означает, что значение данного свойства при сохранении должно быть сгенерировано. В Spring Boot по-умолчанию используется стратегия IDENTITY. Таким образом фактическое значение данной аннотации: @GeneratedValue(strategy = GenerationType.IDENTITY)
  • @Column указывает дополнительные свойства колонки в таблице БД. Атрибут nullable=false указывает фреймворку, что свойство не может быть null, атрибут updatable=false — что свойство не может изменяться, а columnDefinition=»TEXT» — что тип колонки должен быть TEXT. Это важно, так как в некоторых колонках нужно хранить более 255 символов.
  • @Temporal указывает, что свойство должно храниться в таблице БД как дата.
  • @ManyToOne указывает на связанный объект. Для связи в таблице БД должна существовать колонка вида имя_свойства_id, в которой будет храниться первичный ключ связанной записи. В нашем случае имя колонки будет ticket_id.

Структура базы данных

Обратите внимание, что создавать отдельно структуру базы данных не нужно, это за нас сделает Spring Data JPA, так как мы будем использовать СУБД H2, которая расценивается связкой Spring Boot и Spring Data JPA как тестовая. Если есть необходимость автоматического создания структуры базы данных в полноценных СУБД, вроде PostgreSQL или MySQL, то это можно настроить в свойствах приложения. Однако данный подход не рекомендуется и является опасным при применении в рабочих решениях. Более подробно об этом рассказано в документации Spring Boot.

Для доступа к данным нам потребуются репозитории. Поскольку в нашем проекте может понадобится постраничная разбивка данных (пейджинация), мы будем использовать PagingAndSortingRepository.

Репозиторий для заявок:

Репозиторий для комментариев:

Spring Data JPA при инициализации приложения создаст объекты репозиториев и реализует объявленный нами волшебный метод. О волшебных методах я писал в предыдущей статье.

Тестирование репозиториев

Нет никакого смысла тестировать репозитории, в которых нет собственных методов, а работоспособность стандартных методов проверяется тестами разработчиками Spring Data JPA. А вот собственные методы нужно тестировать при помощи интеграционных тестов.

Модульное и интеграционное тестирование

В современном мире разработки программного обеспечения тестирование является обязательным атрибутом. Я считаю, что разработку нужно вести через тестирование (TDD), так как такой подход, на мой взгляд, минимизирует количество ошибок, проявляющихся в итоговом продукте. Разработка через тестирование очень удобна при итеративном подходе, так как условия прохождения тестов легко генерируются из критериев, которым должен удовлетворять результат итерации.

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

Тестироваться должны все возможные варианты поведения компонента, как оптимистичные, так и пессимистичные. Таким образом для тестирования некоторых методов потребуется 4-5, а то и больше тестовых методов. В реальных проектах объём тестового кода может без проблем составлять 50-75%, что может создать впечатление пустой траты рабочего времени разработчика, что совершенно неверно, так как цена ошибок, которые могли быть обнаружены тестами, порой может быть очень высокой.

В нашем проекте будут использованы оба способа тестирования, но я не буду добавлять в статью весь код, относящийся к тестированию, так как его объём значительно больше тестируемого кода.

Контроллеры, тесты и реализация

По аналогии с репозиториями нам понадобятся два класса-контроллера: один — для работы с заявками, второй — для работы с комментариями.

Давайте взглянем на контроллер для работы с заявками:

Здесь мы использовали следующие аннотации:

  • @Controller — аннотация-стереотип Spring Framework, говорящая, что данный класс является классом-контроллером. Spring Framework, обнаружив данную аннотацию, создаст в контексте приложения экземпляр этого класса. В Spring Boot — это поведение по-умолчанию.
  • @RequestMapping — аннотация, указывающая, что отмеченный класс-контроллер или метод будет обрабатывать HTTP-запросы с подходящим адресом. В нашем случае TicketsController будет обрабатывать запросы вида http://localhost:8080/tickets
  • @AllArgsCostructor уже упоминалась, она при компиляции добавит конструктор со всеми атрибутами, которыми в нашем случае являются repository и ticketCommentRepository. Объекты этих типов будут получены из контекста приложения и переданы в конструктор средствами Spring Framework.

При разработке через тестирование заготовка контроллера должна быть минимальной — в ней не должно быть вообще ничего, даже указанных аннотаций.

Теперь на примере страницы со списком заявок мы разберём написание тестов и реализации.

Просмотр списка заявок

Реализуемая задача звучит следующим образом: «В приложении должна быть страница со списком всех заявок, на которой также должна находиться ссылка на страницу создания новой заявки».

В модульном тесте мы можем проверить, что:

  • Метод должен вызываться с аргументом типа Pageable.
  • Из репозитория был получен список заявок
  • Возвращённая в ответе модель содержит список заявок
  • Возвращённое в ответе имя шаблона соответствует ожидаемому

Код модульного теста:

Обратите внимание, что результат работы сторонних компонентов мы имитируем. Имитация всех возможных вариантов поведения сторонних компонентов даёт возможность протестировать правильность работы тестируемого компонента в любых условиях.

В интеграционном тесте мы можем проверить, что GET-запрос к http://localhost:8080/tickets:

  • Возвратит страницу, на которой есть ссылка на создание новой заявки
  • Возвратит HTTP-статус 200 OK

Код интеграционного теста:

Код метода, который будет удовлетворять тестовым методам:

Аннотация @GetMapping является укороченной версией @RequestMapping(method = RequestMethod.GET).

Для прохождения интеграционного теста понадобится шаблон templates/tickets/index.html.

Шаблоны и Thymeleaf

В качестве библиотеки шаблонов в проекте мы используем Thymeleaf. По-умолчанию данная библиотека работает в режиме XHTML 5. Для использования тегов и атрибутов Thymeleaf необходимо подключить соответствующий неймспейс:

Подробно описывать работу с Thymeleaf в рамках данной статьи я не буду, это обязательно будет сделано в одной из ближайших статей, но остановлюсь на основных моментах. Для использования возможностей Thymeleaf в шаблонах используются атрибуты и теги из его неймспейса, например th:text — для вывода текста внутри указанного тега или th:each — для обхода коллекции в стиле foreach:

Интернационализация

Современное приложение, рассчитанное на массовую аудиторию, сложно представить без интернационализации или локализации. В Spring Boot есть поддержка интернационализации прямо из коробки, и всё, что требуется от разработчика — добавить файлы перевода для разных локалей и настроить механизм переключения между локалями.

Для запоминания локали в нашем приложении на данном этапе мы будем использовать файлы Cookie. В классе-конфигурации для этого нужно добавить один метод:

Переключаться между локалями мы будем при помощи гиперссылок на странице нашего приложения, соответственно, нам нужно настроить переключение локали по параметру запроса. Для этого в классе-конфигурации нужно зарегистрировать перехватчик LocaleChangeInterceptor, который будет искать в параметрах запроса параметр с выбранной локалью (по умолчанию — locale).

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

После этого останется в src/main/resources добавить файлы локализации для нужных локалей. В Spring используется имя бандла messages, соответственно, имя файла со значениями по умолчанию — messages.properties. Обращу внимание, что для корректной работы переключения между локалями может потребоваться отключение автоматического использования системной локали при отсутствии файла локализации с выбранной локалью (свойство spring.messages.fallback-to-system-locale).

После того, как всё настроено, можно использовать коды сообщений в шаблонах. Для этого в Thymeleaf используются выражения вида #{}, например — #{msg.greeting}.

Результат

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

Ссылки