欢迎我们重视大众号「JAVA前线」查看更多精彩共享文章,首要包括源码剖析、实践应用、架构思维、职场共享、产品思考等等,一起欢迎我们加我微信「java_front」一起交流学习


1 分层思维

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

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

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

我认为工程分层需求从五个维度思考:

(1) 单一

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

(2) 降噪

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

(3) 适配

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

(4) 事务

事务目标能够整合事务逻辑,例如运用充血模型整合事务

(5) 数据

数据目标尽量纯洁,尽量不要聚合事务

1.2 九层结构

综上所述SpringBoot工程能够分为九层:

  • 东西层:util
  • 整合层:integration
  • 根底层:infrastructure
  • 服务层:service
  • 范畴层:domain
  • 门面层:facade
  • 操控层:controller
  • 客户端:client
  • 发动层:boot

2 分层详解

创立测验项目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 util

东西层承载东西代码

不依靠本项目其它模块

只依靠一些通用东西包

user-demo-service-util
    -/src/main/java
        -date
            -DateUtil.java
        -json
            -JSONUtil.java
        -validate
            -BizValidator.java

2.2 infrastructure

根底层中心是承载数据访问,entity实体目标承载在本层。

2.2.1 项目结构

代码层分为两个范畴:

  • player:运动员
  • game:竞赛

每个范畴具有两个子包:

  • 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

2.2.2 本项目间依靠联系

infrastructure只依靠东西模块

<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-util</artifactId>
</dependency>

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 domain

2.3.1 概念阐明

范畴层是DDD流行鼓起之概念

能够经过三组对比了解范畴层

  • 范畴目标 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.3.2 项目结构

代码层分为两个范畴:

  • player:运动员
  • game:竞赛

每个范畴具有三个子包:

  • 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 本项目间依靠联系

domain依靠本项目两个模块:

  • util
  • client

之所以依靠client模块是由于范畴目标聚合了事务校验,以下信息需求露出至外部:

  • 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 中心代码

以运动员修正范畴目标为例:

// 运动员修正范畴目标
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);
        }
    }
}

2.4 service

2.4.1 项目结构

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 本项目间依靠联系

service依靠本项目四个模块:

  • 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 中心代码

以运动员修正服务为例:

// 运动员服务
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;
    }
}
// 运动员音讯服务
public class PlayerMessageSender {
    @Resource
    private PlayerServiceAdapter playerServiceAdapter;
    public boolean sendPlayerUpdatemessage(PlayerUpdateDomain domain) {
        PlayerUpdateEvent event = playerServiceAdapter.convertUpdateEvent(domain);
        log.info("sendPlayerUpdatemessage event={}", event);
        return true;
    }
}
// 服务适配器
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 intergration

本项目可能会依靠外部服务,那么将外部DTO转换为本项目能够了解的目标,需求在本层处理。

2.5.1 项目结构

假定本项目调用了用户中心服务:

user-demo-service-intergration
    -/src/main/java
        -user
            -adapter
                -UserClientAdapter.java
            -proxy
                -UserClientProxy.java

2.5.2 本项目间依靠联系

intergration依靠本项目两个模块:

  • util
  • domain

之所以依靠domain模块,是由于本层需求将外部DTO转换为本项目能够了解的目标,这些目标就放在domain模块。

<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 中心代码

现在咱们将外部目标UserClientDTO

转换为本项目范畴目标UserInfoDomain

(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) 本项目范畴目标

domain模块新增user范畴:

user-demo-service-domain
    -/src/main/java
        -user
            -domain
                -UserDomain.java
            -vo
                -UserAddressVO.java
                -UserContactVO.java

user范畴目标代码:

// 用户范畴
public class UserInfoDomain extends BaseDomain {
    private UserContactVO contactInfo;
    private UserAddressVO addressInfo;
}
// 地址值目标
public class UserAddressVO extends BaseVO {
    private String cityCode;
    private String addressDetail;
}
// 联系方式值目标
public class UserContactVO extends BaseVO {
    private String mobile;
}

(3) 适配器

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) 调用外部服务

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

规划形式中有一种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
            -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 本项目间依靠联系

client不依靠本项目其它模块,这一点非常重要,由于client会被外部引证,有必要确保这一层简练和安全。

facade依靠本项目三个模块:

  • 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 中心代码

(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) 服务完成

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

facade服务完成能够作为RPC供给服务,controller则作为本项目HTTP接口供给服务,供前端调用。

controller需求注意HTTP相关特性,敏感信息例如登陆用户ID不能依靠前端传递,登陆后前端会在恳求头带一个登陆用户信息,服务端需求从恳求头中获取并解析。

2.7.1 项目结构

user-demo-service-controller
    -/src/main/java
        -config
            -CharsetConfig.java
        -controller
            -player
                -PlayerController.java
            -game
                -GameController.java

2.7.2 本项目依靠联系

controller依靠本项目一个模块:

  • facade

根据依靠传递原理一起依靠以下模块:

  • domain
  • client
  • service
<dependency>
    <groupId>com.test.javafront</groupId>
    <artifactId>user-demo-service-facade</artifactId>
</dependency>

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 本项目间依靠

boot引证本项目一切模块

  • util
  • integration
  • infrastructure
  • service
  • domain
  • facade
  • controller
  • client

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 文章总结

咱们再次回顾分层五个思考维度:

(1) 单一

每层只处理一类工作,例如util只承载东西目标,integration只处理外部服务,每层责任单一且明晰

(2) 降噪

如无必要无增实体,例如查询成果DTO只透出最要害字段,例如运动员ID、创立时刻、修正时刻等事务不强字段无须透出

(3) 适配

service、facade、intergration层都存在适配器,翻译信息为本层或许基层能够了解的信息

(4) 事务

事务目标能够经过充血模型聚合事务,例如在事务目标中聚合事务校验逻辑

(5) 数据

数据目标要纯洁,例如经过string类型保存竞赛体现,数据层无需解析

欢迎我们重视大众号「JAVA前线」查看更多精彩共享文章,首要包括源码剖析、实践应用、架构思维、职场共享、产品思考等等,一起欢迎我们加我微信「java_front」一起交流学习