idea插件开发(二)基础框架代码

1,248 阅读8分钟

导读

上一篇文章中已经介绍cola框架的分层结构,本文需要开发一个基础的框架,提供第三方jar包的引入、通用的DTO、DO、Converter和Mybatis封装等等

模块规划

模块名说明
dependencies常用第三方jar包的引入和版本号的声明
base基础模块,提供工具类、断言和通用异常类
client提供对外的DTO、Query和Cmd
domain提供一些值对象、实体类和聚合根接口
infra提供基础DO类、Mybatis封装等

为了写文章方便,各个模块中包名就不再划分了,大家可以自行划分,下面依次创建以上模块。

项目的groupdIdcom.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;
}

BaseCmdBaseQuery一样,只是名字不一样,就不复制代码了

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用于统一对外输出的格式,并且提供了successerror两个静态方法,方便构造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值对象,如LongIdUserId以及实体和聚合根,依赖于base模块提供的一些工具类,不依赖其他任何模块

先来定义一个ID接口Identifier,之后所有ID值对象都实现自该接口

public interface Identifier {
}

定义两个ID值对象,分别是LongIdUserId,这个在任何系统中都会用到,所以直接在框架基础里定义了,受篇幅影响像UserNamePasswordRevision这些值对象就不写进来了

@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));
    }
}

LongIdUserId一样,就不复制代码了,其中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值对象与普通数据如何进行转换,之后所有ConverterAssembler都继承此接口,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.mybatisMapper,作用是可以自己写一些生成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插件开发环境、开发计划和基础名词解释

如果觉得不错欢迎点个赞,谢谢