☰
MyBatis Generator进阶:generatorConfig.xml一键生成Service和Controller
2026/10/15 5:58:36 网站建设 项目流程

前阵子帮一个团队落地 MyBatis 代码生成链路,发现一个非常普遍的情况:generatorConfig.xml只生成了 Model 和 Mapper,Service、ServiceImpl、Controller 还是靠人肉复制粘贴。表一多,一个模块十几张表,每张表都要手写一遍 Service 接口、实现类、Controller,写到后面全是体力活,还容易漏字段、漏注解。

这篇文章就把一件事讲透:如何通过 MyBatis Generator 的generatorConfig.xml,配合自定义生成器,把 Controller、Service、ServiceImpl 也一并生成出来,产出的代码可以直接丢进 Spring Boot 项目里跑起来。整套方案的核心不复杂,关键在javaClientGenerator这个标签上做文章。读完你可以直接照着抄,适用于项目初始化、新模块搭建、甚至老项目补代码,非常实用。

1. 为什么 MyBatis Generator 默认不生成 Controller 和 Service

先说结论:MyBatis Generator(以下简称 MBG)默认只做三件事:生成实体类、生成 Mapper 接口、生成 Mapper XML 映射文件。这是它的定位决定的,它把自己限制在“数据访问层”这个边界内,Service 和 Controller 属于业务层和接口层,它默认不碰。

但这不代表它做不到。MBG 留了两个扩展口:一个是plugin,一个是自定义javaClientGenerator。很多人在网上搜到的是用 plugin 去改 Mapper 接口或者实体注释,但要让 generatorConfig.xml 一次性产出 Service、ServiceImpl、Controller,更干净的做法是自定义javaClientGenerator。这样做的原因有三个:

  • javaClientGenerator本身负责生成 Mapper 接口,它内部维护了一个CompilationUnit列表,我们可以在这个列表里追加 Service、ServiceImpl、Controller 的编译单元,MBG 会统一把它们写入目标目录,不需要额外写文件输出逻辑;
  • 自定义生成器可以拿到完整的表结构信息,包括列名、字段类型、主键、备注,生成代码时可以按需引用;
  • 整个过程完全由 generatorConfig.xml 驱动,配置一次,团队其他人用同一个配置执行就能生成同样的代码,规范可控。

如果把“生成代码”这件事比作一条流水线,MBG 默认只装配了 Model 和 Mapper 两个工位。我们要做的,就是在“客户端生成器”这个工位后面再接上三个新工位:Service 接口、Service 实现、Controller。接下来我会按环境准备、配置拆解、核心编码、执行验证、问题排查这个顺序完整走一遍。

2. 环境准备与 generatorConfig.xml 核心配置

2.1 依赖版本与项目结构

先明确一下版本建议。MyBatis Generator 有两个大版本线:1.3.x 和 1.4.x。1.4.x 调整了部分内部 API,网上不少老文章中的类名和方法在 1.4.x 里已经变了。我这里以1.3.7为例,这是目前兼容面最广的版本,配合 Spring Boot 2.x、MyBatis 3.5.x 都很稳。

如果是 Maven 项目,建议在pom.xml里引入生成器插件,并把数据库驱动也挂在插件依赖下。这样团队其他人不需要额外安装任何东西,一条命令就能执行生成:

<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies> </plugin>

这里有个容易忽略的细节:overwrite建议在联调阶段设为true,方便反复生成验证。一旦代码正式开始开发,里面有了人工调整,overwrite最好改回false,避免生成器把手工修改的代码冲掉。

2.2 generatorConfig.xml 标签逐项拆解

