六个思考维度:DDD + SpringBoot工程九层结构图解与实战

欢迎大家重视大众号「JAVA前哨」检查更多精彩共享文章,首要包含源码分析、实际运用、架构思维、职场共享、产品考虑等等,一同欢迎大家加我微信「java_front」一同交流学习

1 全体思维

计算机范畴有一句话:计算机中任何问题都可经过添加一个虚拟层解决。这句表现了分层思维重要性,分层思维相同适用于Java工程架构。

分层长处是每层只专注本层作业,能够类比规划形式单一责任准则,或许经济学比较优势原理,每层只做本层最擅长的工作。

分层缺陷是层之间通讯时,需求经过适配器,翻译本钱层或许下层能够了解的信息,通讯本钱有所添加。

我认为工程分层需求从六个维度考虑:

(1) 单一

每层只处理一类工作,满意单一责任准则

(2) 降噪

信息在每一层进行传输,满意最小常识准则,只向下层传输必要信息

(3) 适配

每层都需求一个适配器,翻译信息为本层或许下层能够了解的信息

(4) 纵向

纵向做阻隔,同一个范畴内事务要在本范畴内聚

(5) 横向

横向做编列,运用层聚合多个范畴进行事务编列

(6) 数据

数据目标尽量纯洁,尽量运用根本类型

1.2 九层结构

SpringBoot工程能够分成九层:

  • 东西层:util
  • 整合层:integration
  • 根底层:infrastructure
  • 范畴层:domain
  • 运用层:application
  • 门面层:facade
  • 客户端:client
  • 控制层:controller
  • 发动层:boot

SpringBoot九层结构_DDD.jpg

1.3 微服务与九层结构

微服务和九层架构表述的是不同维度概念。微服务要点描绘体系与体系之间交互联系,九层架构要点描绘一个工程不同模块之间交互联系,这一点不要混淆。

微服务架构规划中通常分为前台、中台、后台:

变化无常_2.jpg

第一点上图一切运用均可选用九层结构。

第二点中台运用承载中心逻辑,露出中心接口,中台并不要了解一切端数据结构,而是经过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」一同交流学习