1. 为什么需要了解MyBatis初始化流程
很多人在用MyBatis的时候,关注点往往集中在SQL怎么写、映射文件怎么配、动态SQL怎么拼这些“用”的层面。但真正遇到启动报错、配置不生效、Mapper加载失败、插件顺序混乱这些问题时,如果对初始化流程没有清晰的认知,排查起来就会非常被动。我自己在早期做项目时,就曾经因为一个Configuration对象被重复构建的问题,导致自定义插件被注册了两次,SQL执行时日志打印了两遍,排查了大半天才定位到根因。
MyBatis的初始化,本质上就是把散落在各处的配置信息——XML配置文件、注解、Java代码中的配置——收集起来,解析、校验、组装成一个全局唯一的Configuration对象。这个对象贯穿MyBatis的整个生命周期,后续的SqlSession创建、SQL执行、结果映射,全部依赖它。所以初始化流程理解得越透彻,你在做定制化扩展、排查启动期问题、优化启动性能时就越有底气。
这篇文章适合几类人看:一是刚接触MyBatis源码,想搞清楚启动阶段到底发生了什么的开发者;二是正在做MyBatis插件开发或二次封装,需要知道在哪个环节介入最合适的工程师;三是遇到过“配置明明写了却不生效”“Mapper莫名其妙加载不上”这类问题,想从根上弄明白原因的同学。我会从整体设计思路讲起,然后逐层拆解核心组件和实操要点,再给出完整的流程复现和参数计算过程,最后整理一份常见问题速查表。
2. 初始化整体设计与核心组件拆解
2.1 初始化的本质:构建全局唯一的Configuration
MyBatis初始化的核心目标只有一个:构建一个完整、可用、线程安全的Configuration对象。你可以把Configuration理解成一个“配置总仓库”,它里面装着:
Environment:数据源和事务工厂MapperRegistry:Mapper接口与代理工厂的注册表MappedStatement集合:每一条SQL语句的完整描述TypeHandlerRegistry:Java类型与JDBC类型的转换器注册表TypeAliasRegistry:别名注册表InterceptorChain:插件拦截器链LanguageDriverRegistry:动态SQL语言驱动注册表
这些组件不是孤立存在的,它们之间有明确的依赖顺序。比如MappedStatement的构建依赖TypeHandlerRegistry和LanguageDriverRegistry,而InterceptorChain又需要在MappedStatement构建之前就位,否则插件无法拦截到SQL解析过程。这个依赖顺序,决定了初始化流程的步骤编排。
从设计模式角度看,MyBatis初始化用到了建造者模式(SqlSessionFactoryBuilder)和外观模式(SqlSessionFactory对外提供统一入口)。SqlSessionFactoryBuilder负责接收各种形式的配置输入,最终产出一个SqlSessionFactory;而SqlSessionFactory内部持有Configuration,对外屏蔽了初始化细节。
2.2 两种初始化入口:XML与Java API
MyBatis提供了两种初始化方式,它们最终都会走到同一个核心解析逻辑:
第一种:基于XML配置文件
这是最常见的方式。典型代码如下:
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSessionFactoryBuilder.build()有多个重载,但最终都会调用:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }这里的关键是XMLConfigBuilder.parse(),它返回一个Configuration对象,然后build(Configuration)方法用这个对象创建DefaultSqlSessionFactory。
第二种:基于Java API编程式配置
DataSource dataSource = new PooledDataSource("com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/test", "root", "password"); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);这种方式跳过了XML解析,直接操作Configuration对象。适合配置项不多、需要动态构建的场景,比如多数据源路由、单元测试等。
注意:无论哪种方式,
Configuration对象在同一个SqlSessionFactory中必须是唯一的。如果你手动new了多个Configuration并分别build,会得到多个独立的SqlSessionFactory,它们之间不共享任何缓存和注册信息。这一点在多数据源场景下要特别小心。
2.3 XMLConfigBuilder的职责边界
XMLConfigBuilder继承自BaseBuilder,它的核心方法是parse()。这个方法做的事情可以概括为:
- 检查是否已经解析过(
parsed标志位),防止重复解析 - 调用
parseConfiguration()解析<configuration>根节点下的所有子节点 - 处理
<properties>、<settings>、<typeAliases>、<plugins>、<objectFactory>、<objectWrapperFactory>、<reflectorFactory>、<environments>、<databaseIdProvider>、<typeHandlers>、<mappers>这些元素 - 返回构建好的
Configuration
这里有一个设计细节值得注意:parseConfiguration()中各个子节点的解析顺序是有讲究的。比如<properties>必须最先解析,因为后面的<environments>中数据源配置可能会引用这些属性;<typeAliases>要在<mappers>之前解析,因为Mapper XML中可能会用到别名;<plugins>要在<mappers>之前解析,因为插件需要拦截Mapper解析过程。
这个顺序不是随便定的,而是由依赖关系决定的。如果你在自定义扩展时打乱了这个顺序,就可能出现“别名找不到”“插件不生效”等问题。
2.4 Mapper接口与XML的绑定机制
Mapper的初始化是整个流程中最复杂的一环。MyBatis支持两种Mapper配置方式:
- 在
<mappers>中通过<mapper resource="..."/>指定XML文件 - 在
<mappers>中通过<mapper class="..."/>或<package name="..."/>指定接口
当指定的是接口时,MyBatis会尝试查找同名的XML文件(在同一包路径下,文件名与接口简单名相同)。如果找到了,就解析XML;如果没找到,就只注册接口,SQL全部依赖注解。
绑定的核心逻辑在MapperRegistry.addMapper()和MapperAnnotationBuilder中。MapperAnnotationBuilder负责解析接口上的注解(如@Select、@Insert、@Results等),并生成对应的MappedStatement。而XML解析则由XMLMapperBuilder负责,它解析完XML后,会调用MapperRegistry.addMapper()把接口和代理工厂注册进去。
这里有一个容易踩的坑:如果接口和XML同时存在,且namespace正确匹配,MyBatis会以XML为准,注解会被忽略。但如果你在XML中只写了部分方法,注解写了另一部分,那么最终生效的是两者的并集——XML中的方法覆盖注解中的同名方法。这个行为在官方文档中没有明确说明,但实际使用中确实如此。
3. 核心细节解析与实操要点
3.1 Properties解析:优先级与覆盖规则
<properties>元素的解析看似简单,但优先级规则容易搞混。MyBatis的属性来源有三个:
- 在
<properties>内部通过<property name="..." value="..."/>定义的属性 - 通过
<properties resource="..." url="..."/>加载的外部属性文件 - 通过
SqlSessionFactoryBuilder.build()方法传入的Properties参数
优先级从低到高依次是:内部定义 < 外部文件 < 方法参数。也就是说,方法参数中的属性会覆盖外部文件中的同名属性,外部文件中的属性会覆盖内部定义的同名属性。
这个规则的设计意图是:允许运行时动态覆盖配置。比如你在测试环境中想临时改一个数据源URL,不需要修改配置文件,只需要在build时传入一个Properties即可。
实操中有一个细节:<properties>的resource和url属性不能同时使用,否则会抛出异常。而且resource的路径是相对于classpath的,url则是标准的URL格式。
<properties resource="db.properties"> <property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> </properties>如果db.properties中也有jdbc.driver,那么最终生效的是文件中的值,因为外部文件优先级高于内部定义。
3.2 Settings解析:默认值与性能影响
<settings>是MyBatis中最影响运行时行为的配置项集合。每一个setting都对应Configuration中的一个字段。解析逻辑在XMLConfigBuilder.settingsElement()中,它通过反射调用Configuration的setter方法。
几个关键setting的默认值和影响:
| Setting名称 | 默认值 | 影响范围 | 建议 |
|---|---|---|---|
| cacheEnabled | true | 二级缓存开关 | 如果不用二级缓存,设为false可减少开销 |
| lazyLoadingEnabled | false | 延迟加载 | 关联对象多时建议开启 |
| aggressiveLazyLoading | false | 延迟加载策略 | 3.4.1后默认false,按需加载 |
| mapUnderscoreToCamelCase | false | 下划线转驼峰 | 数据库字段有下划线时建议开启 |
| defaultExecutorType | SIMPLE | 执行器类型 | 批量操作多时可用BATCH |
| defaultStatementTimeout | null | SQL超时 | 建议设置,防止慢查询拖垮连接池 |
| jdbcTypeForNull | OTHER | null值JDBC类型 | Oracle环境下建议设为NULL |
| logImpl | 自动发现 | 日志实现 | 建议显式指定,避免自动发现失败 |
提示:
logImpl如果不显式指定,MyBatis会按照SLF4J、Commons Logging、Log4J2、Log4J、JDK Logging的顺序自动发现。如果classpath中有多个日志框架,可能会选到你不想用的那个。显式指定可以避免这个问题。
3.3 TypeAlias注册:包扫描与注解
类型别名的作用是简化XML中的类型引用。比如com.example.entity.User可以简写为User。注册方式有三种:
- 在
<typeAliases>中逐个配置<typeAlias type="..." alias="..."/> - 通过
<package name="..."/>批量扫描,默认使用类简单名(首字母小写)作为别名 - 在实体类上使用
@Alias("...")注解指定别名
批量扫描时,TypeAliasRegistry.registerAliases()会遍历包下所有类,跳过接口、内部类、匿名类。如果类上有@Alias注解,就用注解值;否则用类简单名的小写形式。
这里有一个性能注意点:包扫描是通过ResolverUtil实现的,它会遍历classpath下指定包的所有类文件。如果包路径范围过大(比如直接扫描com),启动时会扫描大量无关类,拖慢启动速度。建议精确到实体类所在的包。
3.4 Plugin注册:拦截器链的构建顺序
插件的注册顺序直接决定了拦截器的执行顺序。InterceptorChain内部是一个List<Interceptor>,addInterceptor()按添加顺序追加。执行时,pluginAll()方法会按照添加顺序的逆序包装目标对象。
举个例子:如果你先添加了插件A,再添加了插件B,那么最终包装顺序是B包裹A,执行时A先拦截,B后拦截。这个顺序在<plugins>配置中就是从上到下的顺序。
<plugins> <plugin interceptor="com.example.PluginA"/> <plugin interceptor="com.example.PluginB"/> </plugins>上面配置中,PluginA先被添加,PluginB后被添加。执行时PluginA先拦截,PluginB后拦截。如果你需要PluginB先拦截,就要把PluginB放在前面。
注意:插件可以拦截
Executor、StatementHandler、ParameterHandler、ResultSetHandler四个接口的方法。不同接口的拦截时机不同,插件顺序的影响也不同。建议在开发阶段打印拦截器链,确认顺序符合预期。
3.5 Environment与TransactionFactory
<environments>可以配置多个环境,通过default属性指定默认环境。每个<environment>包含<transactionManager>和<dataSource>。
TransactionFactory有两种内置实现:JdbcTransactionFactory和ManagedTransactionFactory。前者依赖数据源的连接管理,后者把事务交给容器管理(如Spring)。在Spring集成场景中,通常使用ManagedTransactionFactory,因为事务已经由Spring的DataSourceTransactionManager接管了。
数据源类型有三种:UNPOOLED、POOLED、JNDI。UNPOOLED每次请求都新建连接,适合测试;POOLED使用连接池,适合生产;JNDI从容器获取数据源,适合应用服务器环境。
解析<dataSource>时,DataSourceFactory会根据type属性选择对应的工厂类,然后通过反射设置<property>中的属性。这里有一个细节:属性名必须与DataSource实现类的setter方法匹配,否则会抛出异常。比如PooledDataSource有setDriver、setUrl、setUsername、setPassword等方法,配置中的name属性就要对应这些方法名。
4. 完整初始化流程复现与参数计算
4.1 从build()到parse()的调用链
完整的初始化调用链如下:
SqlSessionFactoryBuilder.build(InputStream) -> XMLConfigBuilder(inputStream, environment, properties) -> XMLConfigBuilder.parse() -> parseConfiguration(parser.evalNode("/configuration")) -> propertiesElement(root.evalNode("properties")) -> settingsElement(settings) -> typeAliasesElement(root.evalNode("typeAliases")) -> pluginElement(root.evalNode("plugins")) -> objectFactoryElement(root.evalNode("objectFactory")) -> objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")) -> reflectorFactoryElement(root.evalNode("reflectorFactory")) -> environmentsElement(root.evalNode("environments")) -> databaseIdProviderElement(root.evalNode("databaseIdProvider")) -> typeHandlerElement(root.evalNode("typeHandlers")) -> mapperElement(root.evalNode("mappers")) -> build(Configuration) -> new DefaultSqlSessionFactory(config)每一步的解析结果都直接写入Configuration对象。比如settingsElement()通过反射调用configuration.setCacheEnabled()等方法;typeAliasesElement()调用configuration.getTypeAliasRegistry().registerAlias();pluginElement()调用configuration.addInterceptor()。
4.2 Mapper XML解析的详细步骤
XMLMapperBuilder.parse()是Mapper XML解析的入口,它的步骤是:
- 检查namespace是否已注册,防止重复解析
- 解析
<cache>元素,构建二级缓存 - 解析
<cache-ref>元素,引用其他namespace的缓存 - 解析
<resultMap>元素,构建ResultMap - 解析
<parameterMap>元素(已废弃,但保留兼容) - 解析
<sql>元素,构建SqlFragment - 解析
<select>、<insert>、<update>、<delete>元素,构建MappedStatement - 绑定Mapper接口,调用
bindMapperForNamespace()
其中第7步是最复杂的。每个CRUD元素都会被解析成一个MappedStatement,包含:
id:namespace + "." + 元素idstatementType:STATEMENT、PREPARED、CALLABLEsqlSource:SQL来源,可能是静态SQL或动态SQLparameterMap:参数映射(已废弃)resultMaps:结果映射列表keyGenerator:主键生成器timeout:超时时间fetchSize:抓取大小statementType:语句类型resultSetType:结果集类型databaseId:数据库ID
sqlSource的构建是核心。如果SQL中包含动态标签(<if>、<choose>、<foreach>等),会构建DynamicSqlSource;否则构建RawSqlSource。DynamicSqlSource在每次执行时都会根据参数重新生成SQL,而RawSqlSource在初始化时就生成了最终的SQL字符串,性能更好。
4.3 参数计算:连接池配置的估算
以PooledDataSource为例,几个关键参数的估算方法:
poolMaximumActiveConnections:最大活跃连接数。估算公式:并发线程数 * 每个线程平均同时使用的连接数。一般设置为CPU核数的2-4倍。比如4核机器,可以设为8-16。poolMaximumIdleConnections:最大空闲连接数。一般设为最大活跃连接数的一半左右。poolMaximumCheckoutTime:连接被借出的最大时间。默认20000ms(20秒)。如果SQL执行时间较长,需要调大。poolTimeToWait:获取连接失败后的等待时间。默认20000ms。如果并发高,可以适当调小,让失败更快暴露。
这些参数没有绝对的最优值,需要根据实际压测结果调整。我的经验是:先按默认值跑,观察连接池的活跃连接数和等待时间,再逐步调整。如果频繁出现等待,说明最大活跃连接数不够;如果空闲连接数长期为0,说明最大空闲连接数偏小。
4.4 实操现场记录:一个完整的初始化示例
下面是一个完整的初始化示例,包含配置文件和Java代码:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="db.properties"> <property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> </properties> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="defaultStatementTimeout" value="30"/> <setting name="logImpl" value="SLF4J"/> </settings> <typeAliases> <package name="com.example.entity"/> </typeAliases> <plugins> <plugin interceptor="com.example.plugin.PageInterceptor"> <property name="dialect" value="mysql"/> </plugin> <plugin interceptor="com.example.plugin.SqlLogInterceptor"/> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="poolMaximumActiveConnections" value="16"/> <property name="poolMaximumIdleConnections" value="8"/> </dataSource> </environment> </environments> <mappers> <package name="com.example.mapper"/> </mappers> </configuration>对应的Java代码:
public class MyBatisInitializer { private static SqlSessionFactory sqlSessionFactory; static { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { throw new RuntimeException("MyBatis初始化失败", e); } } public static SqlSessionFactory getSqlSessionFactory() { return sqlSessionFactory; } }这个示例中,db.properties文件内容如下:
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456注意jdbc.driver在<properties>内部定义了,但db.properties中没有,所以最终使用的是内部定义的值。如果db.properties中也有jdbc.driver,则会覆盖内部定义。
5. 常见问题与排查技巧实录
5.1 启动报错:BindingException: Invalid bound statement
这是最常见的启动期问题之一。报错信息通常是:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectById排查思路:
- 检查Mapper XML的namespace是否与接口全限定名完全一致
- 检查XML中statement的id是否与接口方法名完全一致
- 检查XML文件是否在classpath的正确位置(与接口同包路径)
- 检查
<mappers>中是否配置了该Mapper - 检查编译后target目录下是否有XML文件(Maven默认不打包src/main/java下的XML)
提示:Maven项目中,如果XML放在src/main/java下,需要在pom.xml中配置resources插件,否则XML不会被复制到target目录。这是新手最容易踩的坑。
5.2 配置不生效:settings被覆盖
有时候你明明在<settings>中配置了某个选项,但运行时就是不生效。常见原因:
- 多个
<settings>元素:MyBatis只解析第一个,后面的会被忽略 - 属性名拼写错误:比如
mapUnderscoreToCamelCase写成了mapUnderscoreToCamelcase - 值类型不匹配:比如
defaultStatementTimeout需要整数,写成了字符串 - 被Java API覆盖:如果同时使用了XML和Java API,Java API中的设置会覆盖XML中的
排查方法:在Configuration构建完成后,打印关键字段的值,确认是否符合预期。
5.3 插件顺序混乱:拦截器执行顺序不符合预期
插件的执行顺序由添加顺序决定,但很多开发者误以为配置顺序就是执行顺序。实际上:
- 添加顺序:从上到下
- 包装顺序:从下到上(后添加的包裹先添加的)
- 执行顺序:先添加的先执行
如果你希望某个插件最先执行,就把它放在<plugins>的最上面。如果希望它最后执行,就放在最下面。
注意:不同接口的拦截器之间没有顺序关系,因为它们的执行时机不同。比如
Executor的拦截器在SQL执行前触发,ResultSetHandler的拦截器在结果集处理时触发。只有同一接口的多个拦截器之间才有顺序问题。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Invalid bound statement | namespace或id不匹配 | 对比接口全限定名和XML namespace | 修正namespace或id |
| XML文件未加载 | Maven未打包XML | 检查target目录 | 配置resources插件 |
| 别名找不到 | 包扫描路径错误 | 检查<package>的name属性 | 修正包路径 |
| 插件不生效 | 拦截签名错误 | 检查@Intercepts注解 | 修正签名 |
| 数据源连接失败 | 属性名不匹配 | 检查property的name | 对照setter方法名 |
| 二级缓存不生效 | cacheEnabled为false | 检查settings | 设为true |
| 延迟加载失效 | aggressiveLazyLoading为true | 检查settings | 设为false |
| 下划线转驼峰失效 | mapUnderscoreToCamelCase为false | 检查settings | 设为true |
5.5 独家避坑技巧
技巧一:启动时打印Configuration摘要
在初始化完成后,写一个工具方法打印Configuration的关键信息,包括注册的Mapper数量、拦截器数量、TypeHandler数量、缓存数量等。这样在启动阶段就能发现配置遗漏。
public static void printConfigurationSummary(Configuration configuration) { System.out.println("=== MyBatis Configuration Summary ==="); System.out.println("Mappers: " + configuration.getMapperRegistry().getMappers().size()); System.out.println("Interceptors: " + configuration.getInterceptors().size()); System.out.println("TypeHandlers: " + configuration.getTypeHandlerRegistry().getTypeHandlers().size()); System.out.println("Caches: " + configuration.getCaches().size()); System.out.println("MappedStatements: " + configuration.getMappedStatements().size()); }技巧二:使用databaseIdProvider适配多数据库
如果项目需要同时支持MySQL和Oracle,可以使用<databaseIdProvider>。MyBatis会根据数据源的DatabaseMetaData自动识别数据库类型,然后在解析Mapper时,优先选择匹配databaseId的statement。
<databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql"/> <property name="Oracle" value="oracle"/> </databaseIdProvider>然后在Mapper XML中,可以写两个版本的statement:
<select id="selectUser" resultType="User" databaseId="mysql"> SELECT * FROM user WHERE id = #{id} LIMIT 1 </select> <select id="selectUser" resultType="User" databaseId="oracle"> SELECT * FROM user WHERE id = #{id} AND ROWNUM = 1 </select>技巧三:自定义TypeHandler的注册时机
自定义TypeHandler需要在<typeHandlers>中注册,但注册时机必须在Mapper解析之前。如果你在Java API中先addMapper()再注册TypeHandler,Mapper中的类型转换会使用默认的TypeHandler,导致自定义逻辑不生效。正确的顺序是:先注册TypeHandler,再添加Mapper。
技巧四:避免重复初始化
在Web应用中,SqlSessionFactory应该是单例的。如果每次请求都build一次,会导致大量重复解析,严重拖慢性能。建议使用静态初始化块或Spring的@Bean来保证单例。
技巧五:使用Environment隔离多数据源
如果项目有多个数据源,不要试图在一个Configuration中配置多个Environment然后动态切换。Environment是在初始化时确定的,运行时无法切换。正确的做法是为每个数据源创建独立的SqlSessionFactory,每个Factory有自己的Configuration。
6. 初始化性能优化与扩展点
6.1 启动性能优化的几个方向
MyBatis初始化在大型项目中可能耗时数秒甚至数十秒。优化方向主要有:
- 减少包扫描范围:
<typeAliases>和<mappers>的<package>尽量精确,避免扫描整个com包 - 使用RawSqlSource:避免不必要的动态SQL标签,让MyBatis在初始化时就生成静态SQL
- 延迟加载Mapper:MyBatis 3.4.2+支持
lazyLoadingEnabled,但这是针对关联对象的,不是针对Mapper的。Mapper的解析是启动时完成的,无法延迟 - 减少插件数量:每个插件都会增加一层代理,影响启动和执行性能
- 使用注解替代XML:注解解析比XML解析快,但可维护性差,需要权衡
6.2 自定义扩展点的介入时机
MyBatis提供了多个扩展点,它们的介入时机如下:
| 扩展点 | 接口 | 介入时机 | 典型用途 |
|---|---|---|---|
| 拦截器 | Interceptor | 初始化时注册,运行时拦截 | 分页、SQL日志、权限 |
| 类型处理器 | TypeHandler | 初始化时注册,运行时转换 | 自定义类型映射 |
| 对象工厂 | ObjectFactory | 初始化时注册,运行时创建对象 | 依赖注入 |
| 语言驱动 | LanguageDriver | 初始化时注册,解析动态SQL | 自定义动态SQL语法 |
| 数据库ID提供者 | DatabaseIdProvider | 初始化时注册,解析Mapper时使用 | 多数据库适配 |
这些扩展点的注册顺序很重要。拦截器必须在Mapper解析之前注册,否则无法拦截Mapper解析过程;TypeHandler必须在Mapper解析之前注册,否则Mapper中的类型转换会使用默认实现。
6.3 一个完整的自定义扩展示例
下面是一个自定义TypeHandler的完整示例,用于处理JSON字段:
@MappedTypes(JsonObject.class) public class JsonTypeHandler extends BaseTypeHandler<JsonObject> { private static final ObjectMapper objectMapper = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, JsonObject parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.toString()); } @Override public JsonObject getNullableResult(ResultSet rs, String columnName) throws SQLException { String json = rs.getString(columnName); return parseJson(json); } @Override public JsonObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String json = rs.getString(columnIndex); return parseJson(json); } @Override public JsonObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String json = cs.getString(columnIndex); return parseJson(json); } private JsonObject parseJson(String json) { if (json == null || json.isEmpty()) { return null; } try { return objectMapper.readValue(json, JsonObject.class); } catch (Exception e) { throw new RuntimeException("JSON解析失败", e); } } }注册方式:
<typeHandlers> <typeHandler handler="com.example.handler.JsonTypeHandler" javaType="com.example.entity.JsonObject"/> </typeHandlers>或者在实体类字段上使用@TableField(typeHandler = JsonTypeHandler.class)(MyBatis-Plus场景)。
提示:自定义TypeHandler时,
@MappedTypes注解用于指定处理的Java类型,@MappedJdbcTypes用于指定处理的JDBC类型。如果只指定了Java类型,MyBatis会在所有JDBC类型上尝试使用该Handler。
6.4 初始化流程的调试技巧
如果你需要深入调试初始化流程,可以在关键节点打断点:
XMLConfigBuilder.parse():入口XMLConfigBuilder.parseConfiguration():配置解析总入口XMLMapperBuilder.parse():Mapper XML解析入口MapperAnnotationBuilder.parse():注解解析入口Configuration.addMappedStatement():MappedStatement注册Configuration.addInterceptor():插件注册
通过观察这些方法的调用顺序和参数,可以快速定位配置问题。另外,MyBatis的ErrorContext会记录解析过程中的上下文信息,报错时打印的ErrorContext内容非常有用,不要忽略。
7. 个人实操体会与建议
我在多个项目中做过MyBatis的定制化扩展,踩过的坑主要集中在三个方面:配置顺序、插件签名、TypeHandler注册时机。配置顺序问题最隐蔽,因为MyBatis不会报错,只是行为不符合预期;插件签名问题最直接,启动就报错;TypeHandler注册时机问题最容易被忽略,因为默认TypeHandler也能工作,只是自定义逻辑不生效。
我的建议是:在项目初期就建立一套初始化检查清单,每次新增配置或插件时,对照清单确认。清单内容包括:namespace是否匹配、XML是否打包、插件签名是否正确、TypeHandler是否在Mapper之前注册、settings是否被覆盖。这套清单看起来简单,但能避免80%的启动期问题。
另外,不要害怕读源码。MyBatis的初始化代码结构清晰,命名规范,读一遍XMLConfigBuilder和XMLMapperBuilder,比看十篇博客都有用。遇到不确定的行为,直接打断点跟一遍,比猜测靠谱得多。
最后分享一个小技巧:如果你在排查一个复杂的初始化问题,可以临时把Configuration对象序列化出来,对比正常和异常情况下的差异。虽然Configuration没有实现Serializable,但你可以写一个反射工具,把关键字段的值打印成JSON,对比起来非常直观。这个技巧帮我定位过好几次“配置明明一样但行为不同”的诡异问题。