一个典型的 generatorConfig.xml 长这样,我加了注释方便对照:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 数据库驱动路径,用 Maven 插件时不是必须的 --> <classPathEntry location="/path/to/mysql-connector-java.jar"/> <context id="mysqlContext" targetRuntime="MyBatis3" defaultModelType="flat"> <!-- 注释生成器:决定生成代码里的注释长什么样 --> <commentGenerator type="com.example.generator.MyCommentGenerator"> <property name="suppressAllComments" value="false"/> <property name="addRemarkComments" value="true"/> </commentGenerator> <!-- 数据库连接 --> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/demo_db?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai" userId="root" password="123456"> </jdbcConnection> <!-- 类型转换器:控制数据库类型到 Java 类型的映射 --> <javaTypeResolver> <property name="useJSR310Types" value="true"/> </javaTypeResolver> <!-- 实体类生成位置 --> <javaModelGenerator targetPackage="com.example.entity" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- Mapper XML 生成位置 --> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!-- 核心重点:自定义 Java 客户端生成器,不只是生成 Mapper --> <javaClientGenerator type="com.example.generator.MyJavaClientGenerator" targetPackage="com.example.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 表配置,% 会匹配所有表 --> <table tableName="%"> <generatedKey column="id" sqlStatement="JDBC"/> <columnOverride column="create_time" javaType="java.time.LocalDateTime"/> <columnOverride column="update_time" javaType="java.time.LocalDateTime"/> </table> </context> </generatorConfiguration>

逐个说下关键标签的用途。

context的targetRuntime有MyBatis3、MyBatis3Simple等选项。MyBatis3会生成带 Example 的一整套方法,代码量大,适合复杂查询;MyBatis3Simple只生成基本的 CRUD,更精简。我默认用MyBatis3,因为 Service 层偶尔需要拼接条件查询,Example 能用得上。

javaTypeResolver里的useJSR310Types设置为true后,数据库的date、time、datetime会映射成LocalDate、LocalTime、LocalDateTime,而不是老旧的java.util.Date。现代项目基本都会开这个。

table tableName="%"表示匹配库里所有表。如果只要部分表,可以写t_user、t_order。generatedKey表示插入数据时回填自增主键,主键列名是id时这样写就行。

javaClientGenerator的type属性我会写成自己的全限定类名,这个类继承 MBG 的AbstractJavaClientGenerator,在它的getCompilationUnits方法里追加 Service、ServiceImpl、Controller 的编译单元。这一步是整个方案的核心。

2.3 配置速查表

为了让你以后调试时有据可查,我把高频配置项整理成了表格:

配置位置关键属性作用建议
contexttargetRuntime="MyBatis3"决定生成方法集想控制生成量可换MyBatis3Simple
contextdefaultModelType="flat"实体类是否拆分为多文件单表用flat最简单
commentGeneratortype自定义注释生成器不写的话默认注释很啰嗦
javaTypeResolveruseJSR310Types日期类型映射新项目建议true
javaClientGeneratortype自定义客户端生成器本章核心,后面重点讲
tabletableName="%"匹配哪些表可精确指定,也可全量匹配
tabledomainObjectName指定实体类名支持%通配符,如User%
generatedKeycolumn="id"自增主键回填有自增主键就配上

3. 自定义生成器:Controller、Service、ServiceImpl 一次到位

3.1 第一个自定义类:让注释和代码风格像人手写的

MBG 默认生成的注释非常模板化,看一眼就知道是自动生成的,而且中文备注经常丢失。所以我先写一个MyCommentGenerator,作用有两个:去掉那些没营养的方法注释;同时保留数据库字段备注,转到实体字段上。

实现思路是继承DefaultCommentGenerator,重写几个核心方法:

