About the confusion seen in startup Java servers

In this article, we'll look at some of the ways these services can be better directed in the confusing situations found in startup Java servers.

Use Controller Base Class and Service Base Class

Introduction of Base Class

** Controller Base Class **

/** Controller Base Classes */
public class BaseController {    
    /** Injection services related */
    /** User Service */
    @Autowired
    protected UserService userService;
    ...

    /** Static constant correlation */
    /** Phone number mode */
    protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
    ...

    /** Static function related */
    /** Verify phone number */
    protected static vaildPhone(String phone) {...}
    ...
}

The common controller-based class mainly contains injection services, static constants, and static functions, and all controllers inherit these resources from the controller-based class and can use them directly in their functions. I will.

** Service Base Class ** The general Service Base Class is as follows.

/** Service Base Classes */
public class BaseService {
    /** Injection DAO related */
    /** User DAO */
    @Autowired
    protected UserDAO userDAO;
    ...

    /** Injection services related */
    /** SMS service */
    @Autowired
    protected SmsService smsService;
    ...
    
    /** Injection parameters related */
    /** system name */
    @Value("${example.systemName}")
    protected String systemName;
    ...

    /** Injection constant related */
    /** super user ID */
    protected static final long SUPPER_USER_ID = 0L;
    ...

    /** Service function related */
    /** Get user function */
    protected UserDO getUser(Long userId) {...}
    ...

    /** Static function related */
    /** Get user name */
    protected static String getUserName(UserDO user) {...}
    ...
}

Common service-based classes are mainly injection data access object (DAO), injection It includes services, injection parameters, static constants, service functions, and static functions, and all services inherit these resources from service-based classes and can be used directly in the functions.

Need for base class

First, Liskov Substitution Principle (LSP )Let's look at.

According to the LSP, you must make the objects of that subclass transparently available everywhere you reference the base class (superclass).

Next, let's take a look at the advantages of the base class.

  1. As for the base class, the subclass has all the methods and attributes of the superclass, so the workload for creating the subclass is reduced.
  2. The base class improves code reusability because the subclass has all the functions of the superclass.
  3. The base class improves the scalability of the code because subclasses can add their own functions.

Therefore, we can draw the following conclusions.

  1. Controller-based classes and service-based classes are not used directly anywhere in the project and will not be replaced by their subclasses. Therefore, they are not LSP compliant.
  2. Controller-based classes and service-based classes do not have abstract interface functions or virtual functions. That is, all subclasses that inherit from the base class do not have common characteristics. As a result, what is used in the project remains a subclass.
  3. Controller base classes and service base classes focus only on reusability. This means that subclasses can conveniently use base class resources such as injection DAOs, injection services, injection parameters, static constants, service functions, and static functions. However, controller-based and service-based classes ignore the need for these resources. That is, these resources are not essential for subclasses. As a result, it slows down performance when subclasses are loaded.

The bottom line is that both controller-based and service-based classes fall into miscellaneous classes. These are not really base classes and need to be split.

How to split the base class

Since the service base class is more typical than the controller base class, this article will explain how to divide the "base class" using the service base class as an example.

** Put the injection instance in the implementation class ** Inject the DAO, services, parameters, etc. to be used into the implementation class according to the principle of "introduce the class only when it is used and delete it when it is not needed".

/** Udser Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** SMS service */
    @Autowired
    private SmsService smsService;

    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    ...
}

** Put static constants in constant class ** Encapsulate static constants in the corresponding constant class and use them directly when needed.

/** example constant class */
public class ExampleConstants {
    /** super user ID */
    public static final long SUPPER_USER_ID = 0L;
    ...
}

** Put the service function in the service class ** Encapsulate the service function in the corresponding class of service. If you want to use another class of service, you can inject an instance of this class of service and call service functions through the instance.

/** User service class */
@Service
public class UserService {
    /** Ger user function */
    public UserDO getUser(Long userId) {...}
    ...
}

/** Company service class */
@Service
public class CompanyService {
    /** User service */
    @Autowired
    private UserService userService;
    
    /** Get the administrator */
    public UserDO getManager(Long companyId) {
        CompanyDO company = ...;
        return userService.getUser(company.getManagerId());
    }
    ...
}

** Put static functions in tool class ** Encapsulate static functions in the corresponding tool class and use them directly when needed.

/** User Aid Class */
public class UserHelper {
    /** Get the user name */
    public static String getUserName(UserDO user) {...}
    ...
}

Business code is written in controller class

Explanation of the phenomenon

