
1. 逆向工程整体思路与方案选型1.1 什么是MyBatis逆向工程刚开始接触MyBatis的时候我最大的感受就是Mapper接口好写XML映射文件也不算难但一旦表结构变得复杂几十张表、上百个字段摆在那里手工敲实体类、手写ResultMap、逐条维护增删改查SQL工作量是一回事更折磨人的是低级错误——字段名拼错、类型对不上、下划线转驼峰没转干净跑起来一堆Bug排查还特别费劲。MyBatis逆向工程就是解决这个问题的。它的核心逻辑很简单你只要把数据库表结构准备好通过一个生成器工具自动帮你产出实体类、Mapper接口、Mapper XML映射文件以及一套基于条件构造的查询类也就是我们常说的Example。整个过程不需要你手工写一行代码你关注的是表结构设计而不是重复劳动。这几年我在实际项目里用它配合Spring做整合从数据库表到可运行的DAO层通常只需要几分钟。对于CRUD为主的业务系统这几乎是效率最高的起点。而且后续表结构变更时重新生成一遍即可配合代码合并的习惯能把维护成本压得很低。1.2 为什么选择MBG Spring这套组合市面上的生成工具其实不少有基于IDEA插件的有在线网页生成器也有MyBatis Generator以下简称MBG这种官方推荐的生成器。我的选择标准很简单可配置、可重复、可追溯。IDEA插件适合单机玩但团队协作时每个人本地环境不同生成出来的代码可能有差异而且很难落地到持续集成流程里。MBG通过一个XML配置文件描述生成规则用Maven插件或Java命令触发同一份配置文件谁执行结果都一样这个特性在团队项目里太重要了。至于为什么要和Spring整合是因为实际业务项目里几乎没有裸用MyBatis的场景。Spring负责管理数据源、事务、对象生命周期MyBatis的SqlSessionFactory、Mapper代理对象都需要交给Spring容器管理。这套组合看似简单但里面有很多细节比如MapperScannerConfigurer的扫描包路径、事务管理器配置、动态代理的机制每一样没配好都会导致启动报错或者方法无法执行。另外必须提一下现在很多人直接用Spring Boot MyBatis-Plus。PostgreSQL、MySQL都碰到过个人体会是MBG依然值得掌握因为很多老项目、银行类项目、企业级系统还在用传统的Spring MyBatis你掌握了底层原理切到任何上层框架都不慌。而且很多面试题也喜欢问逆向工程、MyBatis初始化流程、缓存机制这些在手动整合一次之后理解会深很多。2. 环境准备与配置细节2.1 建好数据库和项目骨架逆向工程的第一步不是写代码而是把数据库表准备好。这个步骤很多人会忽略导致生成出来的实体类字段缺失或者类型不对。我的建议是在动手之前先做一次表结构评审确认字段类型、长度、注释、索引是否都合理因为生成器只会忠实反映表结构它不会帮你判断字段设计得好不好。以MySQL为例建表时最好把字段注释写完整比如“用户昵称”“创建时间”这种逆向工程默认可以把注释生成到实体类的Javadoc上后面写业务代码时看注释就能明白字段含义省去翻数据库的麻烦。字段类型也要注意varchar对应Stringbigint对应Longdatetime对应Datedecimal对应BigDecimaltinyint(1)在部分版本下会映射成Boolean这些映射关系你最好心里有数。项目骨架我用的是Maven工程标准的SSM结构src/main/java放Java代码src/main/resources放配置文件。这里有个小建议逆向工程生成的代码最好单独放在一个包下比如com.example.dao或者com.example.domain不要和手写的Service、Controller混在一起。这样做的原因有两个一是重新生成时方便整体覆盖二是代码结构清晰代码审查时也容易区分哪些是自动生成的、哪些是手工维护的。2.2 generatorConfig.xml核心配置拆解MBG的配置全部集中在一个XML文件里通常命名为generatorConfig.xml。如果你第一次看这个文件可能会觉得有点复杂但其实核心就五块classPathEntry驱动、jdbcConnection数据库连接、javaModelGenerator实体类、sqlMapGeneratorXML映射文件、javaClientGeneratorMapper接口。再加上一个最重要的table配置。下面是一个我常用的最小配置你直接抄走改参数就能用?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 context idmysqlContext targetRuntimeMyBatis3Simple defaultModelTypeflat commentGenerator property namesuppressAllComments valuefalse/ property namesuppressDate valuetrue/ /commentGenerator jdbcConnection driverClasscom.mysql.cj.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/yourdb?useUnicodetrueamp;characterEncodingutf8 userIdroot passwordyourpass/ javaTypeResolver property nameforceBigDecimals valuetrue/ /javaTypeResolver javaModelGenerator targetPackagecom.example.domain targetProjectsrc/main/java property nameenableSubPackages valuefalse/ property nametrimStrings valuetrue/ /javaModelGenerator sqlMapGenerator targetPackagemapper targetProjectsrc/main/resources property nameenableSubPackages valuefalse/ /sqlMapGenerator javaClientGenerator typeXMLMAPPER targetPackagecom.example.dao targetProjectsrc/main/java property nameenableSubPackages valuefalse/ /javaClientGenerator table tableNameuser domainObjectNameUser/ /context /generatorConfiguration我说一下几个关键的配置项因为这里面的坑我几乎都踩过。第一个是targetRuntime有两种主流选择MyBatis3Simple和MyBatis3。MyBatis3Simple只生成最简单的CRUD方法没有Example相关的复杂查询代码量少很多MyBatis3则会生成完整的Example体系适合查询条件复杂的业务。我自己一般先用MyBatis3Simple快速搭基础CRUD等业务需要复杂查询时再手工扩展或者重新生成。如果你团队规范要求所有查询都走Example那就直接用MyBatis3。第二个是javaTypeResolver里的forceBigDecimals这个建议设成true。不设置的话数据库里的decimal类型在某些驱动下会映射成Double而金额相关的字段用Double会带来精度问题这是财务系统和订单系统绝对不能接受的。设成true之后decimal统一映射为BigDecimal一劳永逸。第三个是table配置里的domainObjectName建议显式指定。如果不指定MBG会根据表名自动推断比如user_profile可能生成UserProfile大多数时候没问题但遇到表名带前缀或者特殊字符时你最好自己控制生成出来的实体类名和Mapper名不然代码风格会很混乱。commentGenerator的suppressDate属性建议设为true否则生成的文件头会带上当前时间戳导致每次生成的代码文件时间不同用版本管理工具提交时会产生大量无意义的diff团队协作时容易引发冲突。还有一个很实用的配置是生成时带上注释把数据库字段注释映射到实体类属性上。这样代码里看属性就能理解业务含义省去反复查表结构的时间。前提是建表时写好了COMMENT前面也强调过这点。3. Spring与逆向工程产物整合3.1 Spring容器配置与SqlSessionFactoryBean生成出来的Mapper接口和XML文件不会自己工作需要把它们交给Spring容器管理。传统Spring项目里核心配置就两样东西一个数据源DataSource一个SqlSessionFactoryBean。SqlSessionFactoryBean是MyBatis和Spring整合的桥梁。它做了这么几件事读取MyBatis配置、解析Mapper XML、把SqlSessionFactory创建出来放进Spring容器。配置大概长这样bean iddataSource classcom.zaxxer.hikari.HikariDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property namejdbcUrl valuejdbc:mysql://localhost:3306/yourdb?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueyourpass/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.domain/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ property namelogImpl valueorg.apache.ibatis.logging.stdout.StdOutImpl/ /bean /property /bean这里有个细节容易踩坑mapperLocations的值是classpath:mapper/*.xml注意这个路径要和generatorConfig.xml里sqlMapGenerator的targetPackage对应。targetPackage设为mapperXML文件最终就会输出到src/main/resources/mapper/目录下。如果路径不匹配Spring启动时会报Mapper XML文件找不到的错。mapUnderscoreToCamelCase这个属性我建议一定打开。数据库字段通常用下划线命名比如create_time而实体类属性是驼峰命名createTime打开这个配置后MyBatis在做自动映射时会自动完成转换不用你手写一堆ResultMap。前提是生成XML时用的是默认的自动映射不要手工写死resultMap。3.2 Mapper扫描与事务管理SqlSessionFactory准备好之后还需要让Spring知道去哪里找Mapper接口。传统做法是用MapperFactoryBean一个个配置但那样太繁琐实际项目都用MapperScannerConfigurer做包扫描bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.dao/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ property nameannotationClass valueorg.springframework.stereotype.Repository/ /beanbasePackage就是Mapper接口所在的包也就是generatorConfig.xml里javaClientGenerator的targetPackage。Spring启动时扫描这个包下所有接口为每个接口生成一个动态代理对象注册到容器里。后面你在Service里直接Autowired注入Mapper接口就行了完全不用写实现类。这里有一个当年的经典坑MapperScannerConfigurer里sqlSessionFactory的引用如果你用ref属性可能会因为Spring容器加载顺序问题导致初始化失败最稳妥的方式是用sqlSessionFactoryBeanName属性传Bean的名字字符串延迟到实例化阶段再解析这样能规避很多奇怪的启动顺序问题。事务管理也不能忽视。如果整合了Spring但没有配置事务管理器那你的Service层方法即使标了Transactional实际也不会生效。配置很简单事务管理器基于数据源创建然后开启注解驱动bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/注意事务管理器依赖的dataSource必须和SqlSessionFactory用的是同一个实例否则事务边界会出现错乱尤其是多数据源场景下这个问题排查起来相当痛苦。3.3 逆向生成代码的正确使用姿势生成出来的代码长什么样很多新手第一次看到会有点懵。实体类好理解就是和表字段一一对应的POJOMapper接口里定义了一批方法XML文件里是这些方法的SQL实现Example类则是一个查询条件构造器。使用时的通用姿势是这样的在Service里注入Mapper接口直接调用内置方法。比如通过主键查详情调selectByPrimaryKey普通查询调selectByExample新增调insert或者insertSelective。insert和insertSelective的区别我特别说一下。insert会把所有字段都拼进SQL包括值为null的字段insertSelective只会把非null字段拼进去null字段使用数据库默认值。如果你的表字段有默认值比如create_time默认CURRENT_TIMESTAMP那就应该用insertSelective否则显式插入null会把默认值顶掉。这是非常高频的一个线上Bug值得记牢。再比如updateByPrimaryKey和updateByPrimaryKeySelective也是同理。前者更新全部字段后者只更新非null字段。业务里通常用Selective版本因为你不想让一个简单的更新操作把其他字段置空。4. 实操过程与核心环节实现4.1 通过Maven插件执行逆向工程配置文件写好之后执行方式我最推荐的是Maven插件。先在pom.xml的build/plugins里加上mybatis-generator-maven-pluginplugin groupIdorg.mybatis.generator/groupId artifactIdmybatis-generator-maven-plugin/artifactId version1.4.2/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需要特别说明的是插件里的MySQL驱动依赖不能少。很多人配置了generatorConfig.xml里的驱动却发现跑不起来报ClassNotFoundException原因多半是插件默认没有引入驱动。因为generatorConfig虽然指定了driverClass但驱动Jar的加载来自插件依赖。加上这个依赖就稳了。在IDEA的Maven面板里找到mybatis-generator:generate这个插件目标双击执行或者在项目根目录命令行执行mvn mybatis-generator:generate首次执行完去targetProject对应的目录看看应该能看到domain、dao、mapper三个包下都生成了文件。这里有个非常重要的习惯写代码之前先看一眼生成的XML文件里ResultMap、基础列列表Base_Column_List是否和你预期一致。如果字段映射不对八成是表结构注释或字段类型有问题而不是生成器的问题。4.2 生成代码的阅读与改造边界代码生成出来之后很多人会有一个困惑我到底能不能直接改这些代码我的建议是分情况处理。实体类可以放心加业务方法比如额外加一个不分页的分页起始下标字段、加一个冗余的展示字段都不会影响生成器的覆盖。但要注意如果你重新运行生成器overwritetrue的情况下实体类会被直接覆盖手工加的字段会被清掉。所以我的习惯是实体类里需要额外加的字段放在另一个扩展类里比如UserExtend继承User或者干脆复制一个UserVO放Controller层。Mapper接口和XML文件的修改要更谨慎。因为它们是和数据库直接打交道的边界我通常不直接在生成的XML里改方法而是新建一个扩展Mapper接口比如UserMapperExt继承UserMapper然后在单独的UserMapperExt.xml里写复杂SQL。这样逆向工程重新生成时基础代码随便覆盖扩展代码不受影响这是我在多个项目里验证过比较稳的一种方式。4.3 Example类的正确打开方式Example类是MyBatis逆向工程里最有价值但也最容易被误解的部分。初次看到example类可能会觉得代码量大、结构复杂其实核心就三大块Criteria集合、OredCriteria集合、以及一系列条件设置方法。用Example实现条件查询的正确姿势UserExample example new UserExample(); UserExample.Criteria criteria example.createCriteria(); criteria.andStatusEqualTo(1); criteria.andCreateTimeGreaterThanOrEqualTo(startTime); criteria.andUserNameLike(% keyword %); example.setOrderByClause(create_time desc); example.setLimit(10); ListUser userList userMapper.selectByExample(example);这套代码底层会根据Criteria里记录的条件在XML里动态拼SQL。注意createCriteria和or方法的使用如果你只调createCriteria生成的SQL是where后面跟一个条件组组内条件用AND连接如果你想要OR逻辑比如“状态为1或者用户名为xxx”就要用example.or()创建第二个条件组最终SQL是where (条件组1) or (条件组2)。这里有个很容易踩的坑同一个Criteria对象里多次调用andXxx方法都是AND关系。如果你以为调用两个and方法就自动构成OR那SQL查出来的数据就不对了。曾见过不少同事在这上面查半天数据不对最后打印SQL才发现是条件逻辑写错了。另一个坑是setLimit方法这个方法只在较新版本的MBG里默认生成。如果你的targetRuntime生成代码里没有limit方法需要升级MBG版本或者在XML里手写分页。老版本还要配合RowBounds用RowBounds是逻辑分页会把所有数据查出来再内存截断数据量大了性能极差。生产环境一定要用setLimit拼物理分页。5. 常见问题与排查技巧实录5.1 Spring整合启动排查速查表整合过程中启动报错是家常便饭而且很多错误信息长得特别像不仔细看就容易被误导。我把这几年遇到的典型问题整理成了一张速查表基本按概率排序现象常见原因快速解法启动报BeanCreationException提示找不到SqlSessionFactoryMapperScannerConfigurer的sqlSessionFactoryBeanName拼写错对照Spring配置检查Bean名称报Invalid bound statement (not found)Mapper接口方法和XML里的id对不上或mapperLocations没匹配检查mapper目录路径和XML里的namespace启动时提示找不到Mapper XML文件mapperLocations路径配置错误改成classpath:mapper/*.xml确认targetPackage一致数据库连接失败报Communications link failure驱动版本和数据库版本不兼容MySQL8用com.mysql.cj.jdbc.Driver注意时区参数查询结果全是null字段mapUnderscoreToCamelCase没开启在Configuration里开启下划线转驼峰插入数据时主键没回填没有配置useGeneratedKeys和keyProperty在insert的XML标签里加上useGeneratedKeystrue keyPropertyidSQL日志不打印没法排查没有配置logImpl在Configuration里配置StdOutImpl或者logback里配置MyBatis的mapper包为DEBUG级别其中Invalid bound statement这个错误我见过太多次了这里多说一句。它是说MyBatis根据Mapper接口的全限定名加上方法名去对应的XML里找SQL语句却找不到。出现这个问题的原因无非三种一是XML文件没被加载mapperLocations路径不对二是XML里的namespace写错了注意namespace必须是Mapper接口的全限定名不能用简称三是方法名和XML里的id大小写不一致。排查时先看启动日志里有没有加载对应的XML文件再去XML里看namespace和id基本能定位。5.2 Example与XML映射的隐性坑还有两个隐蔽问题想单独提一下。第一个是ResultMap的映射问题。MBG生成的ResultMap默认是列名直接映射属性名如果你数据库里用create_time实体类属性是createTime生成的ResultMap会自动用列名create_time映射到createTime吗不一定。老版本生成器生成的ResultMap里写的是columncreate_time propertycreateTime这个没问题但如果你手工改过列名或者数据库改了字段没重新生成映射就会静默失效查出来的对象某些字段是null而且不报错。第二个坑是批量插入。MBG生成的insert方法默认是单条插入业务里如果要用批量插入我建议自己写不要用循环调insert——虽然也能跑但会产生N次数据库交互性能差很多。自己写批量插入的SQL时注意MySQL的JDBC连接串上要加上allowMultiQueriestrue这个参数否则一条语句里多个INSERT会被数据库拒绝。这个参数很多人在本地没配一上生产环境批量导入就报语法错误排查半天还以为是SQL写错了。还有一个小技巧排查SQL问题最快的办法是打印SQL和参数。传统Spring项目里把Logback或Log4j里对应mapper包日志级别调到DEBUGlogger namecom.example.dao levelDEBUG/这样每次执行SQL都会看到完整的PreparedStatement语句以及参数列表大部分问题一眼就能看出来。这个方法比用断点调试高效得多。6. 进阶扩展与个人心得6.1 自定义TypeHandler与缓存机制逆向工程入门之后你会发现真正决定框架好用程度的往往是对细节的掌控。比如自定义TypeHandler就是必须掌握的一项技能也是面试中很爱问的点。TypeHandler的本质是Java类型和JDBC类型之间的转换器。默认情况下LocalDateTime、Date这些类型都有对应的处理器MyBatis能自动处理。但有些特殊场景比如数据库字段存的是JSON字符串而你的实体类里对应的是List默认的TypeHandler就无能为力了。这时候需要自定义一个TypeHandler在写入时把List序列化成JSON字符串读取时再把字符串反序列化成List。自定义TypeHandler需要实现TypeHandler接口或者继承BaseTypeHandler核心方法就四个setNonNullParameter、getNullableResult、getNullableResult、getNullableResult对应不同的调用场景。比如一个处理JSON数组的TypeHandler大致是这样MappedTypes(List.class) MappedJdbcTypes(JdbcType.VARCHAR) public class JsonListTypeHandler extends BaseTypeHandlerListString { Override public void setNonNullParameter(PreparedStatement ps, int i, ListString parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, JSON.toJSONString(parameter)); } Override public ListString getNullableResult(ResultSet rs, String columnName) throws SQLException { String json rs.getString(columnName); return JSON.parseArray(json, String.class); } // 其余两个getNullableResult方法类似 }这样定义好之后在生成XML的ResultMap或insert语句里指定typeHandler或者在Spring配置里注册就能实现自定义类型的自动转换。面试时被问到TypeHandler的工作流程你只要理解了它是通过ResultSet和PreparedStatement做双向转换再结合TypeHandlerRegistry的注册机制来回答就基本能过关。再说说MyBatis缓存这也是面试高频题。一级缓存是SqlSession级别的在同一个SqlSession里执行相同的SQL且参数一致第二次查询会直接命中缓存。但要注意一级缓存默认开启且无法关闭Spring整合后如果SqlSession没有被Spring管理好可能会因为SqlSession生命周期问题出现数据不一致因此实际项目中一级缓存的作用很有限。二级缓存是Mapper级别的默认不开启需要配置cache标签并且实体类要实现Serializable。开了二级缓存之后不同的SqlSession查同一个Mapper时能共享缓存。但我必须说实话在分布式、多表关联、频繁更新的业务系统里二级缓存很容易带来脏数据问题因为一个Mapper的缓存不能感知其他Mapper对相关表的修改。我的建议是默认关闭除非你非常清楚业务场景且做了充分的缓存失效策略。6.2 从MBG平滑升级到MyBatis-Plus最后聊一下现代化改造。现在新项目基本都用Spring Boot MyBatis-Plus很多人会觉得逆向工程这套是不是过时了。其实不然MyBatis-Plus自带类似的生成器而且内置了BaseMapper单表CRUD根本不用自己写方法还提供了LambdaQueryWrapper这样的条件构造器比手写Example类要简洁很多。我的实际经验是老项目如果还在用纯MBG生成的代码完全没有必要推倒重来。你可以保留现有的实体类和Mapper接口把业务代码逐步迁移到MyBatis-Plus的BaseMapper上面。因为实体类的字段结构是通用的两边都能用迁移的成本主要在SQL方法上。先引入MyBatis-Plus依赖让原来的Mapper接口改成继承BaseMapper然后在新的业务模块里用Wrapper写完条件查询旧的Example代码继续跑等后续需求迭代时再逐个替换平滑过渡。需要提醒的是MyBatis-Plus的BaseMapper默认实现了很多单表方法比如selectList、selectOne、insert、updateById等它在底层会自己拼接SQL不再需要对应的XML文件。如果你还有一个Mapper XML文件里面写了和BaseMapper重复的方法名启动时可能会冲突。所以切换的时候要注意XML文件里那些不再使用的方法该删就删。如果你还在坚持用Spring传统配置也有一些经验可以分享数据源的连接池用HikariCP会比DBCP稳定很多尤其是在高并发场景下性能差距不是一点半点SqlSessionFactory的configuration里建议开启cacheEnabled但二级缓存一句话——除非你懂它否则别碰每一个Mapper接口最好都在XML里显式写namespace哪怕只有一个selectById规范的namespace是长期项目的保命符。回到标题本身。逆向工程的本质是把重复劳动自动化把精力聚焦在真正的业务复杂度上。一开始我觉得MyBatis已经是轻量级框架了逆向工程多此一举实战几年之后才明白正是这种“多此一举”的工具才让团队能把时间花在更有价值的地方。如果你团队里还有人手工补丁式地维护几十个Mapper文件不妨花半天时间把MBG配置跑通把重复的部分交给工具你会发现接下来一个月能省下不少时间。最后再分享一个小习惯每次数据库表结构变更我不会立刻全量重新生成而是先看变更的范围。如果只是加了一个字段直接在实体类和XML里手工补上比重新生成然后合并要快如果是大范围重构比如删表、改字段名、加索引那直接全量生成然后用Git diff来review改动比肉眼检查靠谱得多。这个习惯帮我避过好几次因为改字段名导致的线上问题——生成后第一件事就是看git diff而不是急着提交代码。