package com.example.generator; import org.mybatis.generator.api.CommentGenerator; import org.mybatis.generator.api.IntrospectedColumn; import org.mybatis.generator.api.IntrospectedTable; import org.mybatis.generator.api.dom.java.*; import org.mybatis.generator.api.dom.xml.XmlElement; import org.mybatis.generator.config.PropertyRegistry; import org.mybatis.generator.internal.DefaultCommentGenerator; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Properties; public class MyCommentGenerator extends DefaultCommentGenerator { private boolean suppressAllComments = false; private boolean addRemarkComments = true; @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); suppressAllComments = Boolean.parseBoolean(properties.getProperty("suppressAllComments")); addRemarkComments = Boolean.parseBoolean(properties.getProperty("addRemarkComments")); } @Override public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { if (suppressAllComments) { return; } // 数据库备注写到字段上面 String remarks = introspectedColumn.getRemarks(); if (addRemarkComments && remarks != null && !"".equals(remarks.trim())) { field.addJavaDocLine("/**"); field.addJavaDocLine(" * " + remarks); field.addJavaDocLine(" */"); } } @Override public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) { if (suppressAllComments) { return; } // 表名和实体名,顺手留下生成时间 topLevelClass.addJavaDocLine("/**"); topLevelClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().getRemark()); topLevelClass.addJavaDocLine(" * @author generator"); topLevelClass.addJavaDocLine(" */"); } @Override public void addGeneralMethodComment(Method method, IntrospectedTable introspectedTable) { // 去掉方法上那一堆默认注释,保持代码清爽 } }

这里有个经验:默认注释生成器会在 Mapper 接口每个方法上生成一大段英文注释,看着很占地方。重写addGeneralMethodComment让它什么都不做,代码会干净很多。

3.2 第二个自定义类:在 javaClientGenerator 中追加三个编译单元

现在写核心类MyJavaClientGenerator。它继承AbstractJavaClientGenerator,这个类的构造器接收一个布尔值,表示是否还需要生成对应的 XML Mapper 文件。我们需要,所以传true。

该类里最重要的方法是getCompilationUnits()。我们在这里先调用 MBG 自带的MapperInterfaceGenerator生成标准 Mapper 接口,再依次构建 Service 接口、ServiceImpl、Controller 三个编译单元,全部放进同一个列表返回。MBG 会在生成阶段统一处理这些文件。

package com.example.generator; import java.util.ArrayList; import java.util.List; import org.mybatis.generator.api.CompilationUnit; import org.mybatis.generator.api.dom.java.*; import org.mybatis.generator.codegen.AbstractJavaClientGenerator; import org.mybatis.generator.codegen.XmlDocumentGenerator; import org.mybatis.generator.codegen.mybatis3.javamapper.MapperInterfaceGenerator; import org.mybatis.generator.codegen.mybatis3.xmlmapper.XmlMapperGenerator; public class MyJavaClientGenerator extends AbstractJavaClientGenerator { public MyJavaClientGenerator() { super(true); } @Override public List<CompilationUnit> getCompilationUnits() { List<CompilationUnit> answer = new ArrayList<>(); // 1. 保持原生能力:生成 Mapper 接口 MapperInterfaceGenerator mapperInterfaceGenerator = new MapperInterfaceGenerator(); mapperInterfaceGenerator.setContext(context); mapperInterfaceGenerator.setIntrospectedTable(introspectedTable); answer.add(mapperInterfaceGenerator.getCompilationUnit()); // 2. 生成 Service 接口 answer.add(buildServiceInterface()); // 3. 生成 Service 实现类 answer.add(buildServiceImpl()); // 4. 生成 Controller answer.add(buildController()); return answer; } @Override public XmlDocumentGenerator getXmlMapperGenerator() { return new XmlMapperGenerator(); } // 三个构建方法分别写在下面 3.3 节 private CompilationUnit buildServiceInterface() { return null; } private CompilationUnit buildServiceImpl() { return null; } private CompilationUnit buildController() { return null; } }

有几个 API 细节需要特别说明。MapperInterfaceGenerator是 MBG 1.3.7 中现成的类,不用我们自己写 Mapper 接口的生成逻辑,直接构造并设置上下文即可。XmlMapperGenerator负责生成 Mapper XML,也是原生类。也就是说,我们没有破坏 MBG 的任何默认行为,只是在同一输出批次里多追加了几份编译单元。

3.3 核心构建方法详解

下面逐个讲清楚 Service 接口、ServiceImpl、Controller 是怎么用 MBG 的 DOM 类拼出来的。MBG 里操作 Java 代码使用的是org.mybatis.generator.api.dom.java包下的类,大家可以把它们理解成 Java 代码的结构化模型:Interface代表接口,TopLevelClass代表类,Method代表方法,Field代表成员变量,Parameter代表方法参数。我们只要拼这些对象,最后 MBG 会自动渲染成.java文件。

先获取一些通用信息。假设数据库有张表t_user,MBG 默认得到的domainObjectName是TUser,但我在<table>里用domainObjectName="%Entity"可以强制加后缀。实体包名从context的 model 配置里拿取,这里我假设是com.example.entity。那么:

  • Service 接口包名:com.example.service
  • Service 实现包名:com.example.service.impl
  • Controller 包名:com.example.controller

生成 Service 接口的代码:

private CompilationUnit buildServiceInterface() { String domainName = introspectedTable.getFullyQualifiedTable().getDomainObjectName(); FullyQualifiedJavaType serviceType = new FullyQualifiedJavaType("com.example.service." + domainName + "Service"); FullyQualifiedJavaType entityType = new FullyQualifiedJavaType("com.example.entity." + domainName); FullyQualifiedJavaType idType = new FullyQualifiedJavaType("java.lang.Integer"); Interface serviceInterface = new Interface(serviceType); serviceInterface.setVisibility(JavaVisibility.PUBLIC); serviceInterface.addImportedType(entityType); // 列表查询 Method listMethod = new Method("list"); listMethod.setVisibility(JavaVisibility.PUBLIC); listMethod.setReturnType(new FullyQualifiedJavaType("java.util.List")); listMethod.setReturnType(new FullyQualifiedJavaType("java.util.List<" + domainName + ">")); listMethod.addParameter(new Parameter(entityType, "query")); serviceInterface.addMethod(listMethod); // 按主键查询 Method getMethod = new Method("getById"); getMethod.setVisibility(JavaVisibility.PUBLIC); getMethod.setReturnType(entityType); getMethod.addParameter(new Parameter(idType, "id")); serviceInterface.addMethod(getMethod); // 新增 Method insertMethod = new Method("insert"); insertMethod.setVisibility(JavaVisibility.PUBLIC); insertMethod.setReturnType(intType()); insertMethod.addParameter(new Parameter(entityType, "record")); serviceInterface.addMethod(insertMethod); // 更新 Method updateMethod = new Method("update"); updateMethod.setVisibility(JavaVisibility.PUBLIC); updateMethod.setReturnType(intType()); updateMethod.addParameter(new Parameter(entityType, "record")); serviceInterface.addMethod(updateMethod); // 删除 Method deleteMethod = new Method("deleteById"); deleteMethod.setVisibility(JavaVisibility.PUBLIC); deleteMethod.setReturnType(intType()); deleteMethod.addParameter(new Parameter(idType, "id")); serviceInterface.addMethod(deleteMethod); return serviceInterface; } private FullyQualifiedJavaType intType() { return new FullyQualifiedJavaType("int"); }

这里我是按“通用 CRUD”的维度设计的。如果你的业务需要分页查询,可以在list方法里多加两个参数或者在 Service 接口里塞一个PageParam。实际项目中,我见过很多团队会把分页对象、统一返回对象也生成进去,这部分可以后续在生成器里扩展。

生成 ServiceImpl 的代码更复杂一点,因为方法要有方法体。MBG 的Method支持addBodyLine,把代码字符串一行一行加进去。为了可读性,我抽了一个私有方法:

private CompilationUnit buildServiceImpl() { String domainName = introspectedTable.getFullyQualifiedTable().getDomainObjectName(); FullyQualifiedJavaType mapperType = new FullyQualifiedJavaType("com.example.mapper." + domainName + "Mapper"); FullyQualifiedJavaType serviceType = new FullyQualifiedJavaType("com.example.service." + domainName + "Service"); FullyQualifiedJavaType implType = new FullyQualifiedJavaType("com.example.service.impl." + domainName + "ServiceImpl"); FullyQualifiedJavaType entityType = new FullyQualifiedJavaType("com.example.entity." + domainName); FullyQualifiedJavaType idType = new FullyQualifiedJavaType("java.lang.Integer"); TopLevelClass implClass = new TopLevelClass(implType); implClass.setVisibility(JavaVisibility.PUBLIC); implClass.addImportedType(serviceType); implClass.addImportedType(entityType); implClass.addImportedType(mapperType); implClass.addImportedType(new FullyQualifiedJavaType("org.springframework.stereotype.Service")); implClass.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired")); implClass.addAnnotation("@Service"); implClass.addSuperInterface(serviceType); // Mapper 字段注入 Field mapperField = new Field("mapper", mapperType); mapperField.setVisibility(JavaVisibility.PRIVATE); mapperField.addAnnotation("@Autowired"); implClass.addField(mapperField); // list 方法实现 Method listMethod = new Method("list"); listMethod.setVisibility(JavaVisibility.PUBLIC); listMethod.setReturnType(new FullyQualifiedJavaType("java.util.List")); listMethod.setReturnType(new FullyQualifiedJavaType("java.util.List<" + domainName + ">")); listMethod.addParameter(new Parameter(entityType, "query")); listMethod.addBodyLine("return mapper.selectByExample(query == null ? null : new " + domainName + "Example());"); implClass.addMethod(listMethod); // getById Method getMethod = new Method("getById"); getMethod.setVisibility(JavaVisibility.PUBLIC); getMethod.setReturnType(entityType); getMethod.addParameter(new Parameter(idType, "id")); getMethod.addBodyLine("return mapper.selectByPrimaryKey(id);"); implClass.addMethod(getMethod); // insert Method insertMethod = new Method("insert"); insertMethod.setVisibility(JavaVisibility.PUBLIC); insertMethod.setReturnType(intType()); insertMethod.addParameter(new Parameter(entityType, "record")); insertMethod.addBodyLine("return mapper.insertSelective(record);"); implClass.addMethod(insertMethod); // update Method updateMethod = new Method("update"); updateMethod.setVisibility(JavaVisibility.PUBLIC); updateMethod.setReturnType(intType()); updateMethod.addParameter(new Parameter(entityType, "record")); updateMethod.addBodyLine("return mapper.updateByPrimaryKeySelective(record);"); implClass.addMethod(updateMethod); // deleteById Method deleteMethod = new Method("deleteById"); deleteMethod.setVisibility(JavaVisibility.PUBLIC); deleteMethod.setReturnType(intType()); deleteMethod.addParameter(new Parameter(idType, "id")); deleteMethod.addBodyLine("return mapper.deleteByPrimaryKey(id);"); implClass.addMethod(deleteMethod); return implClass; }

这里有几个编码层面的细节值得展开说。

第一,list方法的实现我用了query == null ? null : new 实体Example()。因为 Service 层通常要支持条件查询,这里把请求参数丢进 Example 构造,代码能跑,但查询条件还需要后续人工补。更高级的做法是从 query 对象的字段反射生成 Example 条件,那个复杂度高不少,一般团队很少用。

第二,新增和更新分别用了insertSelective和updateByPrimaryKeySelective,而不是insert和updateByPrimaryKey。两者的区别是:Selective方法只操作非 null 字段,可以避免把 null 覆盖到数据库现有值上,实际业务中更安全。

第三,如果表没有主键,selectByPrimaryKey、updateByPrimaryKeySelective、deleteByPrimaryKey这三个方法在 Mapper 里是不存在的。生成前最好判断一下introspectedTable.hasPrimaryKeyColumns(),没有主键就跳过这些方法。为了避免代码报错,可以在生成器里加一行判断,我在第五节会给出排查提醒。

Controller 相对简单,但要把注解拼对。生成逻辑如下:

private CompilationUnit buildController() { String domainName = introspectedTable.getFullyQualifiedTable().getDomainObjectName(); String lowerDomain = lowerFirst(domainName); FullyQualifiedJavaType controllerType = new FullyQualifiedJavaType("com.example.controller." + domainName + "Controller"); FullyQualifiedJavaType serviceType = new FullyQualifiedJavaType("com.example.service." + domainName + "Service"); FullyQualifiedJavaType entityType = new FullyQualifiedJavaType("com.example.entity." + domainName); FullyQualifiedJavaType idType = new FullyQualifiedJavaType("java.lang.Integer"); TopLevelClass controller = new TopLevelClass(controllerType); controller.setVisibility(JavaVisibility.PUBLIC); controller.addImportedType(serviceType); controller.addImportedType(entityType); controller.addImportedType(idType); controller.addImportedType(new FullyQualifiedJavaType("org.springframework.web.bind.annotation.*")); controller.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired")); controller.addImportedType(new FullyQualifiedJavaType("org.springframework.http.ResponseEntity")); controller.addAnnotation("@RestController"); controller.addAnnotation("@RequestMapping(\"/" + lowerDomain + "\")"); Field serviceField = new Field("service", serviceType); serviceField.setVisibility(JavaVisibility.PRIVATE); serviceField.addAnnotation("@Autowired"); controller.addField(serviceField); // GET /{id} Method getById = new Method("getById"); getById.setVisibility(JavaVisibility.PUBLIC); getById.setReturnType(new FullyQualifiedJavaType("ResponseEntity<?>")); getById.addAnnotation("@GetMapping(\"/{id}\")"); getById.addParameter(new Parameter(idType, "id")); getById.addBodyLine("return ResponseEntity.ok(service.getById(id));"); controller.addMethod(getById); // GET 列表 Method list = new Method("list"); list.setVisibility(JavaVisibility.PUBLIC); list.setReturnType(new FullyQualifiedJavaType("ResponseEntity<?>")); list.addAnnotation("@GetMapping"); list.addParameter(new Parameter(entityType, "query")); list.addBodyLine("return ResponseEntity.ok(service.list(query));"); controller.addMethod(list); // POST 新增 Method insert = new Method("insert"); insert.setVisibility(JavaVisibility.PUBLIC); insert.setReturnType(new FullyQualifiedJavaType("ResponseEntity<?>")); insert.addAnnotation("@PostMapping"); insert.addParameter(new Parameter(entityType, "record")); insert.addBodyLine("return ResponseEntity.ok(service.insert(record));"); controller.addMethod(insert); // PUT 更新 Method update = new Method("update"); update.setVisibility(JavaVisibility.PUBLIC); update.setReturnType(new FullyQualifiedJavaType("ResponseEntity<?>")); update.addAnnotation("@PutMapping"); update.addParameter(new Parameter(entityType, "record")); update.addBodyLine("return ResponseEntity.ok(service.update(record));"); controller.addMethod(update); // DELETE /{id} Method delete = new Method("deleteById"); delete.setVisibility(JavaVisibility.PUBLIC); delete.setReturnType(new FullyQualifiedJavaType("ResponseEntity<?>")); delete.addAnnotation("@DeleteMapping(\"/{id}\")"); delete.addParameter(new Parameter(idType, "id")); delete.addBodyLine("return ResponseEntity.ok(service.deleteById(id));"); controller.addMethod(delete); return controller; } private String lowerFirst(String name) { if (name == null || name.isEmpty()) { return name; } return Character.toLowerCase(name.charAt(0)) + name.substring(1); }

这里我用了ResponseEntity<?>作为统一返回类型,好处是不依赖团队里专门封装的 Result 类,生成的代码拿过来就能跑。如果你们项目里已经有统一返回体,方法直接改成Result<实体>,同时把包名 import 加到addImportedType里即可。

还有一个实操建议:生成 Controller 时,列表查询参数不要直接用实体类接收,因为实际接口大概率需要分页参数、排序参数。我的做法是:生成一个不含任何逻辑的 Controller 脚手架,方法参数里先放实体类和基本的@RequestParam分页参数,具体接口签名后续人工微调。这样生成器代码不用过度复杂,生成出来的 Controller 也有实际参考价值。

4. 执行生成与运行验证

4.1 三种执行方式

自定义生成器写好后,重新执行生成。常用的方式有三种:

第一种是 Maven 命令,项目根目录执行:

mvn mybatis-generator:generate

第二种是在 IDEA 右侧 Maven 面板里,双击plugins -> mybatis-generator -> mybatis-generator:generate。

第三种是写一个本地 main 方法直接调用 MBG 的 API,适合需要在生成前后做额外处理的场景,比如先生成再自动格式化、自动提交 Git。大部分时候前两种就够了。

执行成功后,控制台会输出 SQL 映射文件、Java 文件生成路径。打开项目目录,应该看到类似下面的结构:

src/main/java/com/example/ ├── controller/UserController.java ├── entity/User.java ├── mapper/UserMapper.java ├── service/UserService.java └── service/impl/UserServiceImpl.java src/main/resources/mapper/UserMapper.xml

每个类的内容不再是空壳,而是能直接用的 CRUD 代码。

4.2 验证生成结果

生成完不要急着写业务,先打开几个文件检查。重点看三处:

一是实体类字段类型。比如create_time字段是否映射成了LocalDateTime,如果配了useJSR310Types还是Date,多半是 javaTypeResolver 没生效,检查 generatorConfig.xml 是否写进了正确的<javaTypeResolver>。

二是 Mapper 接口方法是否齐全。MyBatis3模式下应该有countByExample、deleteByExample、deleteByPrimaryKey、insert、insertSelective、selectByExample、selectByPrimaryKey、updateByExampleSelective、updateByPrimaryKeySelective等。如果少了,检查表是否真的是普通表,还是 MySQL 的视图,MBG 对视图的支持和表不太一样。

三是 ServiceImpl 里 Mapper 注入是否生成了@Autowired。如果漏了注解,Spring 启动时会报NoSuchBeanDefinitionException。代码生成器的逻辑很机械,漏一个字段就可能导致运行期错误,所以每次生成后最好对比看一下。

4.3 在 Spring Boot 项目中跑通

生成的代码要真正跑起来,还差最后一步:让 Spring 容器扫描到 Mapper 和生成的组件。

Mapper 接口扫描有两个方案。第一个是在启动类加@MapperScan("com.example.mapper"),这是最省事的;第二个是在 Mapper 接口上加@Mapper注解。我的建议是优先用@MapperScan,因为生成器如果没把@Mapper注解加上,也能顺利注入。

Service 和 Controller 因为带了@Service、@RestController,只要它们在 Spring Boot 的扫描路径下,启动时就会被自动加载。包的扫描路径如果不在启动类同一个包下,记得在启动类上加@ComponentScan补充。这一点经常有人踩坑,生成的代码放在了com.example.app下,启动类却扫描的是com.example,互相不在同一层,导致 404。

启动项目后,可以顺手写一个最简单的查询确认链路通不通。比如访问GET /user/{id},看看能不能正常返回 JSON。如果返回 500,优先看日志里是 Mapper 注入失败还是 SQL 执行报错,这两类问题占了九成。

5. 常见问题与排查经验速查

这套方案我已经在实际项目中跑过多次,也帮同事排查过不少问题。下面把高频问题整理成一张问题-原因-解决方案的速查表,建议直接收藏。

问题现象典型原因解决办法
执行报ClassNotFound: com.mysql.cj.jdbc.DriverMySQL 驱动没挂到 Maven 插件依赖下在 mybatis-generator-maven-plugin 的<dependencies>中加 mysql-connector-java
生成代码里表名带前缀,如TUserdomainObjectName没指定在<table>中配置domainObjectName="%Entity"或明确写成User
字段注释全是/** */空注释注释生成器生成逻辑太单一自定义 CommentGenerator,读取introspectedColumn.getRemarks()
数据库连接失败,报Communications link failure连接串缺少时区或 SSL 参数加useSSL=false&serverTimezone=Asia/Shanghai
生成的日期字段还是java.util.DateuseJSR310Types未开启在 javaTypeResolver 里设置<property name="useJSR310Types" value="true"/>
生成的 ServiceImpl 没有@Service注解自定义生成器漏了加注解在 buildServiceImpl 方法里implClass.addAnnotation("@Service")
执行后文件没有更新overwrite为 false 且文件已存在临时改成 true,或手动删除旧文件后重新生成
方法selectByPrimaryKey报红表没有主键,MBG 不生成该主键方法生成器里用hasPrimaryKeyColumns()判断后跳过
代码生成后中文乱码Maven/数据库/控制台字符集不一致连接串带characterEncoding=utf8,pom 里设置 UTF-8 编码
生成了大量 Example 类,不想用targetRuntime 为 MyBatis3改用MyBatis3Simple,只生成基础 CRUD
Controller 方法参数上没@RequestBody生成器没拼该注解在 Method 的 Parameter 构造后调用parameter.addAnnotation("@RequestBody")

除了表格里的这些,还有一个必须提醒的点:如果项目是 Spring Boot 2.7 以下版本,配合 MBG 1.3.7 一般没问题;如果项目用了 Spring Boot 3.x,Java 版本到了 17,MBG 1.3.7 生成的旧式代码也能跑,但 MyBatis 自身的版本和 spring 集成方式变化比较大,建议把 MBG 升到 1.4.2,同时把自定义生成器里涉及的类路径重新对照一下。

还有一个排查思路:遇到不明原因的生成异常,把 Maven 插件里的<verbose>true</verbose>打开,执行后控制台会输出很详细的日志,包括是哪个 IntrospectedTable 处理失败、哪一步抛了异常。这比猜原因高效得多。

6. 生成器的扩展思路与我的几个建议

6.1 从单表 CRUD 扩展到更贴合业务的生成策略

前面的生成代码是按最通用的 CRUD 设计的,但实际业务往往有额外诉求。比如:

  • 分页查询:可以在 Service 接口里引入PageHelper,或者生成一个PageQuery参数对象,把list方法改成PageResult<实体> page(PageQuery query);
  • 逻辑删除:如果表里有deleted字段,生成器可以在deleteById方法体里生成UPDATE ... SET deleted = 1 WHERE id = ?,而不是物理删除;
  • 审计字段填充:create_by、create_time这类字段,可以在 ServiceImpl 的insert方法里自动补值,生成器可以直接拼record.setCreateTime(LocalDateTime.now());
  • 统一返回体:团队如果有Result、ApiResponse这类公共类,生成器里把所有ResponseEntity<?>替换成Result<实体>即可,改两个方法签名就行。

这些扩展看起来代码量多,但只要在自定义生成器里维护好,一劳永逸,后面每张新表都能直接享受。

6.2 把生成器作为团队基础组件来管理

我建议不要把自定义生成器类放在某个业务项目的src/main/java里,每换一个项目就复制一遍。更好的方式是把MyCommentGenerator、MyJavaClientGenerator单独抽成一个公共模块,比如code-generator-support,里面统一维护配置模板和生成器代码。项目需要时,在 pom 里引一个依赖,generatorConfig.xml 里配的全限定类名都指向公共模块中的类。这样团队里任何人生成的代码风格都一样,修改生成规则时只改一个地方,所有人同步受益。

我见过一个团队把生成器抽成独立仓库后,新同事入职当天就能通过一条命令生成整套 CRUD,业务半个月交付了两个模块。这种基建投入,长期看非常划算。

6.3 一点经验收尾

最后说下我的个人体会:用 generatorConfig.xml 生成 Controller 和 Service,不是为了省掉手写代码本身,而是为了统一代码风格和减少低级错误。手写 Service 时最常见的错误是漏注解、事务忘加、方法命名不统一,生成器把这些都约束住了。真正有复杂业务逻辑的方法,还是要靠人来写,生成器只负责把“壳”搭好。

至于生成器代码本身,能复用就复用,不要过度设计。一开始只需要覆盖你最常用的 CRUD 场景,等团队业务复杂度上来了,再迭代生成逻辑。这个方案我用了很久,从没遇到因为生成器导致项目跑不起来的情况。你可以先在一个模块里试跑通,再复制到其他模块,很快就能适应这种开发节奏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询