Practical research on the implementation of the five-dimensional and nine-level hierarchical architecture of the Spring Boot project—continuously updated

var code = “12433d02-b242-4fd2-937d-750761a365ea”

Note: This blog post refers to the ideas of some technical bloggers and is being continuously summarized and updated based on practical content and code.

Five layering dimensions: SpringBoot engineering layering practice

1 Hierarchical thinking

There is a saying in the computer field: Any problem in a computer can be solved by adding a virtual layer. This sentence reflects the importance of layered thinking, which is also applicable to Java engineering architecture.

The advantage of layering is that each layer only focuses on the work of its own layer. This can be compared to the single responsibility principle of design patterns or the principle of comparative advantage in economics. Each layer only does what it is best at.

The disadvantage of layering is that when communicating between layers, an adapter is needed to translate information that can be understood by this layer or the lower layer, and the communication cost increases.

I think engineering layering needs to be thought from five dimensions:

(1) Single

Each layer only handles one type of things, satisfying the single responsibility principle.

(2) Noise reduction

Information is transmitted at each layer to meet the minimum knowledge principle and only necessary information is transmitted to the lower layer.

(3) Adaptation

Each layer needs an adapter to translate the information into information that can be understood by this layer or the lower layer.

(4) Business

Business objects can integrate business logic, such as using the congestion model to integrate business

(5) Data

Keep data objects as pure as possible and try not to aggregate business

1.2 Nine-layer structure

To sum up, the SpringBoot project can be divided into nine layers:

  • Tool layer: util
  • Integration layer: integration
  • Base layer: infrastructure
  • Service layer: service
  • Domain layer: domain
  • Facade layer: facade
  • Control layer: controller
  • Client: client
  • Boot layer: boot

2 Detailed explanation of layering

Create the test project user-demo-service:

user-demo-service
-user-demo-service-boot
-user-demo-service-client
-user-demo-service-controller
-user-demo-service-domain
-user-demo-service-facade
-user-demo-service-infrastructure
-user-demo-service-integration
-user-demo-service-service
-user-demo-service-util

2.1 The util
tool layer carries tool code

Does not depend on other modules of this project

Only rely on some common toolkits

user-demo-service-util
-/src/main/java
-date
-DateUtil.java
-json
-JSONUtil.java
-validate
-BizValidator.java

2.2
The core of the infrastructure base layer is to carry data access, and entity objects are carried in this layer.

2.2.1 Project structure
The code layer is divided into two areas:

player: player
game: competition
Each field has two sub-packages:

entity
mapper
user-demo-service-infrastructure
-/src/main/java
-player
-entity
-PlayerEntity.java
-mapper
-PlayerEntityMapper.java
-game
-entity
-GameEntity.java
-mapper
-GameEntityMapper.java
-/src/main/resources
-mybatis
-sqlmappers
-gameEntityMappler.xml
-playerEntityMapper.xml

Copy code

2.2.2 Dependencies between this project

infrastructure only relies on tool modules

<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-util</artifactId>
</dependency>

2.2.3 Core code

Create an athlete data table:

CREATE TABLE `player` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `player_id` varchar(256) NOT NULL COMMENT '运动员编号',
    `player_name` varchar(256) NOT NULL COMMENT '运动员名称',
    `height` int(11) NOT NULL COMMENT '身高',
    `weight` int(11) NOT NULL COMMENT '体重',
    `game_performance` text COMMENT '最近一场比赛表现',
    `creator` varchar(256) NOT NULL COMMENT '创建人',
    `updator` varchar(256) NOT NULL COMMENT '修改人',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '修改时间',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

The athlete entity object and gamePerformance field are stored in the database as strings. This reflects that the data layer should be as pure as possible and should not integrate too many businesses. The parsing task should be placed in the business layer:

public class PlayerEntity {
    private Long id;
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private String creator;
    private String updator;
    private Date createTime;
    private Date updateTime;
    private String gamePerformance;
}

Athlete Mapper object:

@Repository
public interface PlayerEntityMapper {
    int insert(PlayerEntity record);
    int updateById(PlayerEntity record);
    PlayerEntity selectById(@Param("playerId") String playerId);
}

2.3 domain

2.3.1 Concept description

