本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。
本篇是手把手搭建基础架构专栏的第三篇。
相信你们在日常开发的过程中一定遇到过以下的问题
- 前后端交互结构混乱,response中业务code定义没有一个统一的规范
- PO、DTO、BO、VO傻傻分不清楚
- 工具类泛滥,同一工程中StringUtil的引用有外部引入,有内部jar包引入还有自己定义的
- 异常定义混乱,导致在Spring统一response拦截的地方区分业务异常与code错误困难
- 通用性高的枚举重复定义,比如是否枚举,男女枚举
- 通用的常量散落在业务系统中,导致各个业务系统中重复的逻辑定义
- ...
我们需要将上述这些与业务本身无关,但又是辅助业务开发的工具性质的定义放在一个统一的base包内。
让业务系统聚焦于业务本身。
本文所有代码均在此link,可提前clone下来,如果遇到报错,请按照如下步骤操作
你需要先clone common-dependency
然后执行mvn clean install 将 common-dependency包打到你本地仓库
否则你拉下来common-frame工程后会报找不到
<parent> <groupId>com.baiyan</groupId> <artifactId>common-dependency</artifactId> <version>1.0.0-SNAPSHOT</version> </parent>
一、Base包中Maven引入的规范
在遵守第一篇与第二篇Maven依赖引入的规范的前提下,我们在base包中可以引入什么样内部、外部的jar包呢?
- 业务无关性
- 工具类型
- 无需配置性
可以引入的示例
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
不建议引入的示例
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
二、定义统一的response body与biz code
前后端分离开发的时代,如何做好前后端的数据交互呢?
我在有的工程中看过这样的后端接口:直接将前端所需要的数据返回,不做任何包装。
这么做会有什么问题呢?
我们只能依据http code来响应后端的请求结果。无法跟前端约定业务code,来让前端在UI上做特定的展示。
而http code本身是请求级别的code定义,只是一个泛的定义。
比如登陆失败有很多种原因:账号不存在,密码错误,账号锁定等等。我们如果只用http code 401告诉前端登陆失败这显然用户交互非常不友好。
再比如,分布式架构体系下,一个请求上涉及很多服务,我们应该有一个统一的链路id将所有请求的日志串联起来,方便后续的日志定位。
综上我们应该定义规范的结构跟前端交互,让大家按照这个既定规范进行开发
普通接口响应结构
{
"code": 200, // 业务code定义,区别于http code标识细分的业务请求结果,比如10000标识账号错误,10001标识密码错误
"errorCode": null, // 发生错误时对于message的code,英文串描述,用于国际化异常映射
"message": "请求成功", // code为200时返回请求成功,10000时返回账号错误,10001返回密码错误。发生异常时,依据errorCode的定义映射国际化请求响应
"traceId": "", // 链路id,串联请求所关联所有应用的日志数据
"data": null // 是的返回的业务数据
}
列表分页数据响应请求
{
..., // 与普通的一致
"total": 100, // 查询条件下数据的总数
"data": [] // 是的返回的业务数据,list结构
}
三、如何正确划分数据载体
实体类作为数据的载体,大家日常工作中绝对会接触到,但是你真的正确使用了吗?
说一下我之前项目中看到的代码。数据查询得到的数据载体,service层交互的数据载体,rpc层交互的数据载体,web层交互的数据载体都集中在一个实体中。这个做法在业务场景特别简单的时候不会出现什么大问题,但是如果是一个比较庞大的业务应用体系。这样就会就会有问题了。
举个例子
用户表中一个四个字段,用户id,用户名,手机号,用户密码。现在需要在用户管理菜单页展示用户数据。如果只有一个实体的情况下,我从数据库里查询出来的数据拥有4个字段,把密码传递到前端肯定是不合适的。做一下脱敏,将password置为空。但是你在前端的报文中还是能看到
{
"userId":1
"userName":"admin"
"mobile":"13888888888"
"password":null
}
显然这个是不合理的,返回给前端的数据应该是
{
"userId":1
"userName":"admin"
"mobile":"13888888888"
}
显然对于java中的数据载体来说,每一层的分层是尤为重要的。我通常在会对数据载体做如下分层
实体类型 | 描述 |
---|---|
PO | 持久化对象,实体属性与表字段一一对应,DAO层产生,在Service层被使用 |
BO | 业务对象,聚合PO层数据,也可以多表关联数据查询聚合,内部会有属性的业务逻辑处理方法。DAO/Service层产生,Service层使用 |
DTO | 数据传输对象,常用语service层,rpc层,controller层,用于数组传输的载体,内部无逻辑 |
VO | 数据展示层,用于controller层,这里我习惯与方法的出参,用于切合DTO与VO层的结构差异 |
Query | 查询参数,controller层方法入参,接收前端的查询类型参数 |
Command | 指令性型参数,例如用户新增,用户修改的数据载体 |
说明:
- DTO与VO我常常会混用,如果数据传输载体只会在controller展示层中被组装使用,那直接返回给前端也可以,如果与前端要求不一致的情况,需要编写对应的Converter类进行处理,不可以将转换逻辑编写在DTO与VO中,他们只是数据载体。
- Command与DTO/VO,网上一些博主会将VO或者DTO作为web层入参进行数据的增删改。从结构化与定义上没有问题,但是这个跟数据载体带有指令就有点关联不上了。我对DTO与VO的理解是他们是结果型数据,是业务逻辑处理后的产物。而Command是指令性数据,通过Command类型参数,经由BO层业务逻辑,将数据映射到PO层与数据库交互。
- Query参数,与Command参数类似,常常有人会使用DTO或者VO来传递数据,一样的道理,业务语义不够强。
依据上面的规范我们能够划分好业务系统内部的实体,对于这些业务实体他们又有那些公用的逻辑呢?
1.DDD结构划分
如果我们的项目是DDD结构的分层,POJO需要有一个显示的标识符表明当前的POJO是什么左右,比如聚合根我会定义一个实体实现这个接口AggregateRoot来表明当前实体是聚合根
/**
* 聚合根标记
*
* @author baiyan
*/
public interface AggregateRoot extends MarkerInterface {
}
/**
* 用户聚合根
*
* @author baiyan
*/
public class User implements AggregateRoot {
}
2.统一的分页查询参数
分页查询参数规范基本上就是两种:
- limit/offset
- pageSize/PageNo
为了兼容以上两种情况,我们设计一个顶级的父类,将上面两种参数都一一关联起来。
@Data
@EqualsAndHashCode(callSuper = true)
public class KeywordQuery extends PageQuery {
@ApiModelProperty("关键字查询")
private String keyword;
}
后续我们如果有分页需求的时候,只需要继承这个顶级的查询父类,只需要在查询条件内定义业务参数即可。
3.顶级的PO类设计
PO是持久化实体,与表结构的字段一一对应。我们在设计表结构数据时,抛开业务不管,应该是要有一些公共的字段的:id、创建时间、修改时间、删除标识(如果数据删除是使用软删除的方式)
@Data
public class BaseUuidEntity {
/**
* 主键id 采用默认雪花算法
*/
@TableId
private Long id;
/**
* 创建时间
*/
private LocalDateTime gmtCreate;
/**
* 修改时间
*/
private LocalDateTime gmtModified;
/**
* 是否删除,0位未删除
*/
@TableLogic(delval = "current_timestamp()")
private Long deleted;
}
4.其他通用类型的mode抽取
再比如,我们经常会返回给前端一些key/value结构的数据,这种结构是具备通用性,我们可以将这种具备高通用的DTO也放在base模块中供业务使用。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DataDictDTO {
@ApiModelProperty("key值")
private String key;
@ApiModelProperty("value值")
private String value;
}
除了DTO以外,只要具备业务无关性与高可复用性的POJO的定义都可以防止在base模块中供业务使用。
四、总结
本篇是base包制作的上篇,从大家在日常开发过程中可能会碰到的一些问题出发,为大家介绍了base包在基础架构工程中的地位。
- 从业务无关性与与工具通用性的角度作为切入点,为大家介绍了Maven依赖在base包中的应用。
- 从前后端协同开发统一语言角度,为大家介绍了统一前后端数据结构的重要性与实现方式。
- 从单一POJO庞大后混乱的数据结构出发,为大家介绍正确划分POJO职责。
五、联系我
如果你觉得文章写得不错,点赞评论+关注,么么哒~
微信:baiyan_lou
我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~
DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信,备注DDD交流,我拉你进群,欢迎交流共同进步。