I often see code like the following in the controller class.

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    @ResponseBody
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        // Get user information
        UserDO userDO = userDAO.getUser(userId);
        if (Objects.isNull(userDO)) {
            return null;
        }
        
        // Copy and return the user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return Result.success(userVO);
    }
    ...
}

The compiler might explain that you could write it this way because the interface function is simple and you don't need to encapsulate the interface function in the service function, but in reality it encapsulates the interface function in the service function. There is no need to interface.

Special case

In this special case, the code looks like this:

/** Test Controller Class */
@Controller
@RequestMapping("/test")
public class TestController {
    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    
    /** Access function */
    @RequestMapping(path = "/access", method = RequestMethod.GET)
    public String access() {
        return String.format("You're accessing System (%s)!", systemName);
    }
}

The access results are as follows.

curl http://localhost:8080/test/access

You are accessing System (null)!

You may be asked why the systemName parameter is not injected. By the way, the document of Spring is as follows. There is an explanation.

The actual processing of @Value annotation is [BeanPostProcessor](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html?spm= Note that it is run by a2c65.11461447.0.0.7a631744QsPCA3).

The BeanPostProcessor interface is scoped on a per-container basis. This is only relevant if you are using a container hierarchy. If you define BeanPostProcessor in one container, the work is performed only for the beans in that container. Beans defined in one container are not post-processed by the BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

According to these explanations, @Value is processed via BeanPostProcessor and WebApplicationContex //docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/WebApplicationContext.html?spm=a2c65.11461447.0.0.7a631744QsPCA3) and [ApplicationContext](https: / /docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html?spm=a2c65.11461447.0.0.7a631744QsPCA3) is processed separately. Therefore, WebApplicationContex cannot use the attribute value of the parent container.

The controller does not meet the service requirements. Therefore, it is inappropriate to write business code in the controller class.

Three-tier server architecture

SpringMVC Server is presentation layer, business layer, persistence It uses the classic three-layer architecture of layers, using @Controller, @Service, and @Repository for class annotation.

image.png

--Presentation layer: Also called the controller layer. This layer is responsible for receiving requests from clients and responding to clients with results from clients. HTTP is often used at this layer. --Business layer: Also called service layer. This layer is in charge of business-related logic processing, and is divided into services and jobs by function. --Persistence layer: Also known as repository layer. This tier is responsible for data persistence and is used by business tiers to access caches and databases.

Therefore, writing business code in the controller class does not comply with the Spring MVC server's three-tier architecture specification.

Persistence layer code to service class

On the functional side, I think it's okay to write persistence layer code in the service class. That's why many users accept this coding method.