Domain layer is the concept behind the rise of DDD popularity

The domain layer can be understood through three sets of comparisons

Domain objects VS data objects
Domain objects VS business objects
Domain layer VS business layer

(1) Domain objects VS data objects

Keep data object fields as pure as possible and use basic types

public class PlayerEntity {
    private Long id;
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private String creator;
    private String updator;
    private Date createTime;
    private Date updateTime;
    private String gamePerformance;
}

Take the query result field object as an example

Domain objects need to reflect business meaning

public class PlayerQueryResultDomain {
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformance;
}

public class GamePerformanceVO {
    // 跑动距离
    private Double runDistance;
    // 传球成功率
    private Double passSuccess;
    // 进球数
    private Integer scoreNum;
}

(2) Domain objects VS business objects

Business objects also reflect business. What is the difference between domain objects and business objects? One of the biggest differences is that domain objects use the congestion model to aggregate business.

New business objects for athletes:

public class PlayerCreateBO {
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformance;
    private MaintainCreateVO maintainInfo;
}

New field objects for athletes:

public class PlayerCreateDomain implements BizValidator {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
private MaintainCreateVO maintainInfo;

    @Override
    public void validate() {
        if (StringUtils.isEmpty(playerName)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == height) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (height > 300) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == weight) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null != gamePerformance) {
            gamePerformance.validate();
        }
        if (null == maintainInfo) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        maintainInfo.validate();
    }
}

(3) Domain layer VS business layer

Both the domain layer and the business layer include businesses. The relationship between the two is not a substitute, but a complementary relationship. The business layer can more flexibly combine businesses in different fields, and can add controls such as flow control, monitoring, logs, permissions, and distributed locks. It has richer functions than the domain layer.

2.3.2 Project structure

The code layer is divided into two areas:

player:运动员
game:比赛

Each domain has three sub-packages:

domain:领域对象
event:领域事件
vo:值对象
user-demo-service-domain
-/src/main/java
-base
-domain
-BaseDomain.java
-event
-BaseEvent.java
-vo
-BaseVO.java
-MaintainCreateVO.java
-MaintainUpdateVO.java
-player
-domain
-PlayerCreateDomain.java
-PlayerUpdateDomain.java
-PlayerQueryResultDomain.java
-event
-PlayerUpdateEvent.java
-vo
-GamePerformanceVO.java
-game
-domain
-GameCreateDomain.java
-GameUpdateDomain.java
-GameQueryResultDomain.java
-event
-GameUpdateEvent.java
-vo
-GameSubstitutionVO.java

2.3.3 Dependencies between this project:
domain depends on two modules of this project:

util
client

The reason why we rely on the client module is that domain objects aggregate business verification, and the following information needs to be exposed to the outside:

BizException
ErrorCodeBizEnum
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-util</artifactId>
</dependency>
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-client</artifactId>
</dependency>

2.3.4 Core code

Take athletes modifying field objects as an example:

//Athletes modify the field object

public class PlayerUpdateDomain extends BaseDomain implements BizValidator {
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private String updator;
private Date updatetime;
private GamePerformanceVO gamePerformance;
private MaintainUpdateVO maintainInfo;

    @Override
    public void validate() {
        if (StringUtils.isEmpty(playerId)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (StringUtils.isEmpty(playerName)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == height) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (height > 300) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == weight) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null != gamePerformance) {
            gamePerformance.validate();
        }
        if (null == maintainInfo) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        maintainInfo.validate();
    }
}

// Game performance value object

public class GamePerformanceVO implements BizValidator {

    // 跑动距离
    private Double runDistance;
    // 传球成功率
    private Double passSuccess;
    // 进球数
    private Integer scoreNum;
 
    @Override
    public void validate() {
        if (null == runDistance) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == passSuccess) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (Double.compare(passSuccess, 100) > 0) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == runDistance) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == scoreNum) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
    }
}

//Modify person value object

public class MaintainUpdateVO implements BizValidator {

    // 修改人
    private String updator;
    // 修改时间
    private Date updateTime;
 
    @Override
    public void validate() {
        if (null == updator) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == updateTime) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
    }
}

2.4 service

2.4.1 Project structure

