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

Запросы от клиента через транспортный слой попадают в слой сервисов. Сервисы при помощи классов DAO-слоя посылают запросы к базе данных. Совершив необходимые операции, классы сервисного слоя передают их результат в транспортный слой, где формируется ответ на обработанный запрос. Иногда, операции сервисного слоя инициируются не по запросу, а по таймеру при помощи планировщика заданий.
Spring Boot
Spring – это стандартный framework для создания backend-сервисов на языке Java. Spring Boot - это расширение Spring, которое позволяет быстро подключать типовые функции приложения (web-сервер, подключение к базе данных, безопасность и т.д.) при помощи «стартеров».
Подключите библиотеки Spring Boot в проект, отредактировав файл build.gradle:
plugins { id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" } group 'org.example' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } test { useJUnitPlat }
Плагины облегчают жизнь, гарантируя использование неконфликтующих между собой версий библиотек Spring Boot. Стартер spring-boot-starter-web создаст и запустит для нас готовый к эксплуатации web-сервер.
Затем необходимо задекларировать и стартовать Spring Boot приложение в файле Main.java:
package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Декларируем Spring Boot приложение @SpringBootApplication public class Main { public static void main(String[] args) { // Стартуем приложение SpringApplication.run(Main.class, args); } }
При старте приложения мы увидим в журнале сообщений н�� консоли, как стартует web-сервер Tomcat:

Обратите внимание, приложение не завершается, как это было раньше. Оно работает как сервис - web-сервер ожидает запросов на порту 8080.
DTO – объекты для передачи данных
В большинстве запросов к сервисам передаются какие-то данные. Например, если мы хотим создать пользователя, то скорее всего нам надо передать в запросе на его создание хотя бы имя. Стандартом передачи данных в REST запросах являются Data Transfer Objects (DTO).
Предположим, функциональность нашего приложения будет связана с пользователями. Тогда первым делом надо написать обработчик запросов на их создание, а значит нам нужно создать соответствующий DTO-класс, чтобы передавать данные о новых пользователях.
Добавьте новый java-package, где будут размещаться DTO-классы, назовите его web.dto:

Добавьте новый класс CreateUserDto в пакет web.dto:

package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; /** * Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида {"name": "John Doe"} в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto { /** Имя пользователя */ private final String name; public static Builder builder() { return new Builder(); } /** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) { this.name = builder.name; } public String getName() { return name; } /** * Используется при выводе сообщений на экран */ @Override public String toString() { return "{" + "name='" + name + '\'' + '}'; } /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder { private String name; public Builder setName(String name) { this.name = name; return this; } public CreateUserDto build() { return new CreateUserDto(this); } } }
REST-контроллеры - обработчики запросов
Стандартом для написания web-сервисов является REST-архитектура. Она крайне проста – сервис получает http-запросы, обрабатывает их и отправляет ответы. Занимаются этим REST-контроллеры приложения.
На данном этапе у нас есть работающий web-сервер и описано, как будут передаваться данные для создания пользователя. Пришла пора написать первый REST-контроллер, который будет обрабатывать запросы на создание новых пользователей.
Создайте класс WebController в java-пакете web:
package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * Обработчик web-запросов */ @RestController public class WebController { /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@RequestBody CreateUserDto createUserDto) { /** * Получили запрос на создание пользователя, * пока можем только залогировать этот факт */ LOGGER.info("Create user request received: {}", createUserDto); } }
Создайте в проекте папку http-test, а в ней создайте файл test.http:
### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json { "name": "JohnDoe" }
Запустите приложение, а затем тестовый POST-запрос в файле test.http
В результате запуска запроса вы должны увидеть ответ Response code: 200 – это значит, что запрос выполнился успешно. В консоли приложения будет выведено сообщение: Create user request received: {name='JohnDoe'} – это значит, что запрос «дошел» до приложения. Но на данном этапе мы пока не можем ничего сделать – нам негде хранить пользователей.
Валидация данных
Наше приложение уже умеет получать запросы на создание пользователей. Но, прежде чем приступить к обработке запроса, было бы неплохо проверить пришедшие данные на корректность. Предположим, мы хотим, чтобы в имени пользователя было от 5 до 25 символов и прису��ствовали только буквы латинского алфавита.
Добавьте стартер валидации в файл build.gradle:
plugins { id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" } group 'org.example' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' // Стартер для валидации implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } test { useJUnitPlatform() }
Добавьте правила валидации на поле name в классе CreateUserDto:
package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; /* Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида {"name": "John Doe"} в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto { /** * Имя пользователя * Ключ "name" - обязательный * Длина - от 5 до 25 символов * Может содержать только символы латинского алфавита */ @NotNull(message = "Key 'name' is mandatory") @Length(min = 5, max = 25, message = "Name length must be from 5 to 25") @Pattern(regexp = "^[a-zA-Z]+$", message = "Name must contain only letters a-z and A-Z") private final String name; public static Builder builder() { return new Builder(); } /** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) { this.name = builder.name; } public String getName() { return name; } /** * Используется при выводе сообщений на экран */ @Override public String toString() { return "{" + "name='" + name + '\'' + '}'; } /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder { private String name; public Builder setName(String name) { this.name = name; return this; } public CreateUserDto build() { return new CreateUserDto(this); } } }
Добавьте проверку входного параметра метода createUser в классе WebController:
package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController { /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) { /* Получили запрос на создание пользователя, пока можем только залогировать этот факт */ LOGGER.info("Create user request received: {}", createUserDto); } }
Запустите приложение и попробуйте послать тестовые запросы в файле http.test с разными вариантами значения поля name. Убедитесь, что при допустимых значениях код ответа равен 200 (успех), иначе – 400 (некорректный запрос).
Модель данных
Итак, наше приложение умеет получать запросы на создание пользователя и проверяет полученные данные на корректность. Пора каким-то образом сохранить нового пользователя в базе данных. Но прежде создадим класс модели пользователя.
Создайте java-пакет model, а в нем java-класс UserInfo, для операций над сущностью пользователя:
package org.example.model; /* Информация о пользователе */ public class UserInfo { /** * Имя пользователя */ private final String name; public static Builder builder() { return new Builder(); } /** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = User.builder().setName("John Doe").build() */ private UserInfo(Builder builder) { this.name = builder.name; } public String getName() { return name; } /** * Используется при выводе сообщений на экран */ @Override public String toString() { return "{" + "name='" + name + '\'' + '}'; } public static class Builder { private String name; public Builder setName(String name) { this.name = name; return this; } public UserInfo build() { return new UserInfo(this); } } }
Внимательный читатель может заметить, что класс UserInfo очень похож на класс CreateUserDto. Неудивительно – если честно, я создал его копированием, удалив аннотации и поправив комментарии. Зачем в приложении два почти одинаковых класса?
CreateUserDto – это класс транспортного слоя для передачи данных, а UserInfo – это класс для оперирования сущностью пользователя на уровне бизнес-логики. Их одинаковость – это временное состояние. В дальнейшем, по мере появления новых требований к транспорту, может меняться класс CreateUserDto, а по мере появления нового бизнес-функционала будет дополнятся класс UserInfo. Иногда эти изменения синхронны, иногда – нет, и классы начнут все больше отличаться.
Транспортные классы отделены в транспортный слой от остального приложения – это признак хорошей архитектуры. В данном случае транспортный слой находится в пакете web. Зачем это нужно?
Представьте, что мы написали отличный менеджер пользователей, но в какой-то момент весь проект включили в платформу, где уже есть соглашение о том, как передаются данные о пользователях, и вам надо работать по указанному протоколу. Например, в целевой платформе вместо HTTP используется обмен сообщениями при помощи Kafka, или вместо ключа name в их системах ходят запросы с ключом userName.
При этом требования к работоспособности по старому протоколу тоже остаются в силе, например, «на время переезда» или «на время внедрения новой платформы». Это состояние может продлиться месяцы, а то и годы.
Если транспортные классы вашего приложения «проникли» куда-то за пределы транспортной логики, то придется переписывать все приложение. Придется иметь несколько версий приложения: «старую» и «новую», и дорабатывать их параллельно. А это уже не просто дублирование кода – это грозит дублированием в��его процесса ведения доработок: постановка, разработка, тестирование.
Но если вы «выдержали» архитектуру, то в вашем приложении просто появится новый транспортный сервис, а бизнес-логику можно будет переиспользовать. Как это делается, будет продемонстрировано позже.
Liquibase - создание базы данных и подключение к ней
На этом этапе у нас уже есть модель сущности пользователя, которого мы хотим сохранить, но пока нет базы данных для этого. Воспользуемся библиотекой Liquibase для создания базы данных.
В промышленных приложениях используют СУБД PostgreSQL, Oracle или MS SQL Server, но мы воспользуемся H2, которая отлично подходит для учебных целей и может создать эфемерную базу данных в оперативной памяти при каждом запуске приложения.
Подключите стартер JDBC, библиотеку Liqubase и библиотеку H2 в файле build.gradle:
... dependencies { ... // Стартер jdbc implementation 'org.springframework.boot:spring-boot-starter-jdbc' // Библиотеки Liquibase implementation 'org.liquibase:liquibase-core:4.9.1' // Библиотеки H2 implementation 'com.h2database:h2:2.1.212' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } ...
Создайте файл application.yml в каталоге resources, указав параметры подключения к базе данных:
db: driverClassName: org.h2.Driver url: jdbc:h2:mem:user_db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS user_db username: admin password: admin maxPoolSize: 10
В каталоге resources создайте каталог db, а в нем файл changelog.xml, по которому liquibase создаст для нас таблицу user_info:
<?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"> <changeSet id="user" author="dev"> <sql> create table user_info ( name varchar(25) primary key ); </sql> </changeSet> </databaseChangeLog>
Создайте java-пакет configuration, а в нем класс DataBaseConfiguration, который обеспечит для приложения возможность отправлять запросы к базе данных:
package org.example.configuration; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; import javax.sql.DataSource; /** * Конфигурация компонентов для работы с БД */ @Configuration @EnableTransactionManagement public class DatabaseConfiguration { @Value("${db.driverClassName}") private String driver = "org.postgresql.Driver"; @Value("${db.maxPoolSize}") private int poolLimit = 10; private final String dbUrl; private final String userName; private final String userPassword; @Autowired public DatabaseConfiguration(@Value("${db.username}") String userName, @Value("${db.password}") String userPassword, @Value("${db.url}") String dbUrl) { this.userName = userName; this.userPassword = userPassword; this.dbUrl = dbUrl; } @Bean(destroyMethod = "close") public HikariDataSource hikariDataSource() { HikariConfig config = new HikariConfig(); config.setDriverClassName(driver); config.setJdbcUrl(dbUrl); config.setUsername(userName); config.setPassword(userPassword); config.setMaximumPoolSize(poolLimit); return new HikariDataSource(config); } @Bean public TransactionAwareDataSourceProxy transactionAwareDataSource() { return new TransactionAwareDataSourceProxy(hikariDataSource()); } @Bean public DataSourceTransactionManager dataSourceTransactionManager() { return new DataSourceTransactionManager(transactionAwareDataSource()); } @Bean public TransactionTemplate transactionTemplate() { return new TransactionTemplate(dataSourceTransactionManager()); } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(hikariDataSource()); } @Bean public NamedParameterJdbcTemplate namedParameterJdbcTemplate() { return new NamedParameterJdbcTemplate(jdbcTemplate()); } @Bean @ConfigurationProperties(prefix = "spring.datasource.liquibase") public LiquibaseProperties mainLiquibaseProperties() { LiquibaseProperties liquibaseProperties=new LiquibaseProperties(); liquibaseProperties.setChangeLog("classpath:/db/changelog.xml"); return liquibaseProperties; } @Bean public SpringLiquibase springLiquibase() { LiquibaseProperties liquibaseProperties = mainLiquibaseProperties(); return createSpringLiquibase(hikariDataSource(), liquibaseProperties); } private SpringLiquibase createSpringLiquibase(DataSource source, LiquibaseProperties liquibaseProperties) { return new SpringLiquibase() { { setDataSource(source); setDropFirst(liquibaseProperties.isDropFirst()); setContexts(liquibaseProperties.getContexts()); setChangeLog(liquibaseProperties.getChangeLog()); setDefaultSchema(liquibaseProperties.getDefaultSchema()); setChangeLogParameters(liquibaseProperties.getParameters()); setShouldRun(liquibaseProperties.isEnabled()); setRollbackFile(liquibaseProperties.getRollbackFile()); setLabels(liquibaseProperties.getLabels()); } }; } }
Если все сделано правильно, то в журнале сообщений при старте приложения будет запись:
ChangeSet db/changelog.xml::user::dev ran successfully
Это означает, что в памяти была создана база данных user_db, а в ней таблица user_info, в которой мы будем сохранять данные о пользователях.
DAO - отправка запросов к базе данных
Благодаря Liquibase наше приложение обеспечено базой данных с необходимой нам таблицей user_info. Пришла пора научиться записывать туда данные. Делается это при помощи Data Access Object (DAO) – специализированного класса, который принято выносить в отдельный DAO-слой приложения.
Создайте java-пакет dao и в нем класс UserInfoDao, который будет отвечать за отправку запросов к таблице user_info:
package org.example.dao; import org.example.dao.mapper.UserInfoRowMapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Запросы к таблице user_info */ public class UserInfoDao { /** * Объект для отправки SQL-запросов к БД */ private final NamedParameterJdbcTemplate jdbcTemplate; public UserInfoDao(NamedParameterJdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } /** * Создает запись о пользователе в БД * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) { jdbcTemplate.update( "INSERT INTO user_info (name) VALUES (:name) ", new MapSqlParameterSource("name", userInfo.getName()) ); } /** * Возращает информацию о пользователе по имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserByName(String userName) { return jdbcTemplate.queryForObject("SELECT * FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName), new UserInfoRowMapper() ); } /** * Удаляет пользователя из БД * @param userName имя пользователя */ public void deleteUser(String userName) { jdbcTemplate.update( "DELETE FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName) ); } }
DAO-классу UserInfoDao необходим вспомогательный класс, отвечающий за преобразование записи из таблицы БД в java-класс UserInfo. В пакете dao создайте пакет mapper и в нем класс UserInfoRowMapper:
package org.example.dao.mapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; /** * Трансляция записи из таблицы user_info в java-класс UserInfo * * Используется в {@link org.example.dao.UserInfoDao} */ public class UserInfoRowMapper implements RowMapper<UserInfo> { /** * Возвращает информацию о пользователе * @param rs запись в таблице user_info * @param rowNum номер записи * @return информация о пользователе * @throws SQLException если в таблице нет колонки */ @Override public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException { return UserInfo.builder() .setName(rs.getString("name")) .build(); } }
Мы описали DAO-класс, теперь надо добавить конфигурацию, по которой Spring Boot создаст при старте приложения “bean” – экземпляр этого класса. В java-пакете configuration создайте класс DaoConfiguration:
package org.example.configuration; import org.example.dao.UserInfoDao; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Создание "бинов" DAO-классов */ @Configuration public class DaoConfiguration { @Bean UserInfoDao userInfoDao(NamedParameterJdbcTemplate jdbcTemplate) { return new UserInfoDao(jdbcTemplate); } }
Добавьте в класс WebController использование класса UserInfoDao для работы с БД:
package org.example.web; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController { /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для операциями с БД * TODO: Позже надо перейти на использование сервисного слоя */ private final UserInfoDao userInfoDao; /** * Инъекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoDao userInfoDao) { this.userInfoDao = userInfoDao; } /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) { LOGGER.info("Create user request received: {}", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoDao.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); } /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/{userName}") public UserInfo getUserInfo(@PathVariable String userName) { return userInfoDao.getUserByName(userName); } /** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/{userName}") public void deleteUser(@PathVariable String userName) { userInfoDao.deleteUser(userName); } }
Дополните тестовый файл test.http новыми запросами:
### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json { "name": "JohnDoe" } ### Запрос информации о пользователе GET http://localhost:8080/users/JohnDoe ### Запрос на удаление пользователя DELETE http://localhost:8080/users/JohnDoe
Выполните запросы последовательно. Если все сделано правильно, будут получены ответы с кодом 200. Наше приложение теперь умеет: сохранять информацию о пользователе, возвращать ее по запросу, удалять информацию о пользователе.
Сервисный слой и бизнес-логика
Приложение работает, но имеет пока скрытую архитектурную проблему – обращение к DAO-классу происходит напрямую из транспортного слоя.
Предположим, мы хотим избежать появления пользователей с именами типа «administrator», «root» или «system». Или, прежде чем посылать запросы на создание и удаление пользователя, неплохо было бы проверить его наличие в БД.
Писать эту логику в транспортном слое нельзя – при появлении нового транспорта, этот фрагмент кода придется дублировать.
Добавлять эту проверку в UserInfoDao тоже не стоит потому что:
Нарушается принцип единой ответственности, класс начинает терять свою специализацию «работа с таблицей user_info»
DAO-классы – это тоже, в известном смысле, «деталь» приложения, которую, возможно, придется заменить или дополнить при переходе на новую СУБД. Это будет сложнее сделать, если код утяжелен какой-то дополнительной логикой, кроме отсылки SQL-запроса.
Для написания подобной «бизнес-логики» правильно будет создать отдельный «сервисный» слой – это смысловое ядро приложения, вокруг которого крутятся сравнительно легко заменяемые «детали»: транспорт, база данных, клиенты других сервисов т.д.
Добавьте java-пакет service и создайте в нем класс UserInfoService, который будет отвечать за бизнес-операции над сущностью пользователя:
package org.example.service; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.EmptyResultDataAccessException; import java.util.Set; /** * Бизнес-логика работы с пользователями */ public class UserInfoService { private static final Logger LOGGER = LoggerFactory.getLogger(UserInfoService.class); /** * Объект для работы с таблице user_info */ private final UserInfoDao userInfoDao; /** * Иньекция испольуземых объектов через конструктор * @param userInfoDao объект для работы с таблице user_info */ public UserInfoService(UserInfoDao userInfoDao) { this.userInfoDao = userInfoDao; } /** * Создание пользователя * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) { checkNameSuspicious(userInfo.getName()); if (!isUserExists(userInfo.getName())) { userInfoDao.createUser(userInfo); LOGGER.info("User created by user info: {}", userInfo); } else { // TODO Заменить на своё исключение RuntimeException exception = new RuntimeException("User already exists with name " + userInfo.getName()); LOGGER.error("Error creating user by user info {}", userInfo, exception); throw exception; } } /** * Возвращает информацию о пользователе по его имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserInfoByName(String userName) { try { return userInfoDao.getUserByName(userName); } catch (EmptyResultDataAccessException e) { LOGGER.error("Error getting info by name {}", userName, e); // TODO Заменить на своё исключение throw new RuntimeException("User not found by name " + userName); } } /** * Удаление пользователя * @param userName имя пользователя */ public void deleteUser(String userName) { if (isUserExists(userName)) { userInfoDao.deleteUser(userName); LOGGER.info("User with name {} deleted", userName); } } /** * Проверка на сущестование пользователя с именем * @param userName имя пользователя * @return true - если пользователь сущестует, иначе - false */ private boolean isUserExists(String userName) { try { userInfoDao.getUserByName(userName); return true; } catch (EmptyResultDataAccessException e) { return false; } } /** * Проверка на то, что имя пользователя не содержится в стоп-листе * @param userName имя пользователя */ private void checkNameSuspicious(String userName) { if (Set.of("administrator", "root", "system").contains(userName)) { // TODO: Заменить на свое исключение RuntimeException exception = new RuntimeException(userName + " is unacceptable"); LOGGER.error("Check name failed", exception); throw exception; } } }
Замените использование dao-объекта на использование сервисного класса в WebController:
package org.example.web; import org.example.model.UserInfo; import org.example.service.UserInfoService; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController { /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для работы с информацией о пользователе */ private final UserInfoService userInfoService; /** * Иньекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoService userInfoService) { this.userInfoService = userInfoService; } /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) { LOGGER.info("Create user request received: {}", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoService.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); } /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/{userName}") public UserInfo getUserInfo(@PathVariable String userName) { LOGGER.info("Get user info request received userName={}", userName); return userInfoService.getUserInfoByName(userName); } /** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/{userName}") public void deleteUser(@PathVariable String userName) { LOGGER.info("Delete user info request received userName={}", userName); userInfoService.deleteUser(userName); } }
Запустите последовательно три тестовых запроса. Если все сделано правильно, будут получены ответы с кодом 200. Обратите внимание на записи в консоли приложения:
Create user request received: {name='JohnDoe'}
User created by user info: {name='JohnDoe'}
Get user info request received userName=JohnDoe
Delete user info request received userName=JohnDoe
User with name JohnDoe deleted
Они могут быт очень полезными при диагностике неполадок. Общие рекомендации по логгированию такие:
Информационное сообщение сразу при получении запроса с выводом содержимого запроса.
Информационное сообщение об успешности операции перед выходом из метода в сервисном классе.
Сообщение об ошибке сразу после catch. Не забудьте показать само исключение.
Сообщение об ошибке перед throw, если не было catch
Отладочные сообщения в сложных алгоритмах
Продолжение следует