Main problems

  1. The business layer and persistence layer are mixed and do not comply with the Spring MVC server's 3-layer architecture specifications.
  2. The complexity of business logic increases because statements and primary keys are constructed with business logic.
  3. It is difficult to replace the third-party persistence middleware because the third-party middleware is used directly in the business logic.
  4. Also, the code of the persistence layer of the same object is scattered in various business logics, and [Object-Oriented Programming](https://www.webopedia.com/TERM/O/object_oriented_programming_OOP.html?spm=a2c65. 11461447.0.0.7a631744Qs Contrary to the principle of PCA3).
  5. If you write a unit test case with this coding method, you cannot directly test the interface function of the persistence layer.

Database code is written by the service

Here, the direct query of the database persistence middleware Hibernate is explained as an example.

** Explanation of the phenomenon **

/** User Service Class */
@Service
public class UserService {
    /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Assemble HQL statement
        String hql = "from t_user where emp_id = '" + empId + "'";
        
        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

** Recommended solution **

/** User DAO CLass */
@Repository
public class UserDAO {
     /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;
    
    /** Get user function based on job number */
    public UserDO getUserByEmpId(String empId) {
        // Assemble HQLstatement
        String hql = "from t_user where emp_id = '" + empId + "'";
        
        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Return user information
        return userList.get(0);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Query user based on job number
        UserDO userDO = userDAO.getUserByEmpId(empId);
        if (Objects.isNull(userDO)) {
            return null;
        }
        
        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return userVO;
    }
}

** About plugins ** AliGenerator was developed by Alibaba [MyBatis Generator] ](Https://mybatis.org/generator/?spm=a2c65.11461447.0.0.7a631744QsPCA3) -based tool that automatically generates code for the DAO (Data Access Object) layer. In the code generated by AliGenerator, when executing a complex query, it is necessary to construct the query condition in the business code. As a result, the business code becomes particularly bloated.

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserVO getUser(String companyId, String empId) {
        // Query database
        UserParam userParam = new UserParam();
        userParam.createCriteria().andCompanyIdEqualTo(companyId)
            .andEmpIdEqualTo(empId)
            .andStatusEqualTo(UserStatus.ENABLE.getValue());
        List<UserDO> userList = userDAO.selectByParam(userParam);
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Convert and return users
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

Personally, I don't like using plugins to generate code for DAO layers. Instead, I prefer to use the original MyBatis XML for the mapping.

--The plugin may import incompatible code into your project. --To execute a simple query, you need to import the complete set of complex code. --For complex queries, the code for constructing conditions is complex and unintuitive. It is better to write the SQL statement directly in XML. --After modifying the table, you need to regenerate and overwrite the code, in the meantime you can accidentally drop a user-defined function (UDF).

If you choose to use a plugin, you should enjoy the benefits it brings while also accepting the disadvantages of the plugin.

Redis code to service class

Description

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** User primary key mode */
    private static final String USER_KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public void saveUser(UserVO user) {
        // Convert user information
        UserDO userDO = transUser(user);

        // Save Redis user
        String userKey = MessageFormat.format(USER_KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(userKey, fieldMap);

        // Save database user
        userDAO.save(userDO);
    }
}

** Recommended solution **

/** User Redis Class */
@Repository
public class UserRedis {
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** Primary key mode */
    private static final String KEY_PATTERN = "hash::user::%s";
    
    /** Save user function */
    public UserDO save(UserDO user) {
        String key = MessageFormat.format(KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, fieldMap);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** User Redis */
    @Autowired
    private UserRedis userRedis;

    /** Save user function */
    public void saveUser(UserVO user) {
        //Personification for transformation
        UserDO userDO = transUser(user);

        // Save Redis user
        userRedis.save(userDO);

        // Save database user
        userDAO.save(userDO);
    }
}

Encapsulates Redis object-related operational interfaces in DAO classes. It adheres to the Spring MVC server's object-oriented programming principles and 3-tier architecture specifications, making code easier to manage and maintain.

Database model class is exposed to interface

Explanation of symptoms

/** User DAO Class */
@Repository
public class UserDAO {
    /** Get user function */
    public UserDO getUser(Long userId) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserDO getUser(Long userId) {
        return userDAO.getUser(userId);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get user function */
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        UserDO user = userService.getUser(userId);
        return Result.success(user);
    }
}

The predecessor code seems to conform to the Spring MVC server's three-tier architecture. The only problem is that the database model UserDO is exposed directly to the external interface.

Existing problems and solutions

** Existing problems **

  1. The database table design is indirectly exposed, which is useful for analyzing competing products.
  2. If no field restrictions are imposed on database queries, the amount of interface data will be huge and waste valuable user traffic.
  3. If no field restrictions are imposed on database queries, sensitive fields can easily be exposed to the interface, creating data security issues.
  4. If the database model class cannot meet the requirements of the interface, you need to add other fields to the database model class, resulting in a mismatch between the database model class and the database fields.
  5. If the interface documentation is not properly maintained, reading the code will not help you identify which fields in the database model class are being used by the interface. This makes the code less maintainable.

solution

  1. From a management system perspective, the database model class must be completely independent of the interface model class.
  2. Due to the structure of the project, the developer must not expose the database model class to the interface.

3 ways to build a project

The following describes how to build Java projects more scientifically to effectively prevent developers from exposing database model classes to the interface.

** Method 1: Build a project with a shared model ** Place all model classes in one model project (example-model). All other projects (example-repository, example-service, example-website, etc.) rely on example-model. The relationship diagram is as follows.

image.png

image.png

risk The presentation layer project (example-webapp) can call any service function of the business layer project (example-service), and the DAO function of the persistence layer project (example-repository) can be directly applied across the business layers. You can also call it.

** Method 2: Build a project with a separate model ** Build an API project (example-api) separately to abstract the external interface and its model VO class. The business layer project (example-service) implements these interfaces and provides services to the presentation layer project (example-webapp). The presentation layer project (example-webapp) calls only the service interfaces defined in the API project (example-api).

image.png

image.png

risk The presentation layer project (example-webapp) can still call the internal service function of the business layer project (example-service) and the DAO function of the persistence layer project (example-repository). To avoid this situation, the management system should allow the presentation layer project (example-webapp) to call only the service interface functions defined by the API project (example-api).

** Method 3: Build a service-oriented project ** Package the business tier project (example-service) and persistence tier project (example-repository) into a service using the Dubbo project (example-dubbo). Provides interface functionality defined in API projects (example-api) for business layer projects (example-webapp) or other business projects (other-service).

image.png

image.png

Note: The Dubbo project (example-dubbo) releases only the service interfaces defined in the API project (example-api). This ensures that the database model is not exposed. Business layer projects (eg-webapp) and other business projects (other services) depend only on API projects (eg-api) and can only call service interfaces defined in the API project.

Less recommended suggestions

Some users may have the following considerations: Considering that the interface model and the persistent layer model are separated, if the interface model defines the VO class of the data query model, the persistent layer model also needs to define the DO class of the data query model. There will be. Also, if the interface model defines the VO class of the data return model, the persistence layer model must also define the DO class of the data return model. However, this is not well suited for rapid iterative development early in the project. In addition, the following questions also arise. Is it possible to let the interface data model be used by the persistence layer without exposing the persistence layer data model through the interface?

This method is unacceptable as it affects the independence of the Spring MVC server's three-tier architecture. However, this method does not expose the database model class and is acceptable for rapid iterative development. Therefore, this is a less recommended proposal.

/** User DAO Class */
@Repository
public class UserDAO {
    /** Calculate user function */
    public Long countByParameter(QueryUserParameterVO parameter) {...}
    /** Query user function */
    public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query user function */
    public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
        Long totalCount = userDAO.countByParameter(parameter);
        List<UserVO> userList = null;
        if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
            userList = userDAO.queryByParameter(parameter);
        }
        return new PageData<>(totalCount, userList);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Query user function (with the page index parameters of startIndex and pageSize) */
    @RequestMapping(path = "/queryUser", method = RequestMethod.POST)
    public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
        PageData<UserVO> pageData = userService.queryUser(parameter);
        return Result.success(pageData);
    }
}

Conclusion

Everyone has their own opinions on how to take advantage of Java, and of course this article only gives my personal opinion. But for me, I thought it was important to express my thoughts based on my experience with some startups I worked for before. Because, in my understanding, if these chaotic configurations are fixed, the whole system will be better.

Recommended Posts

About the confusion seen in startup Java servers
About the idea of anonymous classes in Java
A story about the JDK in the Java 11 era
Think about the JAVA = JAVAscript problem (needed in the future)
About abstract classes in java
Learn about the spec while shortening FizzBuzz written in Java
Think about the differences between functions and methods (in Java)
Guess the character code in Java
About the symbol <%%> in Rails erb
Specify the java location in eclipse.ini
Unzip the zip file in Java
About the current development environment (Java 8)
Parsing the COTOHA API in Java
About file copy processing in Java
Call the super method in Java
About returning a reference in a Java Getter
Get the result of POST in Java
Guess about the 2017 Java Persistence Framework (3) Reladomo
Java reference to understand in the figure
Try using the Stream API in Java
Call the Windows Notification API in Java
About the procedure for java to work
[Creating] A memorandum about coding in Java
About the new Java release model @ Seki Java (2018/07/20)
I tried the new era in Java
[Java] Use cryptography in the standard library
Organized memo in the head (Java --Array)
Try calling the CORBA service in Java 11+
About Records preview added in Java JDK 14
What is the main method in Java?
How to get the date in java
Continued Talk about writing Java in Emacs @ 2018
About the phenomenon that StackOverflowError occurs in processing using Java regular expressions
The story of writing Java in Emacs
Console input in Java (understanding the mechanism)
Impressions and doubts about using java for the first time in Android Studio
About the meaning of type variables, E, T, etc. used in generics used in Java
Regarding the transient modifier and serialization in Java
The story of low-level string comparison in Java
[Java] Handling of JavaBeans in the method chain
The story of making ordinary Othello in Java
About the description order of Java system properties
ChatWork4j for using the ChatWork API in Java
Organized memo in the head (Java --Control syntax)
Try scraping about 30 lines in Java (CSV output)
The intersection type introduced in Java 10 is amazing (?)
The story of learning Java in the first programming
Measure the size of a folder in Java
About var used in Java (Local Variable Type)
Feel the passage of time even in Java
Organized memo in the head (Java --instance edition)
[Java] Read the file in src / main / resources
Organized memo in the head (Java --Data type)
Display "Hello World" in the browser using Java
[Java] Judgment by entering characters in the terminal
Display "Hello World" in the browser using Java
Try using the COTOHA API parsing in Java
[Java] Something is displayed as "-0.0" in the output
Import files of the same hierarchy in Java
About Java interface
[Java] About Java 12 features