Пишем Java веб-приложение на современном стеке. С нуля до микросервисной архитектуры. Часть 1
Постараюсь описать шаги разработки приложения на Java, возникающие проблемы и пути их решения. Наращивать функциональность будем постепенно.
68К открытий75К показов
На сегодняшний день в мире разработки на Java существует огромное количество библиотек и технологий, в которых новичку очень легко запутаться. В этом руководстве я постараюсь простым языком описать все шаги, возникающие проблемы и пути их решения. Начинать будем с самого простого и постепенно наращивать функциональность.
Spring Boot
Spring Boot — один из самых популярных универсальных фреймворков для построения веб-приложений на Java. Создадим в среде разработки Gradle Project. Для облегчения работы воспользуемся сайтом https://start.spring.io, который поможет сформировать build.gradle.
Для начала нам необходимо выбрать следующие зависимости:
- Spring Web — необходим для создания веб-приложения;
- Spring Data JPA — для работы с базами данных;
- PostgreSQL Driver — драйвер для работы с PostgreSQL;
- Lombok — библиотека, позволяющая уменьшить количество повторяющегося кода.
В результате генерации build.gradle должно получиться что-то похожее:
Тот же результат можно получить и в самой IntelliJ Idea: File → New → Project → Spring Initializr.
Объявим Main-класс:
Опишем самый простой контроллер, чтобы удостовериться, что проект работает:
Запустим проект в среде разработки или через терминал: ./gradlew bootRun
.
Результат работы можно проверить в браузере перейдя по адресу http://localhost:8080/hello?name=World или с помощью консольной утилиты curl:
Наш сервис запускается и работает, пора переходить к следующему шагу.
Представим, что нам требуется разработать некий сервис для интернет-магазина по продаже книг. Это будет rest-сервис, который будет позволять добавлять, редактировать, получать описание книги. Хранить данные будем в БД Postgres.
Docker
Для хранения данных нам потребуется база данных. Проще всего запустить инстанс БД с помощью Docker. Docker позволяет запускать приложение в изолированной среде выполнения — контейнере. Поддерживается всеми операционными системами.
Выкачиваем образ БД и запускаем контейнер:
Lombok
Создадим data-класс «книга». Он будет иметь несколько полей, которые должны иметь getters, конструктор и должна быть неизменяемой (immutable). Среда разработки позволяет автоматически генерировать конструктор и методы доступа к полям, но чтобы уменьшить количество однотипного кода, будем использовать Lombok.
Аннотация @Value
при компиляции исходного кода добавит в наш класс getters, конструктор, пометит все поля класса private final
, добавит методы hashCode
, equals
и toString
.
После сборки проекта можно посмотреть, как выглядит класс после компиляции. Воспользуемся стандартной утилитой, входящей в состав JDK:
Lombok очень упрощает читаемость подобного рода классов и очень широко используется в современной разработке.
Spring Data JPA
Для работы с БД нам потребуется Spring Data JPA, который мы уже добавили в зависимости проекта. Дальше нам нужно описать классы Entity и Repository. Первый соответствует таблице в БД, второй необходим для загрузки и сохранения записей в эту таблицу.
Мы также используем аннотации Lombok: @Data
добавляет getters и setters, @NoArgsConstructor
и @AllArgsConstructor
— конструкторы без параметров и со всеми параметрами, соответственно. @Entity
, @Table
, @Id
, @GeneratedValue
— аннотации относящиеся к JPA. Здесь мы указываем, что это объект БД, название таблицы, первичный ключ и стратегию его генерации (в нашем случае автоматическую).
Класс Repository будет выглядеть совсем просто — достаточно объявить интерфейс и наследоваться от CrudRepository:
Никакой реализации не требуется. Spring всё сделает за нас. В данном случае мы сразу получим функциональность CRUD — create, read, update, delete. Функционал можно наращивать — чуть позже мы это увидим. Мы описали DAO-слой.
Теперь нам нужен некий сервис, который будет иметь примерно следующий интерфейс:
Это так называемый сервисный слой. Реализуем этот интерфейс:
Аннотацией @Service
мы возлагаем на Spring создание объекта этого класса. @RequiredArgsConstructor
— уже знакомая нам аннотация, которая генерирует конструктор с необходимыми аргументами. В нашем случае класс имеет final-поле bookRepository
, которое необходимо проинициализировать. Добавив эту аннотацию, мы получим следующую реализацию:
При создании объекта класса Spring опять всё возьмёт на себя — сам создаст объект BookRepository и передаст его в конструктор. Имея объект репозитория мы можем выполнять операции с БД:
Метод findById
возвращает объект типа Optional<BookEntity>
. Это такой специальный тип который может содержать, а может и не содержать значение. Альтернативный способ проверки на null
, но позволяющий более изящно написать код. Метод orElseThrow
извлекает значение из Optional
, и, если оно отсутствует, бросает исключение, которое создается в переданном в качестве аргумента лямбда-выражении. То есть объект исключения будет создаваться только в случае отсутствия значения в Optional
.
MapStruct
Смотря на код может показаться, что класс Book не нужен, и достаточно только BookEntity, но это не так. Book — это класс сервисного слоя, а BookEntity — DAO. В нашем простом случае они действительно повторяют друг друга, но бывают и более сложные случаи, когда сервисный слой оперирует с несколькими таблицами и соответственно DAO-объектами.
Если присмотреться, то и тут мы видим однотипный код, когда мы перекладываем данные из BookEntity в Book и обратно. Чтобы упростить себе жизнь и сделать код более читаемым, воспользуемся библиотекой MapStruct. Это mapper, который за нас будет выполнять перекладывание данных из одного объекта в другой и обратно. Для этого добавим зависимости в build.gradle:
Создадим mapper, для этого необходимо объявить интерфейс, в котором опишем методы для конвертации из BookEntity в Book и обратно:
Так как имена полей классов соотносятся один к одному, то интерфейс получился таким простым. Если поля имеют отличающиеся имена, то потребуется аннотацией @Mapping
указать какие поля соответствуют друг другу. Более подробно можно найти в документации. Чтобы spring смог сам создавать бины этого класса, необходимо указать componentModel = "spring"
.
После сборки проекта, в каталоге build/generated/sources/annotationProcessor появится сгенерированный исходный код mapper, избавив нас от необходимости писать однотипные десятки строк кода:
Воспользуемся мэппером и перепишем DefaultBookService. Для этого нам достаточно добавить добавить final-поле BookMapper, которое Lombok автоматически подставит в аргумент конструктора, а spring сам инстанциирует и передаст параметром в него:
Теперь опишем контроллер, который будет позволять выполнять http-запросы к нашему сервису. Для добавления книги нам потребуется описать класс запроса. Это data transfer object, который относится к своему слою DTO.
Нам также потребуется конвертировать объект AddBookRequest в объект Book. Создадим для этого BookToDtoMapper:
Теперь объявим контроллер, на эндпоинты которого будут приходить запросы на создание и получение книг, добавив зависимости BookService и BookToDtoMapper. При необходимости аналогично объекту AddBookRequest можно описать Response-объект, добавив соответствующий метод в мэппер, который будет конвертировать Book в GetBookResponse. Контроллер будет содержать 3 метода: методом POST мы будем добавлять книгу, методом GET получать список всех книг и книгу по идентификатору, который будем передавать в качестве PathVariable.
Осталось создать файл настроек приложения. Для Spring boot по умолчанию это application.properties
или application.yml
. Мы будем использовать формат properties. Необходимо указать настройки для соединения с БД (выше мы задавали пользователя и его пароль при старте docker-контейнера):
Настройка spring.jpa.hibernate.ddl-auto=update
указывает hibernate необходимость обновить схему когда это нужно. Так как мы не создавали никаких схем, то приложение сделает это автоматически. В процессе промышленной разработки схемы баз данных постоянно меняются, и часто используются инструменты для версионирования и применения этих изменений, например Liquibase.
Запустим наше приложение и выполним запросы на добавление книг:
После выполнения запросов в таблице books должны появиться записи. Чтобы удостовериться в этом, можно использовать любой удобный клиент БД. Для примера сделаем это, используя консольный клиент, входящий в состав docker-контейнера. При создании контейнера, мы указали его имя ‘db’ (если имя не задавалось, то можно вывести список всех запущенных контейнеров командой docker container ls
, и дальше использовать идентификатор нужного контейнера). Для доступа к шелл-оболочке выполним:
Запустим клиент БД и выполним sql-запрос:
Получим список всех книг:
Получим книгу через запрос к api нашего сервиса, указав идентификатор книги:
Добавим к нашему api более сложную функциональность — поиск по автору книги. Для этого в BookRepository нужно описать метод, который будет делать соответствующий SELECT из БД. Это можно сделать с помощью аннотации @Query
, а можно назвать метод в соответствии со специальной нотацией Spring:
В документации можно подробнее прочитать об именовании методов. Здесь мы указываем findAll
— найти все записи, ByAuthor
— параметр обрамляется %. При вызове этого метода (например с аргументом ‘Bloch’) будет сгенерирован следующий запрос:
Далее добавим метод в BookService и DefaultBookService:
А в контроллере немного модифицируем метод получения списка книг таким образом, что при передаче get-параметра author мы искали по автору, а если параметр не передётся, то используется старая логика и выводится список всех книг:
Теперь можно выполнить поиск:
Итак, мы написали веб-сервис и познакомились с очень распространенными библиотеками. В следующей части продолжим улучшать наше приложение, добавив в него авторизацию, а также напишем отдельный микросервис, который будет выписывать токены доступа.
Код проекта доступен на GitHub.
68К открытий75К показов