导读
上一篇文章中已经介绍cola框架的分层结构,本文需要开发一个基础的框架,提供第三方jar包的引入、通用的DTO、DO、Converter和Mybatis封装等等
模块规划
模块名 | 说明 |
---|---|
dependencies | 常用第三方jar包的引入和版本号的声明 |
base | 基础模块,提供工具类、断言和通用异常类 |
client | 提供对外的DTO、Query和Cmd |
domain | 提供一些值对象、实体类和聚合根接口 |
infra | 提供基础DO类、Mybatis封装等 |
为了写文章方便,各个模块中包名就不再划分了,大家可以自行划分,下面依次创建以上模块。
项目的groupdId
为com.ahydd.ddd
,大家可自行修改。
dependencies模块
这个模块只需要添加一个pom.xml文件即可,将常用的第三方jar引入进来,把版本号也声明好,之后其他模块和其他项目中不用再去找版本号了。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ahydd.ddd</groupId>
<artifactId>dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<druid.version>1.1.10</druid.version>
<mysql.version>8.0.16</mysql.version>
<mybatis.version>1.3.0</mybatis.version>
<tk.mybatis.version>2.0.2</tk.mybatis.version>
<jackson.version>2.11.2</jackson.version>
<guava.version>20.0</guava.version>
<mapstruct.version>1.4.2.Final</mapstruct.version>
<testable.version>0.4.9</testable.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${tk.mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.22</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
base模块
pom.xml中指定parent,其他没有什么好说的
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ahydd.ddd</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>base</artifactId>
<packaging>jar</packaging>
</project>
下面编写需要用到的几个常用类
PageQo
用来存放客户端请求中关于分页和排序的请求参数
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageQo {
private Integer page;
private Integer pageSize;
private String orderBy;
private String orderDir;
public PageQo(Integer page, Integer pageSize) {
this.page = page;
this.pageSize = pageSize;
}
}
OrderQo
用来存放客户端请求中关于排序的请求参数,这种就不允许分页了
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderQo {
public static final String ASC = "asc";
public static final String DESC = "desc";
private String orderBy;
private String orderDir;
}
Pager
对于分页数据,需要存放得到的集合和分页信息,如第几页、每页多少条、总记录数和页数
@Data
@NoArgsConstructor
public class Pager<T> implements Serializable {
private static final long serialVersionUID = -2033174573373926746L;
public static final int DEFAULT_PAGE_SIZE = 10;
public static final int MAX_PAGE_SIZE = 100;
private PageInfo page;
private List<T> list;
public Pager(int page, int pageSize, int recordCount, List<T> list) {
this.page = new PageInfo(page, pageSize, recordCount);
this.list = list;
}
public static void initPageQo(PageQo pageQo) {
if (pageQo.getPage() == null) {
pageQo.setPage(1);
}
if (pageQo.getPageSize() == null) {
pageQo.setPageSize(DEFAULT_PAGE_SIZE);
}
pageQo.setPage(Math.max(pageQo.getPage(), 1));
pageQo.setPageSize(Math.min(pageQo.getPageSize(), MAX_PAGE_SIZE));
}
@Data
public static class PageInfo {
private int page;
private int pageSize;
private int pageCount;
private int recordCount;
public PageInfo(int page, int pageSize, int recordCount) {
this.page = Math.max(page, 1);
this.pageSize = pageSize < 1 ? DEFAULT_PAGE_SIZE : pageSize;
this.pageSize = Math.min(this.pageSize, MAX_PAGE_SIZE);
this.recordCount = recordCount;
if (recordCount == 0 || pageSize == 0) {
this.pageCount = 0;
} else {
this.pageCount = (int) Math.ceil((double) recordCount / pageSize);
}
}
}
}
异常
对于异常,我们定义一个基础的通用异常类,之后也方便统一异常处理
public class CommonException extends RuntimeException {
public CommonException(String msg) {
super(msg);
}
}
断言
方便之后的一些逻辑判断并抛出自定义的通用异常,为了减小文章篇幅,很多方法已经删除了,大家可以自行添加更多方法
public class Assert {
public static void isNull(Object obj, String errorMsg) {
if (obj == null) {
throw new CommonException(errorMsg);
}
}
public static void isEmpty(String data, String errorMsg) {
if (StringUtils.isEmpty(data)) {
throw new CommonException(errorMsg);
}
}
public static void isEmpty(Collection<?> data, String errorMsg) {
if (data == null || data.isEmpty()) {
throw new CommonException(errorMsg);
}
}
public static void isTrue(boolean expression, String errorMsg) {
if (expression) {
throw new CommonException(errorMsg);
}
}
public static void isFalse(boolean expression, String errorMsg) {
if (!expression) {
throw new CommonException(errorMsg);
}
}
}
StringUtils
提供对字符串处理的工具类,可以将首字母大小写、横线和下划线转驼峰这些方法写进来
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
}
client模块
此模块主要定义基础的Query、Cmd和Dto,其中Query用于GET请求,Cmd用于写入请求,如POST、PUT和DELETE请求,而Dto用于输出。一般都需要实现Serializable
,本文中为了方便没有这么做,大家可自行添加代码。
pom.xml很简单,和base模块一样指定parent就可以了
我的计划中每个表都有ID字段,且都是Long类型,一般Query和Cmd也都需要传入id,所以在基础Query和Cmd中都只有一个id字段
Query和Cmd
@Data
public class BaseQuery {
private Long id;
}
BaseCmd
和BaseQuery
一样,只是名字不一样,就不复制代码了
BaseListQuery
用于查询列表,所以只需要指定排序方式就可以了,代码如下:
@Data
public class BaseListQuery extends BaseQuery {
private String orderBy;
private String orderDir;
}
BasePageQuery
用于查询带分页的列表,可以继承自BaseListQuery
,再添加分页的两个字段,如下:
@Data
public class BasePageQuery extends BaseListQuery {
private Integer page;
private Integer pageSize;
}
DTO
PagerDto
用于对外输出分页类型的数据
@Data
public class PagerDto<T> {
private List<T> list;
private Pager.PageInfo page;
}
ListDto
用于对外输出不带分页的列表数据
@Data
public class ListDto<T> {
private List<T> list;
}
Result
用于统一对外输出的格式,并且提供了success
和error
两个静态方法,方便构造Result
对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
public static int SUCCESS = 0;
public static int ERROR = 1;
public static final String MSG_OK = "OK";
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(String msg) {
return new Result<>(Result.SUCCESS, msg, null);
}
public static <T> Result<T> success(String msg, T data) {
return new Result<>(Result.SUCCESS, msg, data);
}
public static <T> Result<T> error(String msg) {
return new Result<>(Result.ERROR, msg, null);
}
public static <T> Result<T> error(String msg, T data) {
return new Result<>(Result.ERROR, msg, data);
}
}
根据Result
的定义,Result<PagerDto<UserDto>>
输出的JSON格式如下:
{
"code": 0,
"msg": "xxx",
"data": {
"list": [],
"page": {
"page": 1,
"pageSize": 10,
"pageCount": 1,
"recordCount": 8
}
}
}
domain模块
domain模块需要定义一些ID值对象,如LongId
和UserId
以及实体和聚合根,依赖于base
模块提供的一些工具类,不依赖其他任何模块
先来定义一个ID接口Identifier
,之后所有ID值对象都实现自该接口
public interface Identifier {
}
定义两个ID值对象,分别是LongId
和UserId
,这个在任何系统中都会用到,所以直接在框架基础里定义了,受篇幅影响像UserName
、Password
和Revision
这些值对象就不写进来了
@Getter
@ToString
public class UserId implements Identifier {
private final Long id;
public UserId(Long id) {
Assert.isNullOrLessThanZero(id, "用户ID不正确");
this.id = id;
}
public static Optional<UserId> of(Long id) {
if (id == null) {
return Optional.empty();
}
return Optional.of(new UserId(id));
}
}
LongId
和UserId
一样,就不复制代码了,其中of
方法是为了简化外部调用,值对象要么是null,要么就是有效的,所以当of
方法传入的参数=null时返回的就是Optional.empty()
,省去在外部调用of
方法前还要进行id != null
判断。
有了值对象之后,可以继续定义实体相关的接口Identifiable
来规定实体类必须有getId
方法,之后所有实体类接口都继承自此接口
public interface Identifiable<ID extends Identifier> {
ID getId();
}
定义实体类接口Entity
继承自Identifiable
接口,Entity
用于非聚合根的实体,如订单和订单商品中订单是聚合根,订单商品就是Entity
public interface Entity<ID extends Identifier> extends Identifiable<ID> {
}
定义聚合根接口Aggregate
public interface Aggregate<ID extends Identifier> extends Entity<ID> {
}
到这里聚合根和实体的接口已经定义好了,下面可以定义基础的聚合根和实体类,我们规定所有实体类中都有以下字段
字段名 | 说明 |
---|---|
revision | 版本号 |
createdBy | 创建者ID |
createdTime | 创建时间 |
updatedBy | 更新者ID |
updatedTime | 更新时间 |
isDelete | 是否删除 |
deletedBy | 删除者ID |
deletedTime | 删除时间 |
注意:这些字段是领域实体类的字段,并不是和数据库表里的字段一一对应的关系
结合以上的字段,定义基础实体类
@Data
public class BaseEntity<ID extends Identifier> implements Entity<ID> {
protected ID id;
protected Integer revision;
protected UserId createdBy;
protected Integer createdTime;
protected UserId updatedBy;
protected Integer updatedTime;
protected Boolean isDelete;
protected UserId deletedBy;
protected Integer deletedTime;
/**
* 初始化对象的默认字段
* @param userId 操作人员ID
*/
public void init(UserId userId) {
int nowTime = 当前时间戳;
if (id == null) {
revision = 0;
createdBy = userId;
createdTime = nowTime;
isDelete = false;
} else {
updatedBy = userId;
updatedTime = nowTime;
}
}
}
基础聚合根直接继承自BaseEntity
就好
@Data
public class BaseAggregate<ID extends Identifier> extends BaseEntity<ID> implements Aggregate<ID> {
}
最后还要定义一个Converter
接口,指明ID值对象与普通数据如何进行转换,之后所有Converter
和Assembler
都继承此接口,MapStruct会自动调用这些default方法进行转换,比如需要将UserId对象转为Long时,就会自动调用fromUserId
方法
public interface Converter {
default Long fromUserId(UserId id) {
if (id == null) {
return null;
}
return id.getId();
}
default UserId toUserId(Long id) {
return UserId.of(id).orElse(null);
}
}
LongId
方法也一样,这里就不写了
infra模块
把对数据的操作全放在这一层了,pom.xml需要引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ahydd.ddd</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>infra</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.ahydd.ddd</groupId>
<artifactId>client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.ahydd.ddd</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
定义一个Mapper
继承自tk.mybatis
的Mapper
,作用是可以自己写一些生成SQL语句的方法,比如加一个replace
方法实现MySQL中的replace函数
@RegisterMapper
public interface BaseMapper<T> extends Mapper<T>, MySqlMapper<T>, AggregationMapper<T> {
@UpdateProvider(type = BaseProvider.class, method = "dynamicSQL")
int replace(String fieldName, String oldValue, String newValue, Example example);
}
实现replace函数的代码
public class BaseProvider extends MapperTemplate {
public BaseProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
public String replace(MappedStatement ms) {
final Class<?> entityClass = getEntityClass(ms);
StringBuilder sb = new StringBuilder();
if (getConfig().isSafeUpdate()) {
sb.append(SqlHelper.exampleHasAtLeastOneCriteriaCheck("example"));
}
// 注意这里是拼接的SQL语句,所以fieldName一定要确保没有注入的可能,我们使用时这个fieldName肯定是手写的,不可能是外部传入的
sb.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
sb.append(" set `${fieldName}`=replace(`${fieldName}`, #{oldValue}, #{newValue})");
sb.append(SqlHelper.updateByExampleWhereClause());
return sb.toString();
}
}
BaseProvider
中SQL语句使用了${}
进行拼接,但是和注释上写的一样,fieldName是代码中手写的,不会被注入,比如update xxx set xxx=replace(xxx, 'a', 'b') where xxx=xxx
,但是如果fieldName从外部传入那这样拼接肯定不行
接下来定义一个BaseDo
类,这是之后所有Data Object的基类,规定了每个表必须有以下这些字段
@Data
public class BaseDo {
/**
* ID
*/
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 乐观锁
*/
@Column(name = "revision")
private Integer revision;
/**
* 创建人
*/
@Column(name = "created_by")
private Long createdBy;
/**
* 创建时间
*/
@Column(name = "created_time")
private Integer createdTime;
/**
* 更新人
*/
@Column(name = "updated_by")
private Long updatedBy;
/**
* 更新时间
*/
@Column(name = "updated_time")
private Integer updatedTime;
/**
* 删除标识
*/
@Column(name = "is_delete")
private Boolean isDelete;
/**
* 删除人
*/
@Column(name = "deleted_by")
private Long deletedBy;
/**
* 删除时间
*/
@Column(name = "deleted_time")
private Integer deletedTime;
}
最后定义BaseDaoImpl类,对Mybatis的常用操作进行一次封装,方便使用
public class BaseDaoImpl<T extends BaseDo, D extends BaseMapper<T>> {
@Autowired
private D mapper;
public List<T> list(Example example) {
setDefaultExample(example);
return mapper.selectByExample(example);
}
public Pager<T> page(Example example, PageQo pageQo) {
setDefaultExample(example);
Pager.initPageQo(pageQo);
PageHelper.startPage(pageQo.getPage(), pageQo.getPageSize());
List<T> list = mapper.selectByExample(example);
PageInfo<T> pageInfo = new PageInfo<>(list);
return new Pager<>(pageQo.getPage(), pageQo.getPageSize(), (int) pageInfo.getTotal(), pageInfo.getList());
}
public int count(Example example) {
setDefaultExample(example);
return mapper.selectCountByExample(example);
}
public T findOne(Example example) {
setDefaultExample(example);
RowBounds rowBounds = new RowBounds(0, 1);
List<T> list = mapper.selectByExampleAndRowBounds(example, rowBounds);
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
public T insert(T t) {
mapper.insert(t);
return t;
}
public int insertList(List<T> ts) {
return mapper.insertList(ts);
}
public T update(T t) {
mapper.updateByPrimaryKey(t);
return t;
}
public int delete(T t) {
return mapper.delete(t);
}
public void setDefaultExample(Example example) {
example.and().andEqualTo("isDelete", false);
}
}
只写了一些最常用的方法封装,其他方法太多了,就不发出来了
到这里基础框架已经可以使用了,下篇文章开始搭建IDEA插件开发环境、开发计划和基础名词解释
如果觉得不错欢迎点个赞,谢谢