欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我微信「java_front」一起交流学习文章来源地址https://uudwc.com/A/R1vy
1 整体思想
计算机领域有一句话:计算机中任何问题都可通过增加一个虚拟层解决。这句体现了分层思想重要性,分层思想同样适用于Java工程架构。
分层优点是每层只专注本层工作,可以类比设计模式单一职责原则,或者经济学比较优势原理,每层只做本层最擅长的事情。
分层缺点是层之间通信时,需要通过适配器,翻译成本层或者下层可以理解的信息,通信成本有所增加。
我认为工程分层需要从六个维度思考:
(1) 单一
每层只处理一类事情,满足单一职责原则
(2) 降噪
信息在每一层进行传输,满足最小知识原则,只向下层传输必要信息
(3) 适配
每层都需要一个适配器,翻译信息为本层或者下层可以理解的信息
(4) 纵向
纵向做隔离,同一个领域内业务要在本领域内聚
(5) 横向
横向做编排,应用层聚合多个领域进行业务编排
(6) 数据
数据对象尽量纯净,尽量使用基本类型
1.2 九层结构
SpringBoot工程可以分成九层:
- 工具层:util
- 整合层:integration
- 基础层:infrastructure
- 领域层:domain
- 应用层:application
- 门面层:facade
- 客户端:client
- 控制层:controller
- 启动层:boot
1.3 微服务与九层结构
微服务和九层架构表述的是不同维度概念。微服务重点描述系统与系统之间交互关系,九层架构重点描述一个工程不同模块之间交互关系,这一点不要混淆。
微服务架构设计中通常分为前台、中台、后台:
第一点上图所有应用均可采用九层结构。
第二点中台应用承载核心逻辑,暴露核心接口,中台并不要理解所有端数据结构,而是通过client接口暴露相对稳定的数据。
第三点针对面向B端、面向C端、面向运营三种端,各自拆分出一个应用,在此应用中进行转换、适配和裁剪,并且处理各自业务。
第四点什么是大中台、小前台思想?中台提供稳定服务,前台提供灵活入口。
第五点如果后续要做秒杀系统,那么也可以理解其为一个前台应用(seckill-front)聚合各种中台接口。
2 分层详解
第一步创建项目:
user-demo-service
-user-demo-service-application
-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-util
2.1 util
工具层承载工具代码
不依赖本项目其它模块
只依赖一些通用工具包
user-demo-service-util
-/src/main/java
-date
-DateUtil.java
-json
-JsonUtil.java
-validate
-BizValidator.java
2.2 infrastructure
基础层承载数据访问和entity
同时承载基础服务(ES、Redis、MQ)
2.2.1 项目结构
user-demo-service-infrastructure
-/src/main/java
-base
-service
-redis
-RedisService.java
-mq
-ProducerService.java
-player
-entity
-PlayerEntity.java
-mapper
-PlayerEntityMapper.java
-game
-entity
-GameEntity.java
-mapper
-GameEntityMapper.java
-/src/main/resources
-mybatis
-sqlmappers
-gameEntityMapper.xml
-playerEntityMapper.xml
2.2.2 本项目依赖
- util
2.2.3 核心代码
创建运动员数据表:
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
运动员实体对象,gamePerformance字段作为string保存在数据库,体现了数据层尽量纯净,不要整合过多业务,解析任务应该放在业务层:
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;
}
运动员Mapper对象:
@Repository
public interface PlayerEntityMapper {
int insert(PlayerEntity record);
int updateById(PlayerEntity record);
PlayerEntity selectById(@Param("playerId") String playerId);
}
2.3 integration
本层调用外部服务,转换外部DTO成为本项目可以理解对象。
2.3.1 项目结构
本项目调用用户中心服务:
user-demo-service-integration
-/src/main/java
-user
-adapter
-UserClientAdapter.java
-proxy
-UserClientProxy.java
-vo // 本项目对象
-UserSimpleAddressVO.java
-UserSimpleContactVO.java
-UserSimpleBaseInfoVO.java
2.3.2 本项目依赖
- util
2.3.3 核心代码
(1) 外部服务
// 外部对象
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;
}
// 外部服务
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) 本项目对象
// 基本对象
public class UserBaseInfoVO {
private UserContactVO contactInfo;
private UserAddressVO addressInfo;
}
// 地址值对象
public class UserAddressVO {
private String cityCode;
private String addressDetail;
}
// 联系方式值对象
public class UserContactVO {
private String mobile;
}
(3) 适配器
public class UserClientAdapter {
public UserBaseInfoVO convert(UserInfoClientDTO userInfo) {
// 基础信息
UserBaseInfoVO userBaseInfo = new UserBaseInfoVO();
// 联系方式
UserContactVO contactVO = new UserContactVO();
contactVO.setMobile(userInfo.getMobile());
userBaseInfo.setContactInfo(contactVO);
// 地址信息
UserAddressVO addressVO = new UserAddressVO();
addressVO.setCityCode(userInfo.getCityCode());
addressVO.setAddressDetail(userInfo.getAddressDetail());
userBaseInfo.setAddressInfo(addressVO);
return userBaseInfo;
}
}
(4) 调用外部服务
public class UserClientProxy {
@Resource
private UserClientService userClientService;
@Resource
private UserClientAdapter userIntegrationAdapter;
// 查询用户
public UserBaseInfoVO getUserInfo(String userId) {
UserInfoClientDTO user = userClientService.getUserInfo(userId);
UserBaseInfoVO result = userIntegrationAdapter.convert(user);
return result;
}
}
2.4 domain
2.4.1 概念说明
通过三组对比理解领域层:
- 领域对象 VS 数据对象
- 领域对象 VS 业务对象
- 领域层 VS 应用层
(1) 领域对象 VS 数据对象
数据对象使用基本类型保持纯净:
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;
}
领域对象需要体现业务含义:
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) 领域对象 VS 业务对象
业务对象同样会体现业务,领域对象和业务对象有什么不同?最大不同是领域对象采用充血模型聚合业务。
运动员新增业务对象:
public class PlayerCreateBO {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
private MaintainCreateVO maintainInfo;
}
运动员新增领域对象:
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) 领域层 VS 应用层
第一个区别:领域层关注纵向,应用层关注横向。领域层纵向做隔离,本领域业务行为要在本领域内处理完。应用层横向做编排,聚合和编排领域服务。
第二个区别:应用层可以更加灵活组合不同领域业务,并且可以增加流控、监控、日志、权限,分布式锁,相较于领域层功能更为丰富。
2.4.2 项目结构
user-demo-service-domain
-/src/main/java
-base
-domain
-BaseDomain.java
-event
-BaseEvent.java
-vo
-BaseVO.java
-MaintainCreateVO.java
-MaintainUpdateVO.java
-player
-adapter
-PlayerDomainAdapter.java
-domain
-PlayerCreateDomain.java // 领域对象
-PlayerUpdateDomain.java
-PlayerQueryResultDomain.java
-event // 领域事件
-PlayerUpdateEvent.java
-PlayerMessageSender.java
-service // 领域服务
-PlayerDomainService.java
-vo // 值对象
-GamePerformanceVO.java
-game
-adapter
-GameDomainAdapter.java
-domain
-GameCreateDomain.java
-GameUpdateDomain.java
-GameQueryResultDomain.java
-service
-GameDomainService.java
2.4.3 本项目依赖
- util
- client
领域对象进行业务校验,所以需要依赖client模块:
- BizException
- ErrorCodeBizEnum
2.4.4 核心代码
// 修改领域对象
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();
}
}
// 比赛表现值对象
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);
}
}
}
// 修改人值对象
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);
}
}
}
// 领域服务
public class PlayerDomainService {
@Resource
private UserClientProxy userClientProxy;
@Resource
private PlayerRepository playerEntityMapper;
@Resource
private PlayerDomainAdapter playerDomainAdapter;
@Resource
private PlayerMessageSender playerMessageSender;
public boolean updatePlayer(PlayerUpdateDomain player) {
AssertUtil.notNull(player, new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT));
player.validate();
// 更新运动员信息
PlayerEntity entity = playerDomainAdapter.convertUpdate(player);
playerEntityMapper.updateById(entity);
// 发送更新消息
playerMessageSender.sendPlayerUpdatemessage(player);
// 查询用户信息
UserSimpleBaseInfoVO userInfo = userClientProxy.getUserInfo(player.getMaintainInfo().getUpdator());
log.info("updatePlayer maintainInfo={}", JacksonUtil.bean2Json(userInfo));
return true;
}
}
2.5 application
本层关注横向维度聚合领域服务,引出一种新对象称为聚合对象。因为本层需要聚合多个维度,所以需要通过聚合对象聚合多领域属性,例如提交订单需要聚合商品、物流、优惠券多个领域。
// 订单提交聚合对象
public class OrderSubmitAgg {
// userId
private String userId;
// skuId
private String skuId;
// 购买量
private Integer quantity;
// 地址信息
private String addressId;
// 可用优惠券
private String couponId;
}
// 订单应用服务
public class OrderApplicationService {
@Resource
private OrderDomainService orderDomainService;
@Resource
private CouponDomainService couponDomainService;
@Resource
private ProductDomainService productDomainService;
// 提交订单
public String submitOrder(OrderSubmitAgg orderSumbitAgg) {
// 订单编号
String orderId = generateOrderId();
// 商品校验
productDomainService.queryBySkuId(orderSumbitAgg.getSkuId());
// 扣减库存
productDomainService.subStock(orderSumbitAgg.getStockId(), orderSumbitAgg.getQuantity());
// 优惠券校验
couponDomainService.validate(userId, couponId);
// ......
// 创建订单
OrderCreateDomain domain = OrderApplicationAdapter.convert(orderSubmitAgg);
orderDomainService.createOrder(domain);
return orderId;
}
}
2.5.1 项目结构
user-demo-service-application
-/src/main/java
-player
-adapter
-PlayerApplicationAdapter.java
-agg
-PlayerCreateAgg.java
-PlayerUpdateAgg.java
-service
-PlayerApplicationService.java
-game
-listener
-PlayerUpdateListener.java // 监听运动员更新事件
2.5.2 本项目依赖
- util
- domain
- integration
- infrastructure
2.5.3 核心代码
本项目领域事件交互使用EventBus框架:
// 运动员应用服务
public class PlayerApplicationService {
@Resource
private LogDomainService logDomainService;
@Resource
private PlayerDomainService playerDomainService;
@Resource
private PlayerApplicationAdapter playerApplicationAdapter;
public boolean updatePlayer(PlayerUpdateAgg agg) {
// 运动员领域
boolean result = playerDomainService.updatePlayer(agg.getPlayer());
// 日志领域
LogReportDomain logDomain = playerApplicationAdapter.convert(agg.getPlayer().getPlayerName());
logDomainService.log(logDomain);
return result;
}
}
// 比赛领域监听运动员变更事件
public class PlayerUpdateListener {
@Resource
private GameDomainService gameDomainService;
@PostConstruct
public void init() {
EventBusManager.register(this);
}
@Subscribe
public void listen(PlayerUpdateEvent event) {
// 更新比赛计划
gameDomainService.updateGameSchedule();
}
}
2.6 facade + client
设计模式中有一种Facade模式,称为门面模式或者外观模式。这种模式提供一个简洁对外语义,屏蔽内部系统复杂性。
client承载数据对外传输对象DTO,facade承载对外服务,必须满足最小知识原则,无关信息不必对外透出。这样做有两个优点:
- 简洁性:对外服务语义明确简洁
- 安全性:敏感字段不能对外透出
2.6.1 项目结构
(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
-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 本项目依赖
client不依赖本项目其它模块,这一点非常重要:因为client会被外部引用,必须保证本层简洁和安全。
facade依赖本项目三个模块:
- domain
- client
- application
2.6.3 核心代码
(1) DTO
以查询运动员信息为例,查询结果DTO只封装强业务字段,运动员ID、创建时间、修改时间等业务不强字段无须透出:
public class PlayerQueryResultDTO implements Serializable {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceDTO gamePerformanceDTO;
}
(2) 客户端服务
public interface PlayerClientService {
public ResultDTO<PlayerQueryResultDTO> queryById(String playerId);
}
(3) 适配器
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) 服务实现
本层可以引用applicationService,也可以引用domainService,因为对于类似查询等简单业务场景,没有多领域聚合,可以直接使用领域服务。
public class PlayerClientServiceImpl implements PlayerClientService {
@Resource
private PlayerDomainService playerDomainService;
@Resource
private PlayerFacadeAdapter playerFacadeAdapter;
@Override
public ResultDTO<PlayerQueryResultDTO> queryById(String playerId) {
PlayerQueryResultDomain resultDomain = playerDomainService.queryPlayerById(playerId);
if (null == resultDomain) {
return ResultCommonDTO.success();
}
PlayerQueryResultDTO result = playerFacadeAdapter.convertQuery(resultDomain);
return ResultCommonDTO.success(result);
}
}
2.7 controller
facade服务实现可以作为RPC提供服务,controller则作为本项目HTTP接口提供服务,供前端调用。
controller需要注意HTTP相关特性,敏感信息例如登陆用户ID不能依赖前端传递,登陆后前端会在请求头带一个登陆用户信息,服务端需要从请求头中获取并解析。
2.7.1 项目结构
user-demo-service-controller
-/src/main/java
-controller
-player
-PlayerController.java
-game
-GameController.java
2.7.2 本项目依赖
- facade
2.7.3 核心代码
@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作为启动层承载启动入口
2.8.1 项目结构
所有模块代码均必须属于com.user.demo.service子路径:
user-demo-service-boot
-/src/main/java
-com.user.demo.service
-MainApplication.java
2.8.2 依赖本项目
- 所有模块
2.8.3 核心代码
@MapperScan("com.user.demo.service.infrastructure.*.mapper")
@SpringBootApplication
public class MainApplication {
public static void main(final String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
3 文章总结
3.1 六个维度
(1) 单一
每层只处理一类事情,util只承载工具对象,integration只处理外部服务,每层职责单一且清晰
(2) 降噪
如无必要勿增实体,例如查询结果DTO只透出最关键字段,例如运动员ID、创建时间、修改时间等业务不强字段无须透出
(3) 适配
service、facade、intergration层都存在适配器,翻译信息为本层或者下层可以理解的信息
(4) 纵向
domain service内聚本领域业务
(5) 横向
application service编排多个领域业务
(6) 数据
数据对象要纯净,尽量使用基本类型
3.2 微服务与九层结构
微服务和九层结构表述的是不同维度概念。微服务重点描述系统与系统之间交互关系,九层结构重点描述一个工程不同模块之间交互关系。文章来源:https://uudwc.com/A/R1vy
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我微信「java_front」一起交流学习