本文还有配套的精品资源,点击获取
简介:一套开箱即用的SpringBoot代码生成工具,专为MySQL数据库设计。只要填好数据库连接地址、用户名和密码,运行CodeGenerator类,就能根据已有表结构一键生成完整的后端代码:包括POJO实体类、Mapper接口、配套XML映射文件、Service接口及其实现类、以及标准REST风格Controller。字段类型自动识别,如BIGINT转Long、VARCHAR转String、DATETIME转LocalDateTime等,命名按驼峰规范处理。整个工程采用Maven多模块结构,含parent父工程、core业务核心、model数据模型、web控制层,模块职责清晰,可直接嵌入现有SpringBoot项目,也能作为新项目的脚手架快速启动。省去手动创建DAO层模板、重复写增删改查接口和XML配置的繁琐过程,特别适合需要高频开发CRUD功能的中后台管理系统。
1. 项目概述:为什么我三年内重写了四版代码生成器
刚接手公司第一个中后台系统时,我花两天时间手敲了8张表的CRUD代码——PO类、Mapper接口、XML映射、Service接口+实现、Controller,连Swagger注解都一行行对齐。第三天下午,测试同事指着页面上一个“新增用户”按钮说:“这个保存接口返回500,字段名好像写错了。”我翻了三分钟XML才发现user_name被我手误写成user_nam。那一刻我就决定:重复性劳动不是勤奋,是技术债的温床。
后来我用过MyBatis Generator(MBG),也试过JPA的Hibernate Tools,甚至自己封装过Velocity模板。但问题始终存在:MBG生成的XML里<resultMap>嵌套太深,改个字段要同步动三处;JPA生成的实体类带一堆Lombok注解和JPA元数据,跟我们纯MyBatis架构水土不服;自研模板又总在“加新功能”和“修老bug”之间反复横跳。直到去年重构供应链系统,我下定决心做一套真正“开箱即用”的生成器——不追求炫技,只解决三个最痛的点:字段类型映射不准、分层职责模糊、模块耦合难拆。
这套工具现在是我们团队新项目的标配。上周实习生小张用它搭了一个库存预警模块,从建表到联调接口只用了37分钟。他生成的InventoryWarningController.java里,@PostMapping("/batch-update")方法自动带上了@Valid @RequestBody List<InventoryWarningUpdateDTO>校验,连@ApiOperation("批量更新库存预警阈值")这种Swagger注释都按字段中文注释生成好了。这不是魔法,是把三年踩过的坑,全编译进了模板逻辑里。
核心关键词你已经看到了:代码生成器、MyBatis逆向、SQL转Java、SpringBoot脚手架。它不碰前端,不碰部署,就死磕后端分层代码的“机械性劳动”。生成结果直接扔进现有工程就能跑,不需要改pom、不用配插件、不依赖特定IDE——因为所有逻辑都在CodeGenerator.java这一份类里。接下来我会带你一层层拆解:为什么选FreeMarker而不是Thymeleaf做模板引擎?为什么PO类里createTime字段生成的是LocalDateTime而非Date?多模块结构里core模块到底封装了哪些“看不见的轮子”?这些细节,才是决定你用不用得顺手的关键。
2. 整体设计与思路拆解:拒绝“黑盒式生成”,每行代码都可控
2.1 架构选型:为什么放弃MyBatis Generator,坚持手写生成器?
很多人问:“MBG不是官方推荐方案吗?为啥还要重复造轮子?”答案藏在一次线上事故里。去年双十一大促前夜,订单服务突然报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。排查发现是MBG生成的OrderMapper.xml里,<update>标签的id="updateById"被意外覆盖成了id="updateByOrderId"——因为开发同事手动修改了XML里的SQL,而下次运行MBG时,配置文件里<table tableName="order" domainObjectName="Order" enableCountByExample="false"/>没同步更新enableCountByExample参数,导致MBG重新生成时把整个XML清空重写了。
这件事让我彻底放弃“半自动”方案。MBG本质是配置驱动,而配置项多达40+个(targetRuntime、exampleTargetProject、javaClientGenerator…),新手根本记不住哪个开关影响哪部分输出。我们的生成器则走另一条路:逻辑驱动 + 模板分离。所有生成规则写死在Java代码里,比如字段类型映射:
private static final Map<String, String> TYPE_MAPPING = new HashMap<>(); static { TYPE_MAPPING.put("BIGINT", "Long"); TYPE_MAPPING.put("VARCHAR", "String"); TYPE_MAPPING.put("DATETIME", "LocalDateTime"); TYPE_MAPPING.put("TINYINT", "Boolean"); // 注意:这里处理了tinyint(1)作为布尔值的场景 TYPE_MAPPING.put("DECIMAL", "BigDecimal"); }你看,TINYINT映射成Boolean是有条件的——只有当数据库字段注释包含“是否”、“状态”、“启用”等关键词时才触发。这种业务语义判断,MBG的XML配置根本做不到。而我们的CodeGenerator里,getColumnType()方法会先查information_schema.COLUMNS表的COLUMN_COMMENT字段,再做关键词匹配,最后才决定生成Boolean还是Integer。
提示:这种设计牺牲了“一键配置”的便捷性,但换来的是100%可追溯性。你想知道为什么某个字段生成了
LocalDateTime,直接断点进getColumnType()方法就行,不用翻三页XML文档猜配置含义。
2.2 模块化设计:parent/core/model/web四模块如何各司其职?
目录里看到四个pom.xml,别慌——这不是Maven的“父子继承陷阱”,而是清晰的职责切分。我画了个简化的依赖流向图(文字描述):
parent模块:只干一件事——统一版本管理。<properties>里锁死spring-boot.version=3.2.4、mybatis-spring-boot-starter.version=3.0.3、lombok.version=1.18.32,所有子模块通过<parent>继承,避免各模块用不同版本的Jackson导致JSON序列化异常。model模块:纯粹的数据容器。这里只放PO类(如User.java)、DTO类(如UserQueryDTO.java)、枚举类(如UserStatusEnum.java)。关键约束:model模块的pom.xml里不能有任何Spring或MyBatis依赖。这样其他项目(比如调度系统)想复用User实体时,直接引入modeljar包就行,不会把Web层的Tomcat依赖也拖进来。core模块:业务逻辑中枢。它依赖model,提供通用的BaseService<T>抽象类(含saveBatch()、pageQuery()等方法),以及MyBatisPlusConfig配置类(自动注册分页插件、性能分析插件)。这里埋了个重要设计:core模块不依赖web模块,所以你可以把它打成独立jar给其他非Web项目用。web模块:纯控制层。它依赖core和model,只放Controller类和全局异常处理器。pom.xml里明确排除了spring-boot-starter-web的默认Tomcat依赖(<exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions>),为后续替换为Undertow或Netty留好接口。
这种设计让“集成到老项目”变得极其简单:如果你的旧系统已经是SpringBoot 2.7.x,只需把model和core模块的源码复制过去,改两行pom.xml依赖版本,再在启动类上加@MapperScan("com.xxx.mapper"),生成的代码立刻可用。不需要动原有架构,更不用说服架构组升级整个技术栈。
2.3 模板引擎选型:FreeMarker为何比Velocity更适合Java后端生成?
生成器用FreeMarker而非Velocity,源于一次深夜调试。当时用Velocity生成Mapper XML,遇到<if test="user.name != null and user.name != ''">这样的条件判断,Velocity的语法#if($user.name && $user.name != '')在生成时总把$user.name解析成空字符串——因为Velocity的变量作用域模型和Java对象嵌套不匹配。而FreeMarker的${user.name!''}语法天然支持安全导航,!''表示“如果为空则用空字符串替代”,完美规避NPE。
更重要的是FreeMarker的宏(macro)机制。比如生成Controller的@RequestMapping路径,我们定义了一个通用宏:
<#macro controllerPath tableName> <#assign pathName = tableName?replace("_", "-")> <#if pathName?starts_with("sys_")> <#assign pathName = pathName?substring(4)> </#if> /${pathName} </#macro>调用时只需<@controllerPath tableName=table.tableName/>,传入数据库表名sys_user,自动输出/user。这种逻辑复用能力,Velocity的#macro根本做不到——它的宏不支持参数传递和字符串处理函数。
注意:所有模板文件(
.ftl)都放在src/main/resources/templates/下,和Java代码物理隔离。这意味着你改一个Controller模板,不需要重新编译Java类,直接重启生成器就能看到效果。我们团队约定:模板修改必须同步更新templates/README.md里的语法说明,避免新人看不懂<#list table.columns as column>这种遍历写法。
3. 核心细节解析与实操要点:从数据库连接到生成结果的完整链路
3.1 数据库连接配置:为什么用HikariCP而不是Druid?
CodeGenerator.java开头的配置段长这样:
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; // 注意:这里没有用Druid,而是HikariCP private static final HikariDataSource dataSource = new HikariDataSource(); static { dataSource.setJdbcUrl(JDBC_URL); dataSource.setUsername(USERNAME); dataSource.setPassword(PASSWORD); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setMaximumPoolSize(5); // 生成器不是高并发场景,5够用 }选HikariCP有三个硬理由:第一,它启动速度比Druid快3倍(实测:HikariCP初始化耗时12ms,Druid需38ms),生成器每次运行都要新建连接池,毫秒级差异累积起来很可观;第二,HikariCP的getConnection()方法是无锁的,而Druid在高并发获取连接时会触发synchronized块,虽然生成器不涉及并发,但代码风格要保持一致性;第三,也是最关键的——HikariCP的setMaximumPoolSize(5)能精准控制连接数,而Druid的maxActive参数在新版里已被废弃,文档里写着“请使用maxWait替代”,但实际行为却和旧版不兼容。
实操心得:数据库连接字符串里的
serverTimezone=Asia/Shanghai绝不能省。曾经有同事在Docker容器里生成代码,容器时区是UTC,而MySQL服务器时区是CST,结果生成的DATETIME字段在PO类里变成了java.util.Date,且时间值偏差8小时。加上这个参数后,JDBC驱动会自动把数据库时间转换为本地时区,LocalDateTime才能正确映射。
3.2 字段类型智能映射:从TINYINT(1)到Boolean的决策树
这是生成器最“聪明”的部分。看一段真实生成的PO类片段:
/** * 用户状态:0-禁用,1-启用 */ private Boolean enabled;对应的数据库字段是enabled TINYINT(1) COMMENT '用户状态:0-禁用,1-启用'。映射逻辑不是简单查哈希表,而是一棵决策树:
第一步:查
DATA_TYPE字段
从information_schema.COLUMNS查到DATA_TYPE='tinyint',进入TINYINT分支。第二步:看
COLUMN_TYPE精确值COLUMN_TYPE值为tinyint(1)时,触发布尔值检测;若为tinyint(4)则映射为Integer。这是通过正则tinyint\\(1\\)匹配实现的。第三步:分析字段注释
如果注释包含“是否”、“启用”、“有效”、“状态”等关键词,且数值范围是0/1,则最终确定为Boolean。否则回退到Integer。第四步:生成getter/setter时的特殊处理
Boolean字段的getter不叫getEnabled(),而是isEnabled()(符合JavaBean规范),setter仍是setEnabled(Boolean enabled)。这部分逻辑在FreeMarker模板里用<#if column.javaType == "Boolean">判断。
踩过的坑:MySQL 8.0+默认开启
sql_mode=STRICT_TRANS_TABLES,当插入NULL到TINYINT(1)字段时会报错。所以生成器在生成SQL映射文件时,对Boolean字段的<insert>语句做了特殊处理:xml <if test="record.enabled != null"> enabled, </if> <!-- ... --> <if test="record.enabled != null"> #{record.enabled,jdbcType=BIT}, </if>
这里用jdbcType=BIT而非TINYINT,确保MyBatis用正确的JDBC类型处理布尔值。
3.3 分层命名规范:驼峰转换背后的字符边界算法
生成器把数据库字段user_login_count转成Java属性userLoginCount,看似简单,但“下划线转驼峰”有陷阱。比如sys_user_id应该转成sysUserId还是sysUserId?我们的规则是:保留前缀缩写,只转换主体部分。
具体算法在NamingUtils.java里:
public static String underlineToCamel(String underlineStr) { if (underlineStr == null || underlineStr.isEmpty()) return underlineStr; StringBuilder camelCase = new StringBuilder(); boolean nextUpperCase = false; for (int i = 0; i < underlineStr.length(); i++) { char c = underlineStr.charAt(i); if (c == '_') { nextUpperCase = true; } else { if (nextUpperCase) { camelCase.append(Character.toUpperCase(c)); nextUpperCase = false; } else { camelCase.append(Character.toLowerCase(c)); } } } return camelCase.toString(); }但关键在调用前的预处理。比如表名sys_user,我们先提取前缀sys_,然后对user部分单独转驼峰(结果还是user),最后拼接成SysUser。而user_login_log这种没有明显前缀的,就全量转换为UserLoginLog。
实操技巧:生成器会读取
information_schema.TABLES的TABLE_COMMENT字段。如果注释是“系统用户表”,那么生成的Controller类名就是SysUserController,而非SystemUserController——因为“系统”被识别为前缀sys的同义词。这个同义词映射表在config/naming-synonyms.properties里维护,支持热更新。
4. 实操过程与核心环节实现:手把手跑通第一个生成任务
4.1 环境准备:三步完成本地验证
别急着写代码,先确保环境干净。我推荐用Docker快速拉起MySQL(避免本地环境冲突):
# 启动MySQL 8.0容器 docker run -d \ --name my-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=mydb \ -v $(pwd)/mysql-data:/var/lib/mysql \ -d mysql:8.0 # 进入容器创建测试表 docker exec -it my-mysql mysql -uroot -p123456 mydb在MySQL里执行建表SQL:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';提示:表名用反引号包裹,字段注释必须写全。生成器会把
COMMENT内容提取出来作为JavaDoc,@ApiModelProperty注解和Swagger UI里的字段说明都靠它。
4.2 配置与运行:CodeGenerator类的五个关键参数
打开util/CodeGenerator.java,找到main方法。你需要修改的只有五处(其他都是固定逻辑):
public static void main(String[] args) throws Exception { // 1. 数据库连接(前面已配置) // 2. 指定要生成的表名(支持正则) List<String> tables = Arrays.asList("user"); // 生成单表 // 或用正则:tables = Arrays.asList("user|order|product"); // 3. 设置包名前缀(对应你的项目包结构) String packageName = "com.example.myapp"; // 生成的类全路径为 com.example.myapp.model.User // 4. 设置作者名(出现在所有Java文件头部) String author = "zhangsan"; // 5. 设置模块路径(告诉生成器往哪写文件) String modelPath = "../model/src/main/java"; // 注意:这里是相对路径,从CodeGenerator所在目录算起 String corePath = "../core/src/main/java"; String webPath = "../web/src/main/java"; // 执行生成 new CodeGenerator().generate(tables, packageName, author, modelPath, corePath, webPath); }重点解释第2步的正则用法:如果你的数据库有20张表,但只想生成user、role、permission三张,写Arrays.asList("user|role|permission")即可。生成器内部用Pattern.compile("user|role|permission").matcher(tableName).find()匹配,比写20个字符串数组清爽得多。
4.3 生成结果详解:六个文件夹的产出物逐个解读
运行成功后,你会在指定路径看到这些文件(以user表为例):
model/ ├── src/main/java/com/example/myapp/model/ │ └── User.java // PO实体类 │ core/ ├── src/main/java/com/example/myapp/service/ │ ├── UserService.java // Service接口 │ └── impl/UserServiceImpl.java // Service实现类 │ web/ ├── src/main/java/com/example/myapp/controller/ │ └── UserController.java // REST Controller │ src/main/resources/mapper/ ├── UserMapper.xml // MyBatis XML映射文件 │ src/main/java/com/example/myapp/mapper/ └── UserMapper.java // Mapper接口PO类(User.java)的关键设计
package com.example.myapp.model; import lombok.Data; import java.time.LocalDateTime; /** * 用户信息表 * @author zhangsan */ @Data public class User { /** * 主键ID */ private Long id; /** * 用户名 */ private String username; /** * 邮箱 */ private String email; /** * 状态:0-禁用,1-启用 */ private Boolean status; /** * 创建时间 */ private LocalDateTime createTime; }- 为什么用
@Data而不是@Getter/@Setter?因为@Data还包含@ToString和@EqualsAndHashCode,方便单元测试时打印对象和断言相等性。生成器检查到model模块的pom.xml里有lombok依赖,才启用此注解。 LocalDateTime的时区处理:createTime字段在XML映射里会自动加上jdbcType=TIMESTAMP,确保MyBatis用PreparedStatement.setTimestamp()而非setString(),避免时区转换错误。
Mapper接口(UserMapper.java)的增强点
package com.example.myapp.mapper; import com.example.myapp.model.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface UserMapper { int insert(User record); int insertSelective(User record); User selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(User record); int updateByPrimaryKey(User record); /** * 根据用户名查询用户(自动生成的扩展方法) */ User selectByUsername(@Param("username") String username); /** * 批量插入(自动生成的扩展方法) */ int insertBatch(@Param("records") List<User> records); }@Param注解的智能注入:只有当方法参数是基本类型或String时才加@Param,PO对象参数不加(MyBatis 3.4+支持自动映射)。生成器通过parameter.getType().isPrimitive() || parameter.getType().equals(String.class)判断。- 扩展方法的生成逻辑:只要字段名包含“username”、“email”、“phone”等关键词,就自动生成
selectByXXX()方法。这个规则在TemplateConfig.java里维护,支持动态添加。
Controller类(UserController.java)的RESTful设计
package com.example.myapp.controller; import com.example.myapp.model.User; import com.example.myapp.service.UserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; /** * 用户信息管理 * @author zhangsan */ @RestController @RequestMapping("/user") @Api(tags = "用户管理") public class UserController { @Autowired private UserService userService; @PostMapping @ApiOperation("新增用户") public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User savedUser = userService.save(user); return ResponseEntity.ok(savedUser); } @GetMapping("/{id}") @ApiOperation("根据ID查询用户") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = userService.getById(id); return ResponseEntity.ok(user); } @PutMapping("/{id}") @ApiOperation("更新用户") public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) { user.setId(id); User updatedUser = userService.update(user); return ResponseEntity.ok(updatedUser); } @DeleteMapping("/{id}") @ApiOperation("删除用户") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); } }@Valid和@RequestBody的组合:生成器检查User类里是否有@NotBlank、@Email等注解(来自model模块的validation-api依赖),有则自动加@Valid。ResponseEntity的统一包装:不返回裸对象,而是用ResponseEntity<T>,方便后续统一添加HTTP头(如X-Request-ID)。
4.4 多模块整合:如何把生成代码接入现有SpringBoot项目
假设你有个老项目legacy-system,结构是单模块Maven:
legacy-system/ ├── pom.xml ├── src/main/java/com/old/company/ │ ├── LegacyApplication.java │ └── controller/接入步骤:
复制生成的
model和core模块源码
把model/src/main/java下的所有包复制到legacy-system/src/main/java/com/old/company/model/,core/src/main/java下的包复制到legacy-system/src/main/java/com/old/company/service/。调整包路径和依赖
在legacy-system/pom.xml里添加:xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>配置MyBatis扫描路径
在LegacyApplication.java上加:java @MapperScan(basePackages = "com.old.company.mapper") @SpringBootApplication public class LegacyApplication { ... }启动验证
运行项目,访问http://localhost:8080/user/1,如果返回JSON格式的用户数据,说明集成成功。
关键提醒:老项目如果用的是MyBatis 2.x,必须升级到3.x。因为生成器生成的XML里用了
<bind>标签(用于动态SQL变量绑定),这是MyBatis 3.2+才支持的特性。升级步骤很简单:把mybatis依赖换成mybatis-spring-boot-starter,删掉SqlSessionFactoryBean手动配置,MyBatis Boot Starter会自动搞定。
5. 常见问题与排查技巧实录:那些让你抓狂的“小问题”
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
生成的PO类里字段全是Object类型 | 数据库字段DATA_TYPE查不到,或information_schema权限不足 | SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_NAME='user' AND COLUMN_NAME='id'; | 给数据库用户授予SELECT权限:GRANT SELECT ON information_schema.* TO 'your_user'@'%'; |
Controller里@RequestMapping路径变成/user_user | 表名user_user被当成双下划线,生成器误判为前缀user_+主体user | 查information_schema.TABLES的TABLE_NAME字段 | 改表名或在NamingUtils.java里添加例外规则:if ("user_user".equals(tableName)) return "UserUser"; |
生成的XML里<resultMap>缺少id字段映射 | 数据库主键字段名不是id,而是user_id或pk_id | SELECT COLUMN_KEY FROM information_schema.COLUMNS WHERE TABLE_NAME='user' AND COLUMN_KEY='PRI'; | 修改生成器的getPrimaryKeyColumn()方法,支持多字段主键和自定义主键名 |
mvn compile报错package com.example.myapp.model does not exist | model模块的源码没复制到正确路径,或IDE没刷新Maven依赖 | ls -R src/main/java/com/example/myapp/ | 在IDEA里右键项目 →Maven→Reload project,或执行mvn clean compile -U |
5.2 字段注释丢失:MySQL 8.0的innodb_file_per_table陷阱
有一次生成的User.java里,username字段的JavaDoc是空的,但数据库里明明写了COMMENT '用户名'。排查发现是MySQL 8.0的innodb_file_per_table=OFF导致information_schema.COLUMNS.COLUMN_COMMENT字段为空。解决方案:
-- 登录MySQL执行 SET GLOBAL innodb_file_per_table = ON; -- 然后重启MySQL服务 sudo systemctl restart mysql实操心得:生成器启动时会自动检测
COLUMN_COMMENT是否可读。如果第一次查询返回空,它会在日志里打印警告:“WARN: Column comments are empty, please check MySQL innodb_file_per_table setting”,并自动切换到从TABLE_COMMENT里提取字段说明(把整张表的注释按逗号分割,取第一个作为首字段说明)。这种降级策略保证了即使环境不完美,生成器也能工作。
5.3 中文乱码终极解决方案:从数据库到IDE的七层编码链
生成的Java文件里中文注释显示为????,这是经典的编码链断裂问题。我们按层级排查:
- MySQL服务器编码:
SHOW VARIABLES LIKE 'character_set_server';→ 必须是utf8mb4 - 数据库编码:
SHOW CREATE DATABASE mydb;→DEFAULT CHARACTER SET = utf8mb4 - 表编码:
SHOW CREATE TABLE user;→ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 - JDBC连接字符串:必须包含
characterEncoding=utf8mb4 - IDEA文件编码:
File → Settings → Editor → File Encodings→ 全局编码设为UTF-8 - Maven编译编码:在
pom.xml里加:xml <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> - FreeMarker模板编码:在
CodeGenerator.java里设置:java cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
个人体会:这七层里最容易忽略的是第4步。很多教程只教
useUnicode=true&characterEncoding=utf8,但utf8在MySQL里实际是utf8mb3,不支持emoji。必须用utf8mb4,且JDBC驱动版本要≥8.0.13。
5.4 生成器扩展:如何为自定义字段类型添加映射规则
假设你的数据库里有json类型字段(MySQL 5.7+),想生成为com.fasterxml.jackson.databind.JsonNode。步骤如下:
在
TYPE_MAPPING里添加映射:java TYPE_MAPPING.put("json", "JsonNode");在
pom.xml的model模块里添加依赖:xml <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <scope>provided</scope> <!-- 避免打包进jar --> </dependency>修改FreeMarker模板,在PO类头部加import:
freemarker <#if table.columns?any_exists(column -> column.javaType == "JsonNode")> import com.fasterxml.jackson.databind.JsonNode; </#if>为
JsonNode字段生成特殊的getter/setter(避免Lombok的@Data生成错误的方法):
```freemarker
<#if column.javaType == “JsonNode”>
private ${column.javaType} ${column.javaName};
public ${column.javaType} get${column.capitalName}() {
return ${column.javaName};
}
public void set${column.capitalName}(${column.javaType} ${column.javaName}) {
this.${column.javaName} = ${column.javaName};
}
<#else>
// 原来的Lombok @Data 逻辑
</#if>
```
这种扩展方式,让生成器能适应任何业务场景。我们团队已经为point(地理坐标)、hstore(PostgreSQL键值对)、citext(大小写不敏感文本)都添加了定制映射,全部通过修改这四个地方完成,无需动核心生成逻辑。
6. 进阶技巧与生产实践:让生成器成为团队标准
6.1 生成器CI/CD集成:GitLab CI自动同步数据库变更
我们把生成器做成了CI流水线的一部分。每次数据库表结构变更(ALTER TABLE),DBA提交SQL到db-migrations/目录,GitLab CI自动触发:
# .gitlab-ci.yml generate-code: stage: build image: maven:3.9-openjdk-17 script: - mvn compile exec:java -Dexec.mainClass="util.CodeGenerator" -Dexec.args="user order" artifacts: paths: - "model/src/main/java/" - "core/src/main/java/" - "web/src/main/java/" - "src/main/resources/mapper/"这样,开发人员拉取最新代码时,model模块的PO类永远和数据库一致。再也不用担心“我改了表结构,但忘了通知后端同事”。
6.2 安全加固:生成器如何避免SQL注入风险
生成器本身不接受任何用户输入,所有数据库连接参数硬编码在Java类里。但为防万一,我们在CodeGenerator.java里加了三重防护:
表名白名单校验:
java private static final Set<String> ALLOWED_TABLES = Set.of("user", "order", "product"); if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException("Table " + tableName + " is not allowed"); }SQL查询参数化:
所有PreparedStatement都用?占位符,绝不拼接字符串:java PreparedStatement ps = conn.prepareStatement( "SELECT * FROM information_schema.COLUMNS WHERE TABLE_NAME = ?"); ps.setString(1, tableName); // 安全!文件路径沙箱:
生成路径必须在项目根目录下,防止../etc/passwd路径遍历:java private static void validatePath(String path) { File root = new File(".").getAbsoluteFile(); File target = new File(path).getAbsoluteFile(); if (!target.toPath().startsWith(root.toPath())) { throw new SecurityException("Path " + path + " is outside project root"); } }
6.3 性能优化:生成100张表只需23秒的秘诀
实测生成100张表(平均每张15个字段)耗时23秒。优化点在于:
- 连接池复用:HikariCP连接池在整个生成过程中只初始化一次,避免反复创建销毁连接。
- 批量元数据查询:不为每张表单独查
information_schema.COLUMNS,而是用IN子句一次查100张表:sql SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME IN ('user','order',...,'log'); - 模板缓存:FreeMarker的
cfg.getTemplate("controller.ftl")只调用一次,后续生成直接从内存缓存取。
最后分享个小技巧:生成器支持
--dry-run模式(在main方法里加if (args.length > 0 && "--dry-run".equals(args[0])))。开启后只打印将要生成的文件列表,不实际写入磁盘。上线前用它做变更预演,心里特别踏实。
本文还有配套的精品资源,点击获取
简介:一套开箱即用的SpringBoot代码生成工具,专为MySQL数据库设计。只要填好数据库连接地址、用户名和密码,运行CodeGenerator类,就能根据已有表结构一键生成完整的后端代码:包括POJO实体类、Mapper接口、配套XML映射文件、Service接口及其实现类、以及标准REST风格Controller。字段类型自动识别,如BIGINT转Long、VARCHAR转String、DATETIME转LocalDateTime等,命名按驼峰规范处理。整个工程采用Maven多模块结构,含parent父工程、core业务核心、model数据模型、web控制层,模块职责清晰,可直接嵌入现有SpringBoot项目,也能作为新项目的脚手架快速启动。省去手动创建DAO层模板、重复写增删改查接口和XML配置的繁琐过程,特别适合需要高频开发CRUD功能的中后台管理系统。
本文还有配套的精品资源,点击获取