• 从零到一搭建基础架构(3)-base模块搭建上篇


    1. 前后端交互结构混乱,response中业务code定义没有一个统一的规范
    2. PO、DTO、BO、VO傻傻分不清楚
    3. 工具类泛滥,同一工程中StringUtil的引用有外部引入,有内部jar包引入还有自己定义的
    4. 异常定义混乱,导致在Spring统一response拦截的地方区分业务异常与code错误困难
    5. 通用性高的枚举重复定义,比如是否枚举男女枚举
    6. 通用的常量散落在业务系统中,导致各个业务系统中重复的逻辑定义
    7. ...

    我们需要将上述这些与业务本身无关,但又是辅助业务开发的工具性质的定义放在一个统一的base包内。

    让业务系统聚焦于业务本身。

    本文所有代码均在此link,可提前clone下来,如果遇到报错,请按照如下步骤操作

    你需要先clone common-dependency

    然后执行mvn clean install 将 common-dependency包打到你本地仓库

    否则你拉下来common-frame工程后会报找不到

    1. <parent>
    2. <groupId>com.baiyan</groupId>
    3. <artifactId>common-dependency</artifactId>
    4. <version>1.0.0-SNAPSHOT</version>
    5. </parent>
    6. 复制代码

    一、Base包中Maven引入的规范

    遵守第一篇与第二篇Maven依赖引入的规范的前提下,我们在base包中可以引入什么样内部、外部的jar包呢?

    1. 业务无关性
    2. 工具类型
    3. 无需配置性

    可以引入的示例

    1. <dependency>
    2. <groupId>com.baomidou</groupId>
    3. <artifactId>mybatis-plus-extension</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>com.alibaba</groupId>
    7. <artifactId>transmittable-thread-local</artifactId>
    8. </dependency>
    9. <dependency>
    10. <groupId>com.google.code.gson</groupId>
    11. <artifactId>gson</artifactId>
    12. </dependency>
    13. <dependency>
    14. <groupId>cn.hutool</groupId>
    15. <artifactId>hutool-all</artifactId>
    16. </dependency>
    17. 复制代码

    不建议引入的示例

    1. <dependency>
    2. <groupId>com.baomidou</groupId>
    3. <artifactId>mybatis-plus-boot-starter</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>com.alibaba</groupId>
    7. <artifactId>druid-spring-boot-starter</artifactId>
    8. </dependency>
    9. <dependency>
    10. <groupId>mysql</groupId>
    11. <artifactId>mysql-connector-java</artifactId>
    12. <scope>runtime</scope>
    13. </dependency>
    14. 复制代码

    二、定义统一的response body与biz code

    前后端分离开发的时代,如何做好前后端的数据交互呢?

    我在有的工程中看过这样的后端接口:直接将前端所需要的数据返回,不做任何包装。

    这么做会有什么问题呢?

    我们只能依据http code来响应后端的请求结果。无法跟前端约定业务code,来让前端在UI上做特定的展示。

    而http code本身是请求级别的code定义,只是一个泛的定义。

    比如登陆失败有很多种原因:账号不存在,密码错误,账号锁定等等。我们如果只用http code 401告诉前端登陆失败这显然用户交互非常不友好。

    再比如,分布式架构体系下,一个请求上涉及很多服务,我们应该有一个统一的链路id将所有请求的日志串联起来,方便后续的日志定位。

    综上我们应该定义规范的结构跟前端交互,让大家按照这个既定规范进行开发

    普通接口响应结构

    1. {
    2. "code": 200, // 业务code定义,区别于http code标识细分的业务请求结果,比如10000标识账号错误,10001标识密码错误
    3. "errorCode": null, // 发生错误时对于message的code,英文串描述,用于国际化异常映射
    4. "message": "请求成功", // code200时返回请求成功,10000时返回账号错误,10001返回密码错误。发生异常时,依据errorCode的定义映射国际化请求响应
    5. "traceId": "", // 链路id,串联请求所关联所有应用的日志数据
    6. "data": null // 是的返回的业务数据
    7. }
    8. 复制代码

    列表分页数据响应请求

    1. {
    2. ..., // 与普通的一致
    3. "total": 100, // 查询条件下数据的总数
    4. "data": [] // 是的返回的业务数据,list结构
    5. }
    6. 复制代码

    code实现

    三、如何正确划分数据载体

    实体类作为数据的载体,大家日常工作中绝对会接触到,但是你真的正确使用了吗?

    说一下我之前项目中看到的代码。数据查询得到的数据载体,service层交互的数据载体,rpc层交互的数据载体,web层交互的数据载体都集中在一个实体中。这个做法在业务场景特别简单的时候不会出现什么大问题,但是如果是一个比较庞大的业务应用体系。这样就会就会有问题了。

    举个例子

    用户表中一个四个字段,用户id,用户名,手机号,用户密码。现在需要在用户管理菜单页展示用户数据。如果只有一个实体的情况下,我从数据库里查询出来的数据拥有4个字段,把密码传递到前端肯定是不合适的。做一下脱敏,将password置为空。但是你在前端的报文中还是能看到

    1. {
    2. "userId":1
    3. "userName":"admin"
    4. "mobile":"13888888888"
    5. "password":null
    6. }
    7. 复制代码

    显然这个是不合理的,返回给前端的数据应该是

    1. {
    2. "userId":1
    3. "userName":"admin"
    4. "mobile":"13888888888"
    5. }
    6. 复制代码

    显然对于java中的数据载体来说,每一层的分层是尤为重要的。我通常在会对数据载体做如下分层

    实体类型描述
    PO持久化对象,实体属性与表字段一一对应,DAO层产生,在Service层被使用
    BO业务对象,聚合PO层数据,也可以多表关联数据查询聚合,内部会有属性的业务逻辑处理方法。DAO/Service层产生,Service层使用
    DTO数据传输对象,常用语service层,rpc层,controller层,用于数组传输的载体,内部无逻辑
    VO数据展示层,用于controller层,这里我习惯与方法的出参,用于切合DTO与VO层的结构差异
    Query查询参数,controller层方法入参,接收前端的查询类型参数
    Command指令性型参数,例如用户新增,用户修改的数据载体

    说明:

    1. DTO与VO我常常会混用,如果数据传输载体只会在controller展示层中被组装使用,那直接返回给前端也可以,如果与前端要求不一致的情况,需要编写对应的Converter类进行处理,不可以将转换逻辑编写在DTO与VO中,他们只是数据载体。
    2. Command与DTO/VO,网上一些博主会将VO或者DTO作为web层入参进行数据的增删改。从结构化与定义上没有问题,但是这个跟数据载体带有指令就有点关联不上了。我对DTO与VO的理解是他们是结果型数据,是业务逻辑处理后的产物。而Command是指令性数据,通过Command类型参数,经由BO层业务逻辑,将数据映射到PO层与数据库交互。
    3. Query参数,与Command参数类似,常常有人会使用DTO或者VO来传递数据,一样的道理,业务语义不够强。

    依据上面的规范我们能够划分好业务系统内部的实体,对于这些业务实体他们又有那些公用的逻辑呢?

    1.DDD结构划分

    如果我们的项目是DDD结构的分层,POJO需要有一个显示的标识符表明当前的POJO是什么左右,比如聚合根我会定义一个实体实现这个接口AggregateRoot来表明当前实体是聚合根

    1. /**
    2. * 聚合根标记
    3. *
    4. * @author baiyan
    5. */
    6. public interface AggregateRoot extends MarkerInterface {
    7. }
    8. /**
    9. * 用户聚合根
    10. *
    11. * @author baiyan
    12. */
    13. public class User implements AggregateRoot {
    14. }
    15. 复制代码

    code演示

    2.统一的分页查询参数

    分页查询参数规范基本上就是两种:

    1. limit/offset
    2. pageSize/PageNo

    为了兼容以上两种情况,我们设计一个顶级的父类,将上面两种参数都一一关联起来。

    1. @Data
    2. @EqualsAndHashCode(callSuper = true)
    3. public class KeywordQuery extends PageQuery {
    4. @ApiModelProperty("关键字查询")
    5. private String keyword;
    6. }
    7. 复制代码

    code演示

    后续我们如果有分页需求的时候,只需要继承这个顶级的查询父类,只需要在查询条件内定义业务参数即可。

    3.顶级的PO类设计

    PO是持久化实体,与表结构的字段一一对应。我们在设计表结构数据时,抛开业务不管,应该是要有一些公共的字段的:id、创建时间、修改时间、删除标识(如果数据删除是使用软删除的方式)

    1. @Data
    2. public class BaseUuidEntity {
    3.   /**
    4.     * 主键id 采用默认雪花算法
    5.     */
    6.   @TableId
    7.   private Long id;
    8.   /**
    9.     * 创建时间
    10.     */
    11.   private LocalDateTime gmtCreate;
    12.   /**
    13.     * 修改时间
    14.     */
    15.   private LocalDateTime gmtModified;
    16.   /**
    17.     * 是否删除,0位未删除
    18.     */
    19.   @TableLogic(delval = "current_timestamp()")
    20.   private Long deleted;
    21. }
    22. 复制代码

    code演示

    4.其他通用类型的mode抽取

    再比如,我们经常会返回给前端一些key/value结构的数据,这种结构是具备通用性,我们可以将这种具备高通用的DTO也放在base模块中供业务使用。

    1. @Data
    2. @NoArgsConstructor
    3. @AllArgsConstructor
    4. public class DataDictDTO {
    5.   @ApiModelProperty("key值")
    6.   private String key;
    7.   @ApiModelProperty("value值")
    8.   private String value;
    9. }
    10. 复制代码

    code演示

    除了DTO以外,只要具备业务无关性与高可复用性的POJO的定义都可以防止在base模块中供业务使用。

    四、总结

    本篇是base包制作的上篇,从大家在日常开发过程中可能会碰到的一些问题出发,为大家介绍了base包在基础架构工程中的地位。

    1. 从业务无关性与与工具通用性的角度作为切入点,为大家介绍了Maven依赖在base包中的应用。
    2. 从前后端协同开发统一语言角度,为大家介绍了统一前后端数据结构的重要性与实现方式。
    3. 从单一POJO庞大后混乱的数据结构出发,为大家介绍正确划分POJO职责。

  • 相关阅读:
    MySQL数据库————存储过程和函数
    DPDK性能影响因素分析
    使用Mask R-CNN模型实现人体关键节点标注
    【大数据】Apache Iceberg 概述和源代码的构建
    【机器学习搞钱】Treynor-Mazu四因子模型
    【电源专题】案例:直接用LDO或Buck不香?为什么非要用Buck降压再转LDO?
    树莓派4B无屏幕连接Wi-Fi/启用ssh/创建用户
    子组件自定义事件$emit实现新页面弹窗关闭之后父界面刷新
    delaunay和voronoi图 人脸三角剖分
    Mybatis-动态SQL
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127785778