欢迎大家重视大众号「JAVA前哨」检查更多精彩共享文章,首要包含源码分析、实际运用、架构思维、职场共享、产品考虑等等,一同欢迎大家加我微信「java_front」一同交流学习
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 微服务与九层结构
微服务和九层结构表述的是不同维度概念。微服务要点描绘体系与体系之间交互联系,九层结构要点描绘一个工程不同模块之间交互联系。
欢迎大家重视大众号「JAVA前哨」检查更多精彩共享文章,首要包含源码分析、实际运用、架构思维、职场共享、产品考虑等等,一同欢迎大家加我微信「java_front」一同交流学习