2. Architecture of the server application

The first part is here

In this part, we will create a workable microservice, adhering to the right architecture, which provides the qualities that are important for an industrial project: flexibility, ease of implementation of improvements and bug fixes, scalability.

After completing the following tasks, you will receive an application with this architecture.
After completing the following tasks, you will receive an application with this architecture.

Requests from the client through the transport layer fall into the service layer. Services using the classes of the DAO layer send queries to the database. Having performed the necessary operations, the service layer classes pass their result to the transport layer, where the response to the processed request is formed. Sometimes, service layer operations are initiated not on demand, but on a timer using the task scheduler.

spring boot

Spring is a standard framework for creating backend services in the Java language. Spring Boot is an extension to Spring that allows you to quickly connect typical application features (web server, database connection, security, etc.) using starters.

Include the Spring Boot libraries in the project by editing the file 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
}

Plugins make life easier by ensuring you use non-conflicting versions of the Spring Boot libraries. The spring-boot-starter-web starter will create and start a ready-to-use web server for us.

Then you need to declare and start the Spring Boot application in the file 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);
    }
}

When the application starts, we will see in the message log on the console how the Tomcat web server starts:

Please note that the application does not terminate as it did before. It runs as a service – the web server listens for requests on port 8080.

DTO – objects for data transfer

In most requests to services, some data is passed. For example, if we want to create a user, then most likely we need to pass at least a name in the request to create it. The data transfer standard in REST requests is Data Transfer Objects (DTO).

Suppose the functionality of our application will be associated with users. Then the first step is to write a request handler to create them, which means we need to create the appropriate DTO class to pass data about new users.

Add a new java-package where the DTO classes will be placed, name it web.dto:

Add a new class CreateUserDto in a package 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 Controllers – Request Handlers

The standard for writing web services is the REST architecture. It is extremely simple – the service receives http requests, processes them and sends responses. The application’s REST controllers do this.

At this stage, we have a working web server and described how data will be transferred to create a user. It’s time to write the first REST controller that will handle requests to create new users.

Create a class WebController in java package 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);
    }
}

Create a folder in the project http testand in it create a file test.http:

### Запрос на создание пользователя
POST http://localhost:8080/users
Content-type: application/json

{
  "name": "JohnDoe"
}

Run the application and then a test POST request on a file test.http

As a result of running the request, you should see the response response code: 200 – this means that the request was completed successfully. A message will be displayed in the application console: Create user request received: {name=”JohnDoe”} – this means that the request “reached” the application. But at this stage, we can’t do anything yet – we have nowhere to store users.

Data Validation

Our application already knows how to receive requests to create users. But, before proceeding with the processing of the request, it would be nice to check the received data for correctness. Suppose we want the username to be from 5 to 25 characters and contain only Latin letters.

Add a validation starter to the file 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()
}

Add validation rules to the field name in class 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); }
    }
}

Add Method Input Parameter Validation createUser in class 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);
    }
}

Run the application and try sending test requests in a file http.test with different field value options name. Make sure that the response code is 200 (success) for valid values, otherwise 400 (bad request).

Data model

So, our application is able to receive requests to create a user and checks the received data for correctness. It’s time to save the new user in the database somehow. But first, let’s create a user model class.

Create a java package modeland in it a java class UserInfo, for operations on the user entity:

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); }
    }
}

The attentive reader may notice that the class UserInfo very similar to the class CreateUserDto. No wonder – to be honest, I created it by copying, removing annotations and correcting comments. Why are there two almost identical classes in the application?

CreateUserDto is the transport layer class for data transmission, and UserInfo is a class for operating with the user entity at the business logic level. Their sameness is a temporary state. In the future, as new requirements for transport appear, the class may change CreateUserDtoand as new business functionality appears, the class will be added UserInfo. Sometimes these changes are synchronous, sometimes they are not, and the classes will start to differ more and more.

Transport classes are separated into a transport layer from the rest of the application – this is a sign of good architecture. In this case, the transport layer is in the package web. Why is this needed?

Imagine that we wrote a great user manager, but at some point the whole project was included in a platform where there is already an agreement on how user data is transferred, and you need to work according to the specified protocol. For example, the target platform uses Kafka messaging instead of HTTP, or requests with the userName key instead of the name key in their systems.

