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.