MyBatis Generator进阶:generatorConfig.xml一键生成Service和Controller
前阵子帮一个团队落地 MyBatis 代码生成链路发现一个非常普遍的情况generatorConfig.xml只生成了 Model 和 MapperService、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 groupIdorg.mybatis.generator/groupId artifactIdmybatis-generator-maven-plugin/artifactId version1.3.7/version configuration configurationFilesrc/main/resources/generatorConfig.xml/configurationFile overwritetrue/overwrite verbosetrue/verbose /configuration dependencies dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies /plugin这里有个容易忽略的细节overwrite建议在联调阶段设为true方便反复生成验证。一旦代码正式开始开发里面有了人工调整overwrite最好改回false避免生成器把手工修改的代码冲掉。2.2 generatorConfig.xml 标签逐项拆解一个典型的 generatorConfig.xml 长这样我加了注释方便对照?xml version1.0 encodingUTF-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 idmysqlContext targetRuntimeMyBatis3 defaultModelTypeflat !-- 注释生成器决定生成代码里的注释长什么样 -- commentGenerator typecom.example.generator.MyCommentGenerator property namesuppressAllComments valuefalse/ property nameaddRemarkComments valuetrue/ /commentGenerator !-- 数据库连接 -- jdbcConnection driverClasscom.mysql.cj.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/demo_db?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghai userIdroot password123456 /jdbcConnection !-- 类型转换器控制数据库类型到 Java 类型的映射 -- javaTypeResolver property nameuseJSR310Types valuetrue/ /javaTypeResolver !-- 实体类生成位置 -- javaModelGenerator targetPackagecom.example.entity targetProjectsrc/main/java property nameenableSubPackages valuetrue/ property nametrimStrings valuetrue/ /javaModelGenerator !-- Mapper XML 生成位置 -- sqlMapGenerator targetPackagemapper targetProjectsrc/main/resources property nameenableSubPackages valuetrue/ /sqlMapGenerator !-- 核心重点自定义 Java 客户端生成器不只是生成 Mapper -- javaClientGenerator typecom.example.generator.MyJavaClientGenerator targetPackagecom.example.mapper targetProjectsrc/main/java property nameenableSubPackages valuetrue/ /javaClientGenerator !-- 表配置% 会匹配所有表 -- table tableName% generatedKey columnid sqlStatementJDBC/ columnOverride columncreate_time javaTypejava.time.LocalDateTime/ columnOverride columnupdate_time javaTypejava.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 配置速查表为了让你以后调试时有据可查我把高频配置项整理成了表格配置位置关键属性作用建议contexttargetRuntimeMyBatis3决定生成方法集想控制生成量可换MyBatis3SimplecontextdefaultModelTypeflat实体类是否拆分为多文件单表用flat最简单commentGeneratortype自定义注释生成器不写的话默认注释很啰嗦javaTypeResolveruseJSR310Types日期类型映射新项目建议truejavaClientGeneratortype自定义客户端生成器本章核心后面重点讲tabletableName%匹配哪些表可精确指定也可全量匹配tabledomainObjectName指定实体类名支持%通配符如User%generatedKeycolumnid自增主键回填有自增主键就配上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 ListCompilationUnit getCompilationUnits() { ListCompilationUnit 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_userMBG 默认得到的domainObjectName是TUser但我在table里用domainObjectName%Entity可以强制加后缀。实体包名从context的 model 配置里拿取这里我假设是com.example.entity。那么Service 接口包名com.example.serviceService 实现包名com.example.service.implController 包名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 参数加useSSLfalseserverTimezoneAsia/Shanghai生成的日期字段还是java.util.DateuseJSR310Types未开启在 javaTypeResolver 里设置property nameuseJSR310Types valuetrue/生成的 ServiceImpl 没有Service注解自定义生成器漏了加注解在 buildServiceImpl 方法里implClass.addAnnotation(Service)执行后文件没有更新overwrite为 false 且文件已存在临时改成 true或手动删除旧文件后重新生成方法selectByPrimaryKey报红表没有主键MBG 不生成该主键方法生成器里用hasPrimaryKeyColumns()判断后跳过代码生成后中文乱码Maven/数据库/控制台字符集不一致连接串带characterEncodingutf8pom 里设置 UTF-8 编码生成了大量 Example 类不想用targetRuntime 为 MyBatis3改用MyBatis3Simple只生成基础 CRUDController 方法参数上没RequestBody生成器没拼该注解在 Method 的 Parameter 构造后调用parameter.addAnnotation(RequestBody)除了表格里的这些还有一个必须提醒的点如果项目是 Spring Boot 2.7 以下版本配合 MBG 1.3.7 一般没问题如果项目用了 Spring Boot 3.xJava 版本到了 17MBG 1.3.7 生成的旧式代码也能跑但 MyBatis 自身的版本和 spring 集成方式变化比较大建议把 MBG 升到 1.4.2同时把自定义生成器里涉及的类路径重新对照一下。还有一个排查思路遇到不明原因的生成异常把 Maven 插件里的verbosetrue/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 场景等团队业务复杂度上来了再迭代生成逻辑。这个方案我用了很久从没遇到因为生成器导致项目跑不起来的情况。你可以先在一个模块里试跑通再复制到其他模块很快就能适应这种开发节奏。

相关新闻

软考 系统架构设计师历年真题集萃(65)

软考 系统架构设计师历年真题集萃(65)

接前一篇文章:软考 系统架构设计师系列知识点之杂项集萃(64) 第105题 “4+1”视图主要用于描述系统逻辑架构,最早由Philippe Kruchten于1995年提出。其中()视图用于描述对象模型,并说明系统应该为用户提供哪些服务。当采用面向对象的设计方法描述对象模型时,通常使用(…

2026/10/15 5:58:39 阅读更多 →
沐问MuAsk:开源免费的本地桌面Text2SQL工具,用自然语言查数据库

沐问MuAsk:开源免费的本地桌面Text2SQL工具,用自然语言查数据库

做本地数据分析的人应该都有过这种体验:老板丢过来一句“这个季度华东区的退货率是多少”,你翻出几十个表名,对着字段名猜了半天,好不容易写出一个JOIN,跑出来结果还不对。Text2SQL这个方向就是来解决这种问题的&#…

2026/10/15 5:58:39 阅读更多 →
微信小程序挂号系统高并发与HIS对接实战方案

微信小程序挂号系统高并发与HIS对接实战方案

简介:本资源是一份完整的本科毕业设计论文文档,面向计算机、软件工程及相关专业学生,聚焦微信小程序在医疗预约场景中的落地实践,解决传统挂号流程效率低、信息不透明等实际问题。文档涵盖系统需求分析、可行性论证、功能模块设计…

2026/10/15 5:58:39 阅读更多 →

最新新闻

Tuxedo客户端域连接配置与TPEDOMAIN错误排查指南

Tuxedo客户端域连接配置与TPEDOMAIN错误排查指南

1. 项目概述:这不是一次普通网络连通测试,而是一次面向生产环境的域身份验证闭环验证“tuxedo域连接配置与测试(客户端测试)”这个标题里藏着三个关键信号:tuxedo——不是Linux发行版Tuxedo OS,而是BEA&…

2026/10/15 9:14:01 阅读更多 →
AMD 82亿美元押注世界模型:从芯片到物理AI的生态布局

AMD 82亿美元押注世界模型:从芯片到物理AI的生态布局

1. 一笔天价收购背后的行业信号2024年夏天,一条消息在芯片圈和AI圈同时炸开:AMD宣布以约6.65亿美元现金加股票的方式收购欧洲最大的私人AI实验室Silo AI,整体交易价值约6.65亿美元。而更早之前,AMD还完成了一笔让整个行业侧目的操…

2026/10/15 9:14:01 阅读更多 →
SpringBoot+Vue企业级电商系统架构设计与实战

SpringBoot+Vue企业级电商系统架构设计与实战

1. 项目概述:企业级电商系统的技术选型与价值最近在整理一个基于SpringBootVue的企业级服装商城管理系统,这套方案在中小型电商项目中非常典型。不同于简单的Demo项目,这套架构真正考虑了高并发场景下的稳定性、前后端协作效率以及可维护性。…

2026/10/15 9:14:01 阅读更多 →
AI模型训练暂停背后:智能体安全边界与DNS防护实战

AI模型训练暂停背后:智能体安全边界与DNS防护实战

1. 从一条新闻说起:模型训练为什么会被按下暂停键前几天有个消息在圈子里传得挺开:某头部AI机构宣布暂停其最强模型的训练,原因不是算力不够,也不是资金断裂,而是训练过程中出现了一系列失控事件——用户照片被外泄、智…

2026/10/15 9:14:01 阅读更多 →
Django足球训练营管理系统开发实践

Django足球训练营管理系统开发实践

1. 项目概述这个基于Django的足球训练营管理系统,是我为一家职业足球俱乐部开发的综合性解决方案。系统主要解决三个核心问题:俱乐部日常管理、球员训练计划制定和球员能力评估。在实际使用中,这套系统已经帮助俱乐部将球员评估效率提升了60%…

2026/10/15 9:14:01 阅读更多 →
Mac本地生图实战:Stable Diffusion与MPS加速,182秒一张、10GB内存

Mac本地生图实战:Stable Diffusion与MPS加速,182秒一张、10GB内存

1. 为什么要在 Mac 上折腾本地生图先把结论摆在最前面:在 Mac 上跑本地生图,目前最现实的路径就是基于 Stable Diffusion 系列模型配合 Apple Silicon 的 MPS 加速,用 10GB 左右统一内存换一张 512512 到 768768 的图,单张耗时大概…

2026/10/15 9:12:55 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →