如果写一个和Mybatis-Plus类似的代码生成框架,思路比较容易想到。核心的几个步骤就是:
- 获取数据库表、字段信息;
- 添加相应的规则,补充额外的信心。
- 根据表、字段信息、新加的规则生成对应的代码;
Mybatis-Plus整个框架依赖于Spring、Mybatis、模板引擎(freemaker或者velocity)和日志框架slf4j。
整个架构如下:
如上所示:
- core:是整个框架的核心。包含了对数据库实体的反射提取,分析数据表的字段的信息,数据的CRUD
- support:定义了相关的接口
- generate:赋值对相关代码的生成。
本篇只针对Generate部分进行分析。
Generate (代码生成)
其使用方式为首先创建一个AutoGenerator
对象,此对象里面包含了所有的相关的配置信息,按照配置的不同类型组织为:
- 总配置ConfigBuilder,
会对下面的各个配置汇总。
- 注入配置InjectionConfig
- 数据源配置DataSourceConfig
- 数据表配置StrategyConfig
- 包配置PackageConfig
- 模板配置TemplateConfig
- 全局配置GlobalConfig
使用
AutoGenerator
生成代码的时候,外界设定好相关的配置类,然后赋值给AutoGenerator
相关属性,最后调用execute
即可,非常简单。但是各种各样的配置非常多,如果没有深入的去了解,很有可能不能充分利用这个框架。
Config(配置详解)
ConfigBuilder
ConfigBuilder
会对所有的配置再一次封装,比如对某些为null的配置设定为默认值,过参数进行过滤、验证等等。各个配置都有了之后,调用对应的handler执行处理。整个处理过程一定要注意配置初始化的顺序,不能打乱,比如最终的表生成策略依赖于模板配置、数据源配置等等。
具体来讲对应代码如下:
public ConfigBuilder(PackageConfig packageConfig,
DataSourceConfig dataSourceConfig,
StrategyConfig strategyConfig,
TemplateConfig template,
GlobalConfig globalConfig) {
// 全局配置
if (null == globalConfig) {
this.globalConfig = new GlobalConfig();
} else {
this.globalConfig = globalConfig;
}
// 模板配置
if (null == template) {
this.template = new TemplateConfig();
} else {
this.template = template;
}
// 包配置
if (null == packageConfig) {
handlerPackage(this.template, this.globalConfig.getOutputDir(), new PackageConfig());
} else {
handlerPackage(this.template, this.globalConfig.getOutputDir(), packageConfig);
}
this.dataSourceConfig = dataSourceConfig;
handlerDataSource(dataSourceConfig);
// 策略配置
if (null == strategyConfig) {
this.strategyConfig = new StrategyConfig();
} else {
this.strategyConfig = strategyConfig;
}
handlerStrategy(this.strategyConfig);
}
这个类比较重要,对于各个配置的handle也是在其进行。比如获取表属性。
GlobalConfig
全局配置主要是对于整个自定生成环境的配置。如目录,开发人员名称,是否使用基类,文件命名等。具体来讲包含有如下配置:
/**
* 生成文件的输出目录【默认 D 盘根目录】
*/
private String outputDir = "D://";
/**
* 是否覆盖已有文件
*/
private boolean fileOverride = false;
/**
* 是否打开输出目录
*/
private boolean open = true;
/**
* 是否在xml中添加二级缓存配置
*/
private boolean enableCache = true;
/**
* 开发人员
*/
private String author;
/**
* 开启 Kotlin 模式
*/
private boolean kotlin = false;
/**
* 开启 ActiveRecord 模式
*/
private boolean activeRecord = true;
/**
* 开启 BaseResultMap
*/
private boolean baseResultMap = false;
/**
* 开启 baseColumnList
*/
private boolean baseColumnList = false;
/**
* 各层文件名称方式,例如: %Action 生成 UserAction
*/
private String mapperName;
private String xmlName;
private String serviceName;
private String serviceImplName;
private String controllerName;
/**
* 指定生成的主键的ID类型
*/
private IdType idType;
需要注意的几个点:
生成文件的输出目录因操作系统不同而不同。默认是Windows的D盘
生成的主键的ID类型有多种。
AUTO(0, "数据库ID自增"), INPUT(1, "用户输入ID"), ID_WORKER(2, "全局唯一ID"), UUID(3, "全局唯一ID"), NONE(4, "该类型为未设置主键类型"), ID_WORKER_STR(5, "字符串全局唯一ID");
PackageConfig
包相关的配置,这个配置比较简单。具体来讲:
/**
* 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名
*/
private String parent = "com.baomidou";
/**
* 父包模块名。
*/
private String moduleName = null;
/**
* Entity包名
*/
private String entity = "entity";
/**
* Service包名
*/
private String service = "service";
/**
* Service Impl包名
*/
private String serviceImpl = "service.impl";
/**
* Mapper包名
*/
private String mapper = "mapper";
/**
* Mapper XML包名
*/
private String xml = "mapper.xml";
/**
* Controller包名
*/
private String controller = "web";
其实也就是对最终生成的目录结构的设定。一个简单的例子:
TemplateConfig
模板配置类,主要是对生成代码文件格式的配置。我们生成不用层次的类对应的模板是不同的,虽然可以通过字符串的方式来实现具体的自动生成。但是使用模板技术更加简单。对于每个层采用不同的模板:
private String entity = ConstVal.TEMPLATE_ENTITY_JAVA;
private String service = ConstVal.TEMPLATE_SERVICE;
private String serviceImpl = ConstVal.TEMPLATE_SERVICEIMPL;
private String mapper = ConstVal.TEMPLATE_MAPPER;
private String xml = ConstVal.TEMPLATE_XML;
private String controller = ConstVal.TEMPLATE_CONTROLLER;
这里的ConstVal
里面定义全局常量。
以模板文件mapper.java.vm为例:
package ${package.Mapper};
import ${package.Entity}.${entity};
import ${superMapperClassPackage};
/**
* <p>
* $!{table.comment} Mapper 接口
* </p>
*
* @author ${author}
* @since ${date}
*/
#if(${kotlin})
interface ${table.mapperName} : ${superMapperClass}<${entity}>
#else
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
}
#end
其对应生成的代码
package com.baomidou.test.mapper;
import com.baomidou.test.entity.Permission;
import com.baomidou.mybatisplus.mapper.BaseMapper;
/**
* <p>
* 权限表 Mapper 接口
* </p>
*
* @author Yanghu
* @since 2018-06-08
*/
public interface PermissionMapper extends BaseMapper<Permission> {
}
DataSourceConfig
数据库配置主要就是定义好相关数据库,用户名,密码等,便于连接到数据库读到想的表字段。具体来讲包含对如下信息的配置:
/**
* 数据库信息查询
*/
private IDbQuery dbQuery;
/**
* 数据库类型
*/
private DbType dbType;
/**
* PostgreSQL schemaname
*/
private String schemaname = "public";
/**
* 类型转换
*/
private ITypeConvert typeConvert;
/**
* 驱动连接的URL
*/
private String url;
/**
* 驱动名称
*/
private String driverName;
/**
* 数据库连接用户名
*/
private String username;
/**
* 数据库连接密码
*/
private String password;
需要说明一下IDbQuery、ITypeConvert。
- IDbQuery是一个接口里面对查询数据库表、字段、注释信息的封装。因为需要满足多种数据库的自动生成,所以需要正对不同数据库实现IDbQuery不同的类。比如MySqlQuery就是对IDbQuery一种实现
@Override
public DbType dbType() {
return DbType.MYSQL;
}
@Override
public String tablesSql() {
return "show table status";
}
@Override
public String tableFieldsSql() {
return "show full fields from `%s`";
}
@Override
public String tableName() {
return "NAME";
}
@Override
public String tableComment() {
return "COMMENT";
}
@Override
public String fieldName() {
return "FIELD";
}
@Override
public String fieldType() {
return "TYPE";
}
@Override
public String fieldComment() {
return "COMMENT";
}
@Override
public String fieldKey() {
return "KEY";
}
@Override
public boolean isKeyIdentity(ResultSet results) throws SQLException {
return "auto_increment".equals(results.getString("Extra"));
}
- ITypeConvert接口是把数据库中filed中的类型转为java中对应的数据类型。具体来讲MySqlTypeConvert实现如下:
@Override
public DbColumnType processTypeConvert(String fieldType) {
String t = fieldType.toLowerCase();
if (t.contains("char") || t.contains("text")) {
return DbColumnType.STRING;
} else if (t.contains("bigint")) {
return DbColumnType.LONG;
} else if (t.contains("int")) {
return DbColumnType.INTEGER;
} else if (t.contains("date") || t.contains("time") || t.contains("year")) {
return DbColumnType.DATE;
} else if (t.contains("text")) {
return DbColumnType.STRING;
} else if (t.contains("bit")) {
return DbColumnType.BOOLEAN;
} else if (t.contains("decimal")) {
return DbColumnType.BIG_DECIMAL;
} else if (t.contains("clob")) {
return DbColumnType.CLOB;
} else if (t.contains("blob")) {
return DbColumnType.BLOB;
} else if (t.contains("binary")) {
return DbColumnType.BYTE_ARRAY;
} else if (t.contains("float")) {
return DbColumnType.FLOAT;
} else if (t.contains("double")) {
return DbColumnType.DOUBLE;
} else if (t.contains("json") || t.contains("enum")) {
return DbColumnType.STRING;
}
return DbColumnType.STRING;
}
注意这里为什么用contains
作为判断,因为mysql中可以设置数据类型具体大小。虽然大小不固定但是,其前缀是固定。
- 其次还在DataSourceConfig中创建了数据库连接对象提供给外界使用。
public Connection getConn() {
Connection conn = null;
try {
Class.forName(driverName);
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
StrategyConfig
对具体生成的表字段进行配置。比如去掉哪些表前缀,字段前缀;定义生成entity的公告字段及相关基类;排除对哪些表自动生成,或者对哪些表自动生成;是否根据表生成对应的注释。具体来讲有如下设置:
/**
* 表名、字段名、是否使用下划线命名(默认 false)
*/
public static boolean DB_COLUMN_UNDERLINE = false;
/**
* 是否大写命名
*/
private boolean isCapitalMode = false;
/**
* 是否跳过视图
*/
private boolean skipView = false;
/**
* 数据库表映射到实体的命名策略
*/
private NamingStrategy naming = NamingStrategy.nochange;
/**
* 数据库表字段映射到实体的命名策略<br/>
* 未指定按照 naming 执行
*/
private NamingStrategy columnNaming = null;
/**
* 表前缀
*/
private String[] tablePrefix;
/**
* 字段前缀
*/
private String[] fieldPrefix;
/**
* 自定义继承的Entity类全称,带包名
*/
private String superEntityClass;
/**
* 自定义基础的Entity类,公共字段
*/
private String[] superEntityColumns;
/**
* 自定义继承的Mapper类全称,带包名
*/
private String superMapperClass = ConstVal.SUPERD_MAPPER_CLASS;
/**
* 自定义继承的Service类全称,带包名
*/
private String superServiceClass = ConstVal.SUPERD_SERVICE_CLASS;
/**
* 自定义继承的ServiceImpl类全称,带包名
*/
private String superServiceImplClass = ConstVal.SUPERD_SERVICEIMPL_CLASS;
/**
* 自定义继承的Controller类全称,带包名
*/
private String superControllerClass;
/**
* 需要包含的表名(与exclude二选一配置)
*/
private String[] include = null;
/**
* 需要排除的表名
*/
private String[] exclude = null;
/**
* 【实体】是否生成字段常量(默认 false)<br>
* -----------------------------------<br>
* public static final String ID = "test_id";
*/
private boolean entityColumnConstant = false;
/**
* 【实体】是否为构建者模型(默认 false)<br>
* -----------------------------------<br>
* public User setName(String name) { this.name = name; return this; }
*/
private boolean entityBuilderModel = false;
/**
* 【实体】是否为lombok模型(默认 false)<br>
* <a href="https://projectlombok.org/">document</a>
*/
private boolean entityLombokModel = false;
/**
* Boolean类型字段是否移除is前缀(默认 false)<br>
* 比如 : 数据库字段名称 : 'is_xxx',类型为 : tinyint. 在映射实体的时候则会去掉is,在实体类中映射最终结果为 xxx
*/
private boolean entityBooleanColumnRemoveIsPrefix = false;
/**
* 生成 <code>@RestController</code> 控制器
* <pre>
* <code>@Controller</code> -> <code>@RestController</code>
* </pre>
*/
private boolean restControllerStyle = false;
/**
* 驼峰转连字符
* <pre>
* <code>@RequestMapping("/managerUserActionHistory")</code> -> <code>@RequestMapping("/manager-user-action-history")</code>
* </pre>
*/
private boolean controllerMappingHyphenStyle = false;
/**
* 是否生成实体时,生成字段注解
*/
private boolean entityTableFieldAnnotationEnable = false;
/**
* 乐观锁属性名称
*/
private String versionFieldName;
/**
* 逻辑删除属性名称
*/
private String logicDeleteFieldName;
/**
* 表填充字段
*/
private List<TableFill> tableFillList = null;
小结
相关配置介绍完了,可以看到每一个配置对应一个具体的层面。这样的好处在于职责清晰。从代码层面上讲也使用到了诸如观面模式,策略模式等。
Handle(处理配置)
各个配置都设置好了之后就开始进行处理了。其实就调用了一个方法而已
public void execute() {
logger.debug("==========================准备生成文件...==========================");
if(null == this.config) {
this.config = new ConfigBuilder(this.packageInfo, this.dataSource, this.strategy, this.template, this.globalConfig);
if(null != this.injectionConfig) {
this.injectionConfig.setConfig(this.config);
}
}
if(null == this.templateEngine) {
this.templateEngine = new VelocityTemplateEngine();
}
this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
logger.debug("==========================文件生成完成!!!==========================");
}
上面代理主要分为两步。
- 第一步:根据配置信息,实例化一个ConfigBuilder。它屏蔽了对配置如何处理的细节,初始化完成之后,ConfigBuilder就完成了对所有配置的加载,以及对应数据库表、字段的提取。
- 第二步:调用模板引擎,传入ConfigBuilder。模板引擎根据ConfigBuilder填充对应的模板。最终生成代码。
构造ConfigBuilder
在构造ConfigBuilder中,会一一相关的配置进行handle。这里主要讲一下handlerStrategy
,因为这个方法包含了对数据库信息的提取过程,并且将数据库表信息映射为实体。
最终会走到一个名叫getTablesInfo
方法。里面涉及到两个基本的、对数据表抽象的实体TableField
与TableInfo
基础实体
TableField
TableField
的内容:
/**
* 是否需要进行转换
*/
private boolean convert;
/**
* 是否为主键
*/
private boolean keyFlag;
/**
* 主键是否为自增类型
*/
private boolean keyIdentityFlag;
/**
* 对应数据表的名称
*/
private String name;
/**
* 转换之后的类型
*/
private String type;
/**
* 转换之后的属性名
*/
private String propertyName;
/**
* 对应数据表的类型
*/
private DbColumnType columnType;
/**
* 该字段的注释信息
*/
private String comment;
/**
* 填充信息
*/
private String fill;
/**
* 自定义查询字段列表
*/
private Map<String, Object> customMap;
注意在设置setConvert的时候是传入的一个StrategyConfig,根据StrategyConfig的某些配置确定是否需要转换。
TableInfo
TableInfo是对数据表的抽象,具体来讲:
/**
* 是否转换
*/
private boolean convert;
/**
* 表名
*/
private String name;
/**
* 表注释
*/
private String comment;
/**
* 表所对应的实体名
*/
private String entityName;
/**
* 表所对应的mapper名
*/
private String mapperName;
/**
* 表所对应的xml名
*/
private String xmlName;
/**
* 表所对应的service名
*/
private String serviceName;
/**
* 表所对应的serviceimpl名
*/
private String serviceImplName;
/**
* 表所对应的controller名
*/
private String controllerName;
/**
* 表所包含的所有field集合
*/
private List<TableField> fields;
/**
* 公共字段
*/
private List<TableField> commonFields;
/**
* 所依赖的包名
*/
private List<String> importPackages = new ArrayList<>();
/**
* 说有字段连在一起的字符串,用于日志信息
*/
private String fieldNames;
这需要注意一点的就是在设置fields的时候需要根据fields的数据类型引入相应的包名。做法就是在设置fields的时候根据field类型引入。具体来讲:
public void setFields(List<TableField> fields) {
if (CollectionUtils.isNotEmpty(fields)) {
this.fields = fields;
// 收集导入包信息,注意为什么用HashSet。因为HashSetk可以自动去除重复的key
Set<String> pkgSet = new HashSet<>();
for (TableField field : fields) {
if (null != field.getColumnType() && null != field.getColumnType().getPkg()) {
pkgSet.add(field.getColumnType().getPkg());
}
if (field.isKeyFlag()) {
// 主键
if (field.isConvert() || field.isKeyIdentityFlag()) {
pkgSet.add("com.baomidou.mybatisplus.annotations.TableId");
}
// 自增
if (field.isKeyIdentityFlag()) {
pkgSet.add("com.baomidou.mybatisplus.enums.IdType");
}
} else if (field.isConvert()) {
// 普通字段
pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
}
if (null != field.getFill()) {
// 填充字段
pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
pkgSet.add("com.baomidou.mybatisplus.enums.FieldFill");
}
}
if (!pkgSet.isEmpty()) {
this.importPackages = new ArrayList<>(Arrays.asList(pkgSet.toArray(new String[]{})));
}
}
}
getTablesInfo
先介绍一下其中的处理逻辑:
判断是否设置了同时设置了include和exclude
保存所有表的信息,包含排除的表,需要生成的表。
-
根据sql查询表的信息,然后依次将数据映射到对应的基础实体上(TableInfo,FiledInfo)。
- 如果设置了include或者exclude,再进一步删选。比如过滤用户输入不存在的表。
- 最终得到includeTableList表,礼包包含了需要转换的表名称
- 调用
convertTableFields
将表中的filed转为基础实体。 - 最后调用
processTable
将表、字段信息、其他信息填充到TableInfo中。完成TableInfo基础实体的构造。
其中用到的几条sql语句如下:
-
show table status
:获取表信息,比如表名、创建(修改)时间、表注释,数量条数等
-
show full fields from xxx
:从特定表中获取表所有字段的信息,比如字段名、字段类型,字段注释以及该表的主键等。
其中使用的而是JDBC最为简单的读取数据库。代码简化一下
//映射Tables
preparedStatement = connection.prepareStatement(tablesSql);
ResultSet results = preparedStatement.executeQuery();
TableInfo tableInfo;
while (results.next()) {
......
includeTableList.add(tableInfo);
}
//映射Fields
for (TableInfo ti : includeTableList) {
PreparedStatement preparedStatement = connection.prepareStatement(tableFieldsSql);
ResultSet results = preparedStatement.executeQuery();
TableField field = new TableField();
while (results.next()) {
......
fieldList.add(field);
}
}
tableInfo.setFields(fieldList);
tableInfo.setCommonFields(commonFieldList);
}
至此所有表以及所有表对应的字段已经完全映射到了基础实体。接下来就是根据基础实体的内容,填充对应的模板。
Template(模板生成)
有了上面所产生的实体,下面就是填模板的过程了。入口如下:
if(null == this.templateEngine) {
this.templateEngine = new VelocityTemplateEngine();
}
this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
TemplateEngine(模板引擎)
MP现在支持两种模板引擎Velocity、Freemarker。这里以Velocity为例。
模板生成相关一共有三个类,分别是AbstractTemplateEngine、FreemarkerTemplateEngine、VelocityTemplateEngine。AbstractTemplateEngine是抽象类定义了相关的接口。具体来讲,提供了如下信息:
其中的writer
与templateFilePath
为抽象方法,根据不同的模板引擎,选择不同的实现。最终是调用batchOutput来输出所有自动生成的代码。
在batchOutPut中将各个层级的对象,根据模板路径,生成最终的文件。
//遍历所有的表信息,生成文件
List<TableInfo> tableInfoList = this.getConfigBuilder().getTableInfoList();
for (TableInfo tableInfo : tableInfoList) {
Map<String, Object> objectMap = this.getObjectMap(tableInfo);
Map<String, String> pathInfo = this.getConfigBuilder().getPathInfo();
TemplateConfig template = this.getConfigBuilder().getTemplate();
// Mp.java
String entityName = tableInfo.getEntityName();
if (null != entityName && null != pathInfo.get(ConstVal.ENTITY_PATH)) {
String entityFile = String.format((pathInfo.get(ConstVal.ENTITY_PATH) + File.separator + "%s" + this.suffixJavaOrKt()), entityName);
if (this.isCreate(entityFile)) {
this.writer(objectMap, this.templateFilePath(template.getEntity(this.getConfigBuilder().getGlobalConfig().isKotlin())), entityFile);
}
}
......
// MpMapper.xml
// IMpService.java
// MpServiceImpl.java
// MpController.java
}
}
}
接下来看一下VelocityTemplateEngine中的write方法。首先会进行初始化配置
public VelocityTemplateEngine init(ConfigBuilder configBuilder) {
//将configBuilder传给父类,在父类中需要用到
super.init(configBuilder);
if (null == velocityEngine) {
Properties p = new Properties();
p.setProperty(ConstVal.VM_LOADPATH_KEY, ConstVal.VM_LOADPATH_VALUE);
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
p.setProperty(Velocity.ENCODING_DEFAULT, ConstVal.UTF8);
p.setProperty(Velocity.INPUT_ENCODING, ConstVal.UTF8);
p.setProperty("file.resource.loader.unicode", "true");
//初始化模板引擎
velocityEngine = new VelocityEngine(p);
}
return this;
}
父类中调用writer,并将objectMap(包含所有的映射信息)传入,根据templatePath(不同类型模板不一样)创建template。最后将模板内容依据objectMap替换掉。其中的模板路径则根据之前的TemplateConfig得到
@Override
public void writer(Map<String, Object> objectMap, String templatePath, String outputFile) throws Exception {
if (StringUtils.isEmpty(templatePath)) {
return;
}
Template template = velocityEngine.getTemplate(templatePath, ConstVal.UTF8);
FileOutputStream fos = new FileOutputStream(outputFile);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, ConstVal.UTF8));
template.merge(new VelocityContext(objectMap), writer);
writer.close();
logger.debug("模板:" + templatePath + "; 文件:" + outputFile);
}
这里以生存entity为例。
生成的entity
package com.baomidou.test.entity;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.activerecord.Model;
import java.io.Serializable;
/**
* <p>
* 权限表
* </p>
*
* @author wesly
* @since 2018-06-08
*/
public class Permission extends Model<Permission> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 上级ID
*/
private Long pid;
......
private String description;
@Override
public String toString() {
return "Permission{" +
", id=" + id +
", pid=" + pid +
", title=" + title +
", type=" + type +
", state=" + state +
", sort=" + sort +
", url=" + url +
", permCode=" + permCode +
", icon=" + icon +
", description=" + description +
"}";
}
}
总结
Generate部分总体来讲思路比价简单。麻烦的部分在于如何去对各个部拆分。最后简单画出了怎个生成框架的类图结构如下: