接上篇MyBatis-Plus 源码阅读(一)CRUD 方法自动生成原理深度剖析,本文我们讲解一下 mp 的代码生成器是如何工作的。
环境
- 核心依赖:mybatis–plus–spring-boot3-starter 3.5.14
- 基础框架:Spring Boot 3.5.6
- JDK 版本:17
- 开发工具:IntelliJ IDEA
- 模板引擎:Velocity
快速上手:3 步生成代码
在剖析源码前,我们先通过一个完整示例感受代码生成器的便捷性,为后续源码分析建立直观认知。
准备数据表
create table `order`
(
id bigint auto_increment comment \'订单主键\'
primary key,
order_no varchar(64) not null comment \'订单编号(业务唯一标识)\',
user_id bigint not null comment \'下单用户ID\',
order_status tinyint default 0 not null comment \'订单状态:0-待付款 1-待发货 2-待收货 3-已完成 4-已取消\',
total_amount decimal(10, 2) default 0.00 not null comment \'订单总金额(含所有费用)\',
pay_amount decimal(10, 2) default 0.00 not null comment \'实际支付金额\',
create_time datetime default CURRENT_TIMESTAMP not null comment \'创建时间\',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment \'更新时间\',
constraint uk_order_no
unique (order_no)
)
comment \'订单主表\';
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.14</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
编写生成器脚本并执行
按照官方文档的示例简单修改了一下。
public class MpGenerator {
public static void main(String[] args) {
FastAutoGenerator.create(\"jdbc:mysql://localhost:3306/test\",
\"root\", \"1234\")
.globalConfig(builder -> builder
.author(\"SheepHappy\")
.outputDir(Paths.get(System.getProperty(\"user.dir\")) + \"/src/main/java\")
.commentDate(\"yyyy-MM-dd\")
)
.packageConfig(builder -> builder
.parent(\"edu.neuq.mptest\")
.entity(\"entity\")
.mapper(\"mapper\")
.service(\"service\")
.serviceImpl(\"service.impl\")
.pathInfo(Collections.singletonMap(OutputFile.xml,
Paths.get(System.getProperty(\"user.dir\")) + \"/src/main/resources/mapper\"))
)
.strategyConfig(builder -> builder
.addInclude(\"order\")
.entityBuilder()
.enableLombok()
)
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}
执行脚本,发现对应的 mapper、entity 和 service 类的代码全部都生成了。
源码分析
代码生成器的源码相对来说还是比较简单的,没有过深的层级。
核心流程可以分为下面四步:
- 读元数据:连接数据库,读取目标表的结构(表名、字段名、字段类型、注释、主键等);
- 数据建模:将读取到的数据库元数据,封装成 MP 内部的 “数据模型”(比如
TableInfo、FieldInfo);
- 模板渲染:加载预设 / 自定义的代码模板(Velocity/Freemarker/Beetl),用 “数据模型” 填充模板中的占位符;
- 文件输出:将渲染后的模板内容,按指定路径和文件名写入本地文件。
第一步:读元数据 —— 从数据库提取表结构信息
核心代码主要在com.baomidou.mybatisplus.generator.query.DefaultQuery#queryTables
@Override
public @NotNull List queryTables() {
try {
boolean isInclude = !strategyConfig.getInclude().isEmpty();
boolean isExclude = !strategyConfig.getExclude().isEmpty();
List tableList = new ArrayList();
List tables = this.getTables();
List includeTableList = new ArrayList();
List excludeTableList = new ArrayList();
tables.forEach(table -> {
String tableName = table.getName();
if (StringUtils.isNotBlank(tableName)) {
TableInfo tableInfo = new TableInfo(this.configBuilder, tableName);
tableInfo.setComment(table.getRemarks());
if (isInclude && strategyConfig.matchIncludeTable(tableName)) {
includeTableList.add(tableInfo);
} else if (isExclude && strategyConfig.matchExcludeTable(tableName)) {
excludeTableList.add(tableInfo);
}
tableList.add(tableInfo);
}
});
filter(tableList, includeTableList, excludeTableList);
tableList.forEach(this::convertTableFields);
return tableList;
} finally {
databaseMetaDataWrapper.closeConnection();
}
}
查询所有表名
this.getTables() 最终调用 com.baomidou.mybatisplus.generator.jdbc.DatabaseMetaDataWrapper#getTables,通过 JDBC 标准接口 DatabaseMetaData 查询表名和表注释:
public List getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) {
List tables = new ArrayList();
try (ResultSet resultSet = databaseMetaData.getTables(catalog, schemaPattern, tableNamePattern, types)) {
Table table;
while (resultSet.next()) {
table = new Table();
table.name = resultSet.getString(\"TABLE_NAME\");
table.remarks = formatComment(resultSet.getString(\"REMARKS\"));
table.tableType = resultSet.getString(\"TABLE_TYPE\");
tables.add(table);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return tables;
}
DatabaseMetaData 是 JDBC 提供的数据库元信息查询接口,不同数据库(MySQL、Oracle)的驱动会实现该接口,因此 MP 能适配多种数据库。
表过滤
filter 方法的核心逻辑是 “排除规则优先于包含规则”:
- 若配置了
exclude:从所有表中移除符合排除规则的表;
- 若未配置
exclude 但配置了 include:仅保留符合包含规则的表;
- 若都未配置:生成数据库中所有表的代码。
拿到表字段等元信息
this::convertTableFields 是字段信息查询的核心,负责读取每个字段的详细属性并封装:
protected void convertTableFields(@NotNull TableInfo tableInfo) {
String tableName = tableInfo.getName();
Map columnsInfoMap = getColumnsInfo(tableName);
Entity entity = strategyConfig.entity();
columnsInfoMap.forEach((k, columnInfo) -> {
String columnName = columnInfo.getName();
TableField field = new TableField(this.configBuilder, columnName);
if (columnInfo.isPrimaryKey()) {
field.primaryKey(columnInfo.isAutoIncrement());
tableInfo.setHavePrimaryKey(true);
if (field.isKeyIdentityFlag() && entity.getIdType() != null) {
LOGGER.warn(\"The primary key of the current table [{}] is configured as auto-incrementing, which will override the global primary key ID type strategy.\", tableName);
}
}
field.setColumnName(columnName).setComment(columnInfo.getRemarks());
String propertyName = entity.getNameConvert().propertyNameConvert(field);
TableField.MetaInfo metaInfo = new TableField.MetaInfo(columnInfo, tableInfo);
IColumnType columnType;
ITypeConvertHandler typeConvertHandler = dataSourceConfig.getTypeConvertHandler();
if (typeConvertHandler != null) {
columnType = typeConvertHandler.convert(globalConfig, typeRegistry, metaInfo);
} else {
columnType = typeRegistry.getColumnType(metaInfo);
}
field.setPropertyName(propertyName, columnType);
field.setMetaInfo(metaInfo);
tableInfo.addField(field);
});
tableInfo.setIndexList(getIndex(tableName));
tableInfo.processTable();
}
第二步:数据建模 —— 封装为 MP 内部数据模型
在上面的 com.baomidou.mybatisplus.generator.query.DefaultQuery#convertTableFields 方法中,先将 columnInfo 转换为 tableFiled,再添加到 tableInfo 里。至此,数据库表的元信息已经全部获取到,终于可以进入核心的模板渲染环节了。
第三步:模板渲染 —— 数据模型 + 模板 = 代码字符串
模板引擎本质就是 “填坑工具” ——— 模板是写好固定结构的 “稿子”(比如后面会看到的 Entity 模板),里面留着 ${变量} 这种 “坑位”,模板引擎的作用就是把数据模型里的内容(比如表名、字段名),自动填进这些 “坑位”,最后生成完整的代码 / 文本。
MP 的模板引擎模块采用模板方法模式,核心优势是 “统一流程、灵活扩展”:
- 抽象父类
AbstractTemplateEngine:定义统一的渲染流程(batchOutput 方法),封装不变逻辑(如获取配置、构建数据模型映射);
- 具体实现类:
VelocityTemplateEngine、FreemarkerTemplateEngine、BeetlTemplateEngine,分别实现不同模板引擎的渲染细节(如占位符解析、模板加载)。

模板渲染的入口为com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine#batchOutput,负责遍历所有目标表,生成对应模块的代码:
public AbstractTemplateEngine batchOutput() {
try {
ConfigBuilder config = this.getConfigBuilder();
List tableInfoList = config.getTableInfoList();
tableInfoList.forEach(tableInfo -> {
Map objectMap = this.getObjectMap(config, tableInfo);
Optional.ofNullable(config.getInjectionConfig()).ifPresent(t -> {
t.beforeOutputFile(tableInfo, objectMap);
outputCustomFile(t.getCustomFiles(), tableInfo, objectMap);
});
outputEntity(tableInfo, objectMap);
outputMapper(tableInfo, objectMap);
outputService(tableInfo, objectMap);
outputController(tableInfo, objectMap);
});
} catch (Exception e) {
throw new RuntimeException(\"代码生成失败\", e);
}
return this;
}
以 outputEntity(tableInfo, objectMap) 方法进行深入分析,最后进入到
com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine#writer(
java.util.Map,
java.lang.String, java.io.File)

通过打断点可以发现,所有的配置信息和数据库元信息都作为 objectMap 的参数注入到模板中。

代码模板文件也很容易在外部库找到。
简单介绍一下 Velocity 语法,模板里的 #xxx 是 Velocity 的 “指令”(循环、判断),${xxx} 是 “占位符”(后续会用数据库元数据填充),其他没被 # 或 $ 包裹的内容(如 package、public class)会直接原样输出。
下面是 Entity 模板代码的简化版本
# 1. 包声明(Entity 放在哪个包下)
package ${package.Entity};
# 2. 自动导入依赖包(框架包 + Java 基础包)
#foreach($pkg in ${importEntityFrameworkPackages})
import ${pkg};
#end
#foreach($pkg in ${importEntityJavaPackages})
import ${pkg};
#end
# 3. 类注释(表说明、作者、生成日期)
# 4. 类上的注解(如 @TableName、@Data)
#foreach($an in ${entityClassAnnotations})
${an.displayName}
#end
# 5. 类定义(生成 Entity 类名)
public class ${entity} { # ${entity} 是 Entity 类名(如 User)
# 6. 循环生成所有字段(核心:数据库字段 → Java 属性)
#foreach($field in ${table.fields})
# 字段注释(有注释才生成)
#if(\"$!field.comment\" != \"\")
#end
# 字段上的注解(如 @TableId、@TableField)
#foreach($an in ${field.annotationAttributesList})
${an.displayName}
#end
# 字段定义(private + Java 类型 + 属性名)
private ${field.propertyType} ${field.propertyName}; # 如 private String userName;
#end
# 7. 生成 getter/setter 方法(非 Lombok 模式)
#foreach($field in ${table.fields})
# 布尔类型 getter 用 isXXX,其他用 getXXX
#if(${field.propertyType.equals(\"boolean\")})
#set($getprefix=\"is\")
#else
#set($getprefix=\"get\")
#end
# getter 方法
public ${field.propertyType} ${getprefix}${field.capitalName}() {
return ${field.propertyName};
}
# setter 方法
public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
this.${field.propertyName} = ${field.propertyName};
}
#end
}
第四步:文件输出
默认情况下,mp 生成代码时会检查目标文件是否存在,如果存在就不生成,避免覆盖原有文件。
自定义模板:按需定制专属代码(实操指南)
现在,我们已经基本了解 mp 代码生成的全流程。但有些时候,mp 内置的模板可能不满足我们的需求,我们也可以自定义自己的模板。
将 MP 内置的模板文件复制到项目 resources/templates 目录下,按相同目录结构存放,修改后生成器会优先加载自定义模板,而非内置模板。
例如,往模板的 class 里面添加
public static final String TABLE_NAME = \"${table.name}\";
就能在生成的 entity 文件里添加表名称这个常量。(仅作演示,mp 有提供entityColumnConstant 这个参数专门用于生成常量)
结语
MP 代码生成器的核心魅力,在于「原理简单且高度可扩展」—— 通过 JDBC 读元数据、数据建模标准化、模板引擎渲染、文件输出,四步完成重复代码生成。而自定义模板则进一步打破了 “通用代码” 的局限,让生成的代码直接贴合项目规范,真正实现 “一键生成,无需修改”。
除了 CRUD 代码生成,模板引擎技术的应用场景还有很多:比如生成接口文档、数据库脚本、配置文件等。掌握这种 “固定格式 + 动态数据” 的思想,能大幅减少重复开发工作,提升开发效率。
如果这篇文章对你理解 MP 代码生成器原理有帮助,麻烦点赞 + 关注支持一下~ 你的认可就是我持续输出源码解析系列的最大动力!️️️
源码地址
版权: 转载请注明出处:https://www.zuozi.net/3560.html
客服
关注微信公众号
客服电话
400-888-8888
客服邮箱
122325244@qq.com
手机
扫描二维码
手机访问本站
还没有评论呢,快来抢沙发~