user-demo-service-service
-/src/main/java
-player
-adapter
-PlayerServiceAdapter.java
-event
-PlayerMessageSender.java
-service
-PlayerService.java
-game
-adapter
-GameServiceAdapter.java
-event
-GameMessageSender.java
-service
-GameService.java

2.4.2 Dependencies between this project
Service depends on four modules of this project:

util
domain
integration
infrastructure
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-domain</artifactId>
</dependency>
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-infrastructure</artifactId>
</dependency>
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-util</artifactId>
</dependency>
<dependency>
<groupId>com.test.javafront</groupId>
<artifactId>user-demo-service-integration</artifactId>
</dependency>

2.4.3 Core code

Take athlete editing services as an example:

//Athlete Services

public class PlayerService {

    @Resource
    private PlayerEntityMapper playerEntityMapper;
    @Resource
    private PlayerMessageSender playerMessageSender;
    @Resource
    private PlayerServiceAdapter playerServiceAdapter;
 
    public boolean updatePlayer(PlayerUpdateDomain player) {
        AssertUtil.notNull(player, new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT));
        player.validate();
        PlayerEntity entity = playerServiceAdapter.convertUpdate(player);
        playerEntityMapper.updateById(entity);
        playerMessageSender.sendPlayerUpdatemessage(player);
        return true;
    }
}

//Athlete message service

public class PlayerMessageSender {

    @Resource
    private PlayerServiceAdapter playerServiceAdapter;
 
    public boolean sendPlayerUpdatemessage(PlayerUpdateDomain domain) {
        PlayerUpdateEvent event = playerServiceAdapter.convertUpdateEvent(domain);
        log.info("sendPlayerUpdatemessage event={}", event);
        return true;
    }
}

// service adapter

public class PlayerServiceAdapter {

    // domain -> entity
    public PlayerEntity convertUpdate(PlayerUpdateDomain domain) {
        PlayerEntity player = new PlayerEntity();
        player.setPlayerId(domain.getPlayerId());
        player.setPlayerName(domain.getPlayerName());
        player.setWeight(domain.getWeight());
        player.setHeight(domain.getHeight());
        if (null != domain.getGamePerformance()) {
            player.setGamePerformance(JacksonUtil.bean2Json(domain.getGamePerformance()));
        }
        String updator = domain.getMaintainInfo().getUpdator();
        Date updateTime = domain.getMaintainInfo().getUpdateTime();
        player.setUpdator(updator);
        player.setUpdateTime(updateTime);
        return player;
    }
 
    // domain -> event
    public PlayerUpdateEvent convertUpdateEvent(PlayerUpdateDomain domain) {
        PlayerUpdateEvent event = new PlayerUpdateEvent();
        event.setPlayerUpdateDomain(domain);
        event.setMessageId(UUID.randomUUID().toString());
        event.setMessageId(PlayerMessageType.UPDATE.getMsg());
        return event;
    }
}

2.5 integration
This project may rely on external services, so converting external DTOs into objects that this project can understand needs to be processed at this layer.

2.5.1 Project structure
Assume that this project calls the user center service:

user-demo-service-intergration
-/src/main/java
-user
-adapter
-UserClientAdapter.java
-proxy
-UserClientProxy.java

Copy code
2.5.2 The dependency
intergration between this project depends on two modules of this project:

util
domain

The reason why it relies on the domain module is that this layer needs to convert external DTOs into objects that this project can understand, and these objects are placed in the domain module.

<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-domain</artifactId>
</dependency>
<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-util</artifactId>
</dependency>

2.5.3 Core code
Now we will use the external object UserClientDTO

Convert to the project domain object UserInfoDomain

(1) External services

// external object

public class UserInfoClientDTO implements Serializable {
private String id;
private String name;
private Date createTime;
private Date updateTime;
private String mobile;
private String cityCode;
private String addressDetail;
}

// external service

public class UserClientService {

    // RPC模拟
    public UserInfoClientDTO getUserInfo(String userId) {
        UserInfoClientDTO userInfo = new UserInfoClientDTO();
        userInfo.setId(userId);
        userInfo.setName(userId);
        userInfo.setCreateTime(DateUtil.now());
        userInfo.setUpdateTime(DateUtil.now());
        userInfo.setMobile("test-mobile");
        userInfo.setCityCode("test-city-code");
        userInfo.setAddressDetail("test-address-detail");
        return userInfo;
    }
}

(2) Objects in this project field

The domain module adds the user field:

user-demo-service-domain
-/src/main/java
-user
-domain
-UserDomain.java
-vo
-UserAddressVO.java
-UserContactVO.java

User field object code:

// user domain

public class UserInfoDomain extends BaseDomain {
    private UserContactVO contactInfo;
    private UserAddressVO addressInfo;
}

//Address value object

public class UserAddressVO extends BaseVO {
    private String cityCode;
    private String addressDetail;
}

//Contact value object

public class UserContactVO extends BaseVO {
private String mobile;
}

(3) Adapter

public class UserClientAdapter {

    // third dto -> domain
    public UserInfoDomain convertUserDomain(UserInfoClientDTO userInfo) {
        UserInfoDomain userDomain = new UserInfoDomain();
        UserContactVO contactVO = new UserContactVO();
        contactVO.setMobile(userInfo.getMobile());
        userDomain.setContactInfo(contactVO);
 
        UserAddressVO addressVO = new UserAddressVO();
        addressVO.setCityCode(userInfo.getCityCode());
        addressVO.setAddressDetail(userInfo.getAddressDetail());
        userDomain.setAddressInfo(addressVO);
        return userDomain;
    }
}

(4) Call external services

public class UserClientProxy {

    @Resource
    private UserClientService userClientService;
    @Resource
    private UserClientAdapter userClientAdapter;
 
    public UserInfoDomain getUserInfo(String userId) {
        UserInfoClientDTO user = userClientService.getUserInfo(userId);
        UserInfoDomain result = userClientAdapter.convertUserDomain(user);
        return result;
    }
}

2.6 facade + client

There is a Facade mode in the design pattern, which is called facade mode or appearance mode. This pattern provides a concise external semantics and shields internal system complexity.

The client carries the external data transmission object DTO, and the facade carries the external service. These two layers must meet the minimum knowledge principle, and irrelevant information does not need to be disclosed to the outside world.

This has two advantages:

Simplicity: The external service semantics are clear and concise.
Security: Sensitive fields cannot be exposed to the outside world.

2.6.1 Project structure

(1) client

user-demo-service-client
-/src/main/java
-base
-dto
-BaseDTO.java
-error
-BizException.java
-BizErrorCode.java
-event
-BaseEventDTO.java
-result
-ResultDTO.java
-player
-dto
-PlayerCreateDTO.java
-PlayerQueryResultDTO.java
-PlayerUpdateDTO.java
-enums
-PlayerMessageTypeEnum.java
-event
-PlayerUpdateEventDTO.java
-service
-PlayerClientService.java

(2) facade

user-demo-service-facade
-/src/main/java
-player
-adapter
-PlayerFacadeAdapter.java
-impl
-PlayerClientServiceImpl.java
-game
-adapter
-GameFacadeAdapter.java
-impl
-GameClientServiceImpl.java

2.6.2 Dependencies between this project
The client does not depend on other modules of this project. This is very important because the client will be referenced externally. This layer must be kept simple and safe.

The facade depends on three modules of this project:

domain
client
service
<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-domain</artifactId>
</dependency>
    <dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-client</artifactId>
</dependency>
<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-service</artifactId>
</dependency>

2.6.3 Core code

(1) DTO

Taking athlete information query as an example, the query result DTO only encapsulates the most critical fields, such as athlete ID, creation time, modification time and other fields that are not business-intensive and do not need to be disclosed:

public class PlayerQueryResultDTO implements Serializable {
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceDTO gamePerformanceDTO;
}

(2) Client service

public interface PlayerClientService {
    public ResultDTO<PlayerQueryResultDTO> queryById(String playerId);
}

(3) Adapter

public class PlayerFacadeAdapter {

    // domain -> dto
    public PlayerQueryResultDTO convertQuery(PlayerQueryResultDomain domain) {
        if (null == domain) {
            return null;
        }
        PlayerQueryResultDTO result = new PlayerQueryResultDTO();
        result.setPlayerId(domain.getPlayerId());
        result.setPlayerName(domain.getPlayerName());
        result.setHeight(domain.getHeight());
        result.setWeight(domain.getWeight());
        if (null != domain.getGamePerformance()) {
            GamePerformanceDTO performance = convertGamePerformance(domain.getGamePerformance());
            result.setGamePerformanceDTO(performance);
        }
        return result;
    }
}        