At the same time, the performance requirements under the old protocol also remain in force, for example, “during the move” or “during the implementation of the new platform”. This state can last for months or even years.

If the transport classes of your application “penetrated” somewhere outside the transport logic, then you will have to rewrite the entire application. You will have to have several versions of the application: “old” and “new”, and refine them in parallel. And this is no longer just code duplication – it threatens to duplicate the entire process of improvements: staging, development, testing.

But if you “withstood” the architecture, then a new transport service will simply appear in your application, and the business logic can be reused. How this is done will be demonstrated later.

Liquibase – creating a database and connecting to it

At this point, we already have the entity model of the user we want to store, but we don’t have the database for it yet. Let’s use the Liquibase library to create a database.

Industrial applications use PostgreSQL, Oracle, or MS SQL Server, but we’ll use H2, which is great for learning purposes and can create an ephemeral in-memory database each time the application is run.

Connect JDBC starter, Liqubase library and H2 library in file 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'
}

...

Create a file application.yml in the catalog resources, by specifying the database connection parameters:

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

In catalog resources create a directory dband in it the file changelog.xml according to which liquibase will create the user_info table for us:

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

Create a java package configurationand in it the class database configuration, which will allow the application to send queries to the database:

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

If everything is done correctly, then in the message log when the application starts, there will be an entry:

ChangeSet db/changelog.xml::user::dev ran successfully

This means that the user_db database was created in memory, and the user_info table in it, in which we will store user data.

DAO – sending queries to the database

Thanks to Liquibase, our application is provided with a database with the table we need user_info. It’s time to learn how to write data there. This is done using the Data Access Object (DAO) – a specialized class that is usually taken out in a separate DAO layer of the application.

Create a java package dao and in it a class UserInfoDao, which will be responsible for sending queries to the user_info table:

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 class UserInfoDao an auxiliary class is needed that is responsible for converting a record from a database table into a java class UserInfo. In the dao package, create a mapper package and the UserInfoRowMapper class in it:

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

We have described the DAO class, now we need to add the configuration according to which Spring Boot will create a “bean” instance of this class when the application starts. In the configuration java package, create the DaoConfiguration class:

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);
    }
}

Add to class WebController class use UserInfoDao for working with the database:

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);
    }
}

Add a test file test.http new requests:

### Запрос на создание пользователя
POST http://localhost:8080/users
Content-type: application/json

{
  "name": "JohnDoe"
}

### Запрос информации о пользователе
GET http://localhost:8080/users/JohnDoe

### Запрос на удаление пользователя
DELETE http://localhost:8080/users/JohnDoe

Execute queries sequentially. If everything is done correctly, responses with the code 200 will be received. Our application can now: save user information, return it on request, delete user information.

Service layer and business logic

The application works, but it still has a hidden architectural problem – the DAO class is accessed directly from the transport layer.

Suppose we want to avoid the appearance of users with names like “administrator”, “root”, or “system”. Or, before sending requests to create and delete a user, it would be nice to check its presence in the database.

You cannot write this logic in the transport layer – when a new transport appears, this code fragment will have to be duplicated.

Add this check to UserInfoDao also not worth it because:

  1. The principle of single responsibility is violated, the class begins to lose its specialization “working with the user_info table”

  2. DAO classes are also, in a sense, a “detail” of the application, which may have to be replaced or supplemented when moving to a new DBMS. This will be more difficult to do if the code is heavy with some additional logic other than sending a SQL query.

To write such “business logic”, it would be correct to create a separate “service” layer – this is the semantic core of the application, around which relatively easily replaceable “details” revolve: transport, database, clients of other services, etc.

Add java package service and create a class in it UserInfoService, which will be responsible for business operations on the user entity:

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

Replace use of dao object with use of service class in 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);
    }
}

Run three test queries in sequence. If everything is done correctly, responses with the code 200 will be received. Pay attention to the entries in the application console:

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 John Doe deleted

They can be very helpful in diagnosing problems. General recommendations for logging are as follows:

  1. An informational message immediately upon receipt of a request with the output of the contents of the request.

  2. Informational message about the success of the operation before exiting the method in the service class.

  3. The error message immediately after the catch. Don’t forget to show the exception itself.

  4. Error message before throw if there was no catch

  5. Debug messages in complex algorithms

To be continued

Similar Posts

Leave a Reply Cancel reply