В одной из классических статей для новичков, мелькавших недавно на Хабре, рассказывалось про создание базового Web приложения на Java. Все начиналось с сервлета, потом создания JSP страницы и, наконец, деплоймента в контейнер. Посмотрев на это свежим взглядом я понял, что для как раз для новичков это, наверняка, выглядит совершенно жутко — на фоне простых и понятных PHP или Node.js, где все просто — написал контроллер, вернул объект, он стал JSON или HTML. Чтобы немного развеять это ощущение, я решил написать "Гайд для новичков в Spring". Цель это статьи — показать, что создание Web приложений на Java, более того — на Spring Framework это не боль и мучительное продирание через web.xml, persistence.xml, beans.xml, и собирание приложения как карточного домика по кусочкам, а вполне себе быстрый и комфортный процесс. Аудитория — начинающие разработчики, разработчики на других языках, ну и те, кто видел Спринг в его не самые лучше времена.
Введение
В этой статье мы посмотрим, что включает в себя современный Спринг, как настроить локальное окружение для разработки Веб приложений, и создадим простое веб-приложение, которое берет данные из БД и отдает HTML страницу и JSON. Как ни странно, большинство статей (на русском языке) для начинающих, которые я нашел в топе поиска описывают и ручное создание контекста, и запуск приложения, и конфигурацию через XML — ничего из этого в современном Спринге делать, разумеется, не обязательно.
Что такое Spring?
Для начала пара слов, что же такое Spring. В настоящее время, под термином "Spring" часто подразумевают целое семейство проектов. В большинстве своем, они развиваются и курируются компанией Pivotal и силами сообщества. Ключевые (но не все) проекты семейства Spring это:
-
Spring Framework (или Spring Core)
Ядро платформы, предоставляет базовые средства для создания приложений — управление компонентами (бинами, beans), внедрение зависимостей, MVC фреймворк, транзакции, базовый доступ к БД. В основном это низкоуровневые компоненты и абстракции. По сути, неявно используется всеми другими компонентами. -
Spring MVC (часть Spring Framework)
Стоит упомянуть отдельно, т.к. мы будем вести речь в основном о веб-приложениях. Оперирует понятиями контроллеров, маппингов запросов, различными HTTP абстракциями и т.п. Со Spring MVC интегрированы нормальные шаблонные движки, типа Thymeleaf, Freemaker, Mustache, плюс есть сторонние интеграции с кучей других. Так что никакого ужаса типа JSP или JSF писать не нужно. -
Spring Data
Доступ к данным: реляционные и нереляционные БД, KV хранилища и т.п. -
Spring Cloud
Много полезного для микросервисной архитектуры — service discovery, трасировка и диагностика, балансировщики запросов, circuit breaker-ы, роутеры и т.п. -
Spring Security
Авторизация и аутентификация, доступ к данным, методам и т.п. OAuth, LDAP, и куча разных провайдеров. - Spring Integration
Обработка данных из разных источников. Если надо раз в час брать файл с ФТП, разбивать его на строки, которые потом фильтровать, а дальше отправлять в какую-то очередь — это к Spring Integration.
Типичное веб приложение скорее всего будет включать набор вроде Spring MVC, Data, Security. Ниже мы увидим, как это все работает вместе.
Особняком стоит отметить Spring Boot — это вишенка на торте (а некоторые думают, что собственно сам торт), которые позволяет избежать всего ужаса XML конфигурации. Boot позволяет быстро создать и сконфигурить (т.е. настроить зависимости между компонентами) приложение, упаковать его в исполняемый самодостаточный артефакт. Это то связующее звено, которое объединяет вместе набор компонентов в готовое приложение. Пару вещей, которые нужно знать про Spring Boot:
- Он не использует кодогенерацию. Из кода, который генерится, присутствует только метод main. Если вы ищете утилиту для генерации приложений — это скорее JHipster
- Не использует XML для конфигурации. Все конфигурится через аннотации
- Используются автоконфигурации по максимуму. Если у вас добавлена зависимость на Mongo, и не указано, куда подключаться — Boot попробует
localhost:27017
- Используется convention over configuration. Для большинства конфигураций не нужно ничего настраивать
- Его легко отодвинуть в сторону и "перекрыть" конфигурацию по умолчанию. Например, если в настройках указать хост для подключения к Монго — он автоматически перекроет
localhost
Настройка окружения
Для того, чтобы создать простое приложение, знать, как создать проект Maven с нуля, как настроить плагины, чтобы создать JAR, какие бывают лейауты в JAR, как настроить Surefire для запуска тестов, как установить и запустить локально Tomcat, а уж тем более, как работает DispatcherServlet — совершенно не нужно.
Современное приложение на Spring создается в два шага:
- Идем на Spring Initializr.
- … и все, второго шага нет
Spring Initializr позволяет "набрать" в свое приложение нужных компонентов, которые потом Spring Boot (он автоматически включен во все проекты, созданные на Initializr) соберет воедино.
В качестве среды разработки подойдет что угодно, например бесплатная IntelliJ IDEA CE прекрасно справляется — просто импортируйте созданный pom.xml (Maven) или build.gradle (Gradle) файл в IDE.
Стоит отдельно отметить компонент Spring Boot который называется DevTools. Он решает проблему цикла локальной разработки, который раньше выглядел как:
- Собрать WAR локально
- Задеплоить его в Очень Крутой Энтерпрайз Аппликейшен Сервер на тестовом сервере, потому что настройка ОКЭАС локально требует недюжинной сноровки
- Выпить чашку кофе, т.е. ОКЭАС перезапустить приложение в лучшем случае через несколько минут
- Скопировать стектрейс ошибки
- Перейти на п. 1
В те древние времена даже родилась поговорка, что Spring это DSL для конвертации XML конфигов в стектрейсы.
С включенными Spring Boot DevTools цикл разработки сокращается до:
- Запустить приложение через зеленый треугольничек в IDEA
DevTools будут автоматом проверять изменения в скомпилированном коде или шаблонах, и очень быстро перезапускать (hot reload) только "боевую" часть приложения (как nodemon, если вы знакомы с миром node.js). Более того, DevTools включают интеграцию с Live Reload и после установки расширения в браузере, достаточно скомпилировать проект в IDEA, чтобы он автоматом обновился в браузере.
Разработка
Окей, пора приступать к практической части. Итак, наша цель — создать веб-приложение, которое отдает welcome страницу, обращается с нее же к собственному API, получает JSON с данными из базы и выводит их в таблицу.
Новый проект
Для начала, идем на start.spring.io и создаем проект с зависимостями Web, DevTools, JPA (доступ к реляционным базам), H2 (простая in-memory база), Mustache (движок шаблонов). Сгенерированный pom.xml
импортируем в IDEA. Все, приложение готово к запуску! Можно его запустить из командной строки командой ./mvnw spring-boot:run
или прямо из IDEA — запустив метод main. Да, серверов приложений, контейнеров и деплоймента не нужно.
Точнее, контейнер нужен — только он предоставлен и настроен Spring Boot-ом — используя Embedded Tomcat
Контроллер
Итак, наш следующий шаг — создать контроллер и вернуть "домашнюю" страницу. Код контроллера выглядит так просто, как и ожидается:
@Controller
public class IndexController {
@GetMapping("/")
public ModelAndView index() {
Map<String, String> model = new HashMap<>();
model.put("name", "Alexey");
return new ModelAndView("index", model);
}
}
Пара вещей, на которые стоит обратить внимание.
- Метод возвращает
ModelAndView
— дальше Spring знает, что нужно взять вьюindex.html
из папкиresources/templates
(это соглашение по умолчанию) и передать туда модель - Модель в нашем случае просто словарь, но это может быть и строго-типизированная модель (объект) тоже
С Котлин это бы выглядело еще лучше и проще, но это потребует введения сразу большого количества новых понятий — язык, фреймворк. Лучше начинать с малого.
Класс, помеченный как @Controller
автоматически регистрируется в MVC роутере, а используя аннотации @(Get|Post|Put|Patch)Mapping
можно регистрировать разные пути.
Все файлы из каталога
resources/static/
считаются статическими, там можно хранить CSS и картинки.
Шаблон
Мы используем Mustache (Handlebar) синтаксис, поэтому шаблон очень напоминает обычный HTML
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Welcome to Spring, {{ name }}</h1>
</body>
</html>
После компиляции проекта (⌘/Ctrl + F9) — можно сразу идти на http://localhost:8080
и увидеть созданную страницу.
Доступ к базе
Для начала, опишем нашу предметную область. Мы будем собирать статистику посещений — каждый раз, когда кто-то заходит на главную страницу, мы будем писать это в базу. Модель выглядит до крайности примитивно:
@Entity
public class Visit {
@Id
@GeneratedValue
public Long id;
public String description;
}
Предвидя череду комментариев "Как же без геттеров и сеттеров" и "Где же equals / hashCode" — эти элементы упущены сознательно с целью упрощения кода. Совершенно чудовищная ошибка дизайна Java которая заставляет писать эту ерунду (геттеры и методы сравнения), это, конечно, отдельный разговор. Котлин эту проблему, кстати, решает.
Мы снова очень активно используем аннотации — в этот раз из Spring Data (точнее, JPA — это дремучая спецификация для доступа к данным). Этот класс описывает модель с двумя полями, одно из которых генерится автоматически. По этому классу будет автоматически создана модель данных (таблицы) в БД.
Теперь для этой модели пора создать репозиторий. Это еще проще, чем контроллер.
@Repository
public interface VisitsRepository extends CrudRepository<Visit, Long> {
}
Все, репозиторий можно использовать для работы с базой — читать и писать записи. У внимательного читателя должен сработать WTF детектор — что здесь вообще происходит? Мы определяем интерфейс и внезапно он начинает работать с базой? Все так. Благодаря магии Spring Boot и Spring Data "под капотом" происходит следующее:
- Увидев в зависимостях H2 (встраиваемая БД), Boot автоматически конфигурит
DataSource
(это ключевой компонент для подключения к базе) чтобы приложение работало с этой базой - Spring Data ищет всех наследников
CrudRepository
и автоматически генерит для них дефолтные реализации, которые включают базовые методы репозитория, типаfindOne
,findAll
,save
etc. - Spring автоматически конфигурит слой для доступа к данным — JPA (точнее, его реализацию Hibernate)
- Благодаря аннотации
@Repository
этот компонент становится доступным в нашем приложении (и мы его используем через пару минут)
Чтобы использовать репозиторий в контроллере мы воспользуемся механизмом внедрения зависимостей, предоставляемый Spring Framework. Чтобы это сделать, как ни странно, нужно всего лишь объявить зависимость в нашем контроллере.
@Controller
public class IndexController {
final VisitsRepository visitsRepository;
public IndexController(VisitsRepository visitsRepository) {
this.visitsRepository = visitsRepository;
}
...
}
Увидев в нашем конструкторе параметр типа VisitRepository
, Spring найдет созданный Spring Data-ой репозиторий и передаст его в конструктор.
Теперь можно писать в базу в методе контроллера.
@GetMapping("/")
public ModelAndView index() {
Map<String, String> model = new HashMap<>();
model.put("name", "Alexey");
Visit visit = new Visit();
visit.description = String.format("Visited at %s", LocalDateTime.now());
visitsRepository.save(visit);
return new ModelAndView("index", model);
}
REST контроллер
Следующий шаг — это вернуть все записи из базы в JSON формате, чтобы потом их можно было читать на клиенте.
Для REST в Spring есть отдельный тип контроллера который называется @RestController
, код которого не сильно отличается от обычного контроллера.
@RestController
@RequestMapping("/api")
public class ApiController {
final VisitsRepository visitsRepository;
public ApiController(VisitsRepository visitsRepository) {
this.visitsRepository = visitsRepository;
}
@GetMapping("/visits")
public Iterable<Visit> getVisits() {
return visitsRepository.findAll();
}
}
На что обратить внимание:
- В этот раз мы определяем "префикс" для всех методов контроллера использую базовый
@RequestMapping
- Внедрение зависимостей работает точно так же, как и для обычных контроллеров (как и вообще для всего в Spring)
- Метод теперь возвращает не имя шаблона, а модель. Spring автоматически преобразует это в массив JSON объектов
- Мы используем persistence модель для сериализации в JSON, что в общем случае не самая лучшая практика
Теперь при запросе http://localhost:8080/api/visits
(предварительно скомпилировав проект и дав DevTools обновить приложение) мы получим JSON с нужными данными.
Клиентский код
Оставим за рамками этой статьи, пример можно увидеть в исходном коде. Цель этого кода — исключительно продемонстрировать как получить JSON данные с сервера, интеграции с клиентскими фреймворками React, Angular etc намеренно оставлены вне рамок этой статьи.
Тестирование
Spring так же предоставляет мощные средства для Integration и Unit тестирования приложения. Пример кода, который проверяет контроллер:
@Test
public void indexControllerShouldReturnHtmlPage() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Welcome to Spring")));
}
Используя абстракции типа MockMvc
можно легко тестировать внешний интерфейс приложения, в то же время имея доступ к его внутренностям. Например, можно целиком заменить компоненты приложения на моки (заглушки).
Аналогично для API тестов есть набор хелперов для проверки JsonPath выражений.
@Test
public void apiControllerShouldReturnVisits() throws Exception {
mockMvc.perform(get("/"));
mockMvc.perform(get("/api/visits"))
.andExpect(jsonPath("$.*.description", iterableWithSize(1)));
}
Тестирование в Spring это все таки отдельная тема, поэтому мы не будем сильно на этом останавливаться сейчас.
Деплоймент
Чтобы собрать и запустить наше приложение в продакшене есть несколько вариантов.
- Задеплоить полученный JAR (или даже WAR) в сервлет контейнер, например Tomcat. Это не самый простой путь, его нужно выбирать только если у вас уже есть работающий сервлет контейнер или сервер приложений.
- Использовать магию Spring Boot. JAR файл, собранный используя плагин Spring Boot (который автоматически добавляется в проекты созданные через Spring Initializr), является полностью самодостаточным.
Таким образом сборка и запуск приложения выглядит как:
./mvnw package
java -jar ./target/demo-0.0.1-SNAPSHOT.jar
Для деплоймента этого JAR файла не нужно ничего, кроме установленной Java (JRE). Это так называемый fat JAR — он включает в себя и встроенный сервлет контейнер (Tomcat по умолчанию) и фреймворк, и все библиотеки-зависимости. По сути, он является единственным артефактом деплоймтента — его можно просто копировать на целевой сервер и запускать там.
Более того, файл можно сделать "выполняемым" и запускать его просто из командной строки (Java, конечно, все равно необходима).
На базе этого файла можно легко создать Docker образ или установить его как демон. Больше деталей доступно в официальной документации.
Заключение
Получилось, все же, очень сжато — но уложить даже самый простой вводный курс по Spring в рамки одной статьи не очень просто. Надеюсь, это поможет кому-то сделать первый шаги в Spring-е, и хотя понять его фундаментальные концепции.
Как вы успели заметить, в тексте статьи много раз звучало слово "магия Spring". По сути своей, это очень "магический" фреймворк — даже взглянув на самую верхушку айсберга мы уже видели, что Spring много всего делает в фоне. Это является и плюсом, и минусом фреймворка. Плюс несомненно в том, что многие сложные вещи (очень многие) можно сделать одной аннотацией или зависимостью. Минус же это скрытая сложность — чтобы решить какие-то сложные проблемы, заставить фреймворк работать в крайних случаях или понимать все тонкости и аспекты нужно его неплохо знать.
Чтобы сделать этап "знать" как можно проще, Spring обладает отличной документацией, огромным сообществом, и чистыми исходниками, которые вполне можно читать. Если расположить Spring на шкале Рича Хики, он (Spring) несомненно попадет в easy, но уж точно не simple. Но для современного энтерпрайза (и не только энтерпрайза) он дает невероятные возможности чтобы получить production-ready приложение очень быстро и концентрироваться на логике приложения, а не инфраструктуры вокруг.
Ссылки
- Исходный код на github
- Главный сайт Spring с кучей разных гайдов spring.io
- Официальная документация Spring Framework
Автор: alek_sys