(4) Service implementation

public class PlayerClientServiceImpl implements PlayerClientService {

    @Resource
    private PlayerService playerService;
    @Resource
    private PlayerFacadeAdapter playerFacadeAdapter;
 
    @Override
    public ResultDTO<PlayerQueryResultDTO> queryById(String playerId) {
        PlayerQueryResultDomain resultDomain = playerService.queryPlayerById(playerId);
        if (null == resultDomain) {
            return ResultCommonDTO.success();
        }
        PlayerQueryResultDTO result = playerFacadeAdapter.convertQuery(resultDomain);
        return ResultCommonDTO.success(result);
    }
}

2.7 controller

The facade service implementation can provide services as RPC, and the controller provides services as the HTTP interface of this project for front-end calls.

The controller needs to pay attention to HTTP-related characteristics. Sensitive information such as login user ID cannot be passed by the front end. After login, the front end will carry a login user information in the request header, and the server needs to obtain and parse it from the request header.

2.7.1 Project structure

user-demo-service-controller
-/src/main/java
-config
-CharsetConfig.java
-controller
-player
-PlayerController.java
-game
-GameController.java

2.7.2 Dependencies of this project

The controller depends on a module of this project:

facade

According to the principle of dependency transfer, the following modules are also dependent on:

domain
client
service
<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-facade</artifactId>
</dependency>

2.7.3 Core code

@RestController
@RequestMapping("/player")
public class PlayerController {

    @Resource
    private PlayerClientService playerClientService;
 
    @PostMapping("/add")
    public ResultDTO<Boolean> add(@RequestHeader("test-login-info") String loginUserId, @RequestBody PlayerCreateDTO dto) {
        dto.setCreator(loginUserId);
        ResultCommonDTO<Boolean> resultDTO = playerClientService.addPlayer(dto);
        return resultDTO;
    }
 
    @PostMapping("/update")
    public ResultDTO<Boolean> update(@RequestHeader("test-login-info") String loginUserId, @RequestBody PlayerUpdateDTO dto) {
        dto.setUpdator(loginUserId);
        ResultCommonDTO<Boolean> resultDTO = playerClientService.updatePlayer(dto);
        return resultDTO;
    }
 
    @GetMapping("/{playerId}/query")
    public ResultDTO<PlayerQueryResultDTO> queryById(@RequestHeader("test-login-info") String loginUserId, @PathVariable("playerId") String playerId) {
        ResultCommonDTO<PlayerQueryResultDTO> resultDTO = playerClientService.queryById(playerId);
        return resultDTO;
    }
}

2.8 boot

boot is the startup layer and only has the startup entry code.

2.8.1 Project structure

All module code must belong to the com.user.demo.service subpath

user-demo-service-boot
-/src/main/java
-com.user.demo.service
-MainApplication.java

2.8.2 This project relies on
boot to reference all modules of this project

util
integration
infrastructure
service
domain
facade
controller
client

2.8.3 Core code

@MapperScan("com.user.demo.service.infrastructure.*.mapper")
@SpringBootApplication
public class MainApplication {
    public static void main(final String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

3 Article summary

Let’s review the five hierarchical thinking dimensions again:

(1) Single

Each layer only handles one type of thing. For example, util only carries tool objects, and integration only handles external services. The responsibilities of each layer are single and clear.

(2) Noise reduction

If there is no need to add entities, for example, the query result DTO only reveals the most critical fields, such as athlete ID, creation time, modification time and other fields that are not business-intensive and do not need to be disclosed.

(3) Adaptation

There are adapters in the service, facade, and integration layers, and the translation information is information that can be understood by this layer or the lower layer.

(4) Business

Business objects can aggregate services through the congestion model, such as aggregating business verification logic in business objects.

(5) Data

The data object should be pure, for example, the game performance should be saved in the string type, and the data layer does not need to be parsed.

Guess you like

Origin blog.csdn.net/lqzixi/article/details/132114466
Recommended