赞
踩
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我微信「java_front」一起交流学习
计算机领域有一句话:计算机中任何问题都可通过增加一个虚拟层解决。这句体现了分层思想重要性,分层思想同样适用于Java工程架构。
分层优点是每层只专注本层工作,可以类比设计模式单一职责原则,或者经济学比较优势原理,每层只做本层最擅长的事情。
分层缺点是层之间通信时,需要通过适配器,翻译成本层或者下层可以理解的信息,通信成本有所增加。
我认为工程分层需要从六个维度思考:
每层只处理一类事情,满足单一职责原则
信息在每一层进行传输,满足最小知识原则,只向下层传输必要信息
每层都需要一个适配器,翻译信息为本层或者下层可以理解的信息
纵向做隔离,同一个领域内业务要在本领域内聚
横向做编排,应用层聚合多个领域进行业务编排
数据对象尽量纯净,尽量使用基本类型
SpringBoot工程可以分成九层:
微服务和九层架构表述的是不同维度概念。微服务重点描述系统与系统之间交互关系,九层架构重点描述一个工程不同模块之间交互关系,这一点不要混淆。
微服务架构设计中通常分为前台、中台、后台:
第一点上图所有应用均可采用九层结构。
第二点中台应用承载核心逻辑,暴露核心接口,中台并不要理解所有端数据结构,而是通过client接口暴露相对稳定的数据。
第三点针对面向B端、面向C端、面向运营三种端,各自拆分出一个应用,在此应用中进行转换、适配和裁剪,并且处理各自业务。
第四点什么是大中台、小前台思想?中台提供稳定服务,前台提供灵活入口。
第五点如果后续要做秒杀系统,那么也可以理解其为一个前台应用(seckill-front)聚合各种中台接口。
第一步创建项目:
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
工具层承载工具代码
不依赖本项目其它模块
只依赖一些通用工具包
user-demo-service-util
-/src/main/java
-date
-DateUtil.java
-json
-JsonUtil.java
-validate
-BizValidator.java
基础层承载数据访问和entity
同时承载基础服务(ES、Redis、MQ)
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
创建运动员数据表:
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);
}
本层调用外部服务,转换外部DTO成为本项目可以理解对象。
本项目调用用户中心服务:
user-demo-service-integration
-/src/main/java
-user
-adapter
-UserClientAdapter.java
-proxy
-UserClientProxy.java
-vo // 本项目对象
-UserSimpleAddressVO.java
-UserSimpleContactVO.java
-UserSimpleBaseInfoVO.java
// 外部对象 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; } }
// 基本对象 public class UserBaseInfoVO { private UserContactVO contactInfo; private UserAddressVO addressInfo; } // 地址值对象 public class UserAddressVO { private String cityCode; private String addressDetail; } // 联系方式值对象 public class UserContactVO { private String mobile; }
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; } }
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;
}
}
通过三组对比理解领域层:
数据对象使用基本类型保持纯净:
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; }
业务对象同样会体现业务,领域对象和业务对象有什么不同?最大不同是领域对象采用充血模型聚合业务。
运动员新增业务对象:
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(); } }
第一个区别:领域层关注纵向,应用层关注横向。领域层纵向做隔离,本领域业务行为要在本领域内处理完。应用层横向做编排,聚合和编排领域服务。
第二个区别:应用层可以更加灵活组合不同领域业务,并且可以增加流控、监控、日志、权限,分布式锁,相较于领域层功能更为丰富。
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
领域对象进行业务校验,所以需要依赖client模块:
// 修改领域对象 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; } }
本层关注横向维度聚合领域服务,引出一种新对象称为聚合对象。因为本层需要聚合多个维度,所以需要通过聚合对象聚合多领域属性,例如提交订单需要聚合商品、物流、优惠券多个领域。
// 订单提交聚合对象 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; } }
user-demo-service-application
-/src/main/java
-player
-adapter
-PlayerApplicationAdapter.java
-agg
-PlayerCreateAgg.java
-PlayerUpdateAgg.java
-service
-PlayerApplicationService.java
-game
-listener
-PlayerUpdateListener.java // 监听运动员更新事件
本项目领域事件交互使用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(); } }
设计模式中有一种Facade模式,称为门面模式或者外观模式。这种模式提供一个简洁对外语义,屏蔽内部系统复杂性。
client承载数据对外传输对象DTO,facade承载对外服务,必须满足最小知识原则,无关信息不必对外透出。这样做有两个优点:
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
user-demo-service-facade
-/src/main/java
-player
-adapter
-PlayerFacadeAdapter.java
-impl
-PlayerClientServiceImpl.java
-game
-adapter
-GameFacadeAdapter.java
-impl
-GameClientServiceImpl.java
client不依赖本项目其它模块,这一点非常重要:因为client会被外部引用,必须保证本层简洁和安全。
facade依赖本项目三个模块:
以查询运动员信息为例,查询结果DTO只封装强业务字段,运动员ID、创建时间、修改时间等业务不强字段无须透出:
public class PlayerQueryResultDTO implements Serializable {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceDTO gamePerformanceDTO;
}
public interface PlayerClientService {
public ResultDTO<PlayerQueryResultDTO> queryById(String playerId);
}
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; } }
本层可以引用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); } }
facade服务实现可以作为RPC提供服务,controller则作为本项目HTTP接口提供服务,供前端调用。
controller需要注意HTTP相关特性,敏感信息例如登陆用户ID不能依赖前端传递,登陆后前端会在请求头带一个登陆用户信息,服务端需要从请求头中获取并解析。
user-demo-service-controller
-/src/main/java
-controller
-player
-PlayerController.java
-game
-GameController.java
@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; } }
boot作为启动层承载启动入口
所有模块代码均必须属于com.user.demo.service子路径:
user-demo-service-boot
-/src/main/java
-com.user.demo.service
-MainApplication.java
@MapperScan("com.user.demo.service.infrastructure.*.mapper")
@SpringBootApplication
public class MainApplication {
public static void main(final String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
每层只处理一类事情,util只承载工具对象,integration只处理外部服务,每层职责单一且清晰
如无必要勿增实体,例如查询结果DTO只透出最关键字段,例如运动员ID、创建时间、修改时间等业务不强字段无须透出
service、facade、intergration层都存在适配器,翻译信息为本层或者下层可以理解的信息
domain service内聚本领域业务
application service编排多个领域业务
数据对象要纯净,尽量使用基本类型
微服务和九层结构表述的是不同维度概念。微服务重点描述系统与系统之间交互关系,九层结构重点描述一个工程不同模块之间交互关系。
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我微信「java_front」一起交流学习
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。