1. 先搞清楚:没有它,MyBatis在Spring里根本跑不起来
如果你和我一样,长期维护基于Spring的Java后端项目,多半见过这么一条让人头疼的报错:
Invalid bound statement (not found): com.example.mapper.UserMapper.selectById第一反应往往是"Mapper接口没被扫描到"或"XML没放进target/classes"。于是查@ComponentScan、查@MapperScan、查namespace,折腾一圈回来发现全都正常——真正出问题的,很可能只是SqlSessionFactoryBean里那行不起眼的mapperLocations配置。
SqlSessionFactoryBean是MyBatis与Spring整合时的核心类,但它不在业务代码里出现,容易被"一带而过"。很多人只知道它是用来造SqlSessionFactory的,却说不清它有哪些属性、这些属性在什么时机生效、为什么配错了会导致事务不回滚或Mapper注入失败。这篇文章我想从属性拆解、启动链路源码、日常排错、事务与批量场景、Spring Boot差异、进阶配置这几个维度,把这颗"幕后基石"彻底讲透。
适合谁看:正在用Spring XML或JavaConfig手动整合MyBatis的人、排查了很久都搞不定Mapper注入或事务失效的后端开发,以及想把MyBatis启动过程从"黑盒"变成"透明"的读者。如果你用的是Spring Boot,看完也会明白为什么Boot里通常不用手写它,以及什么情况下你确实需要自己搞一个。
1.1 原生MyBatis启动方式回顾
要理解SqlSessionFactoryBean的价值,得先回到原生MyBatis的启动方式。在没有Spring的年代,写MyBatis项目是这样一个套路:
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1L); session.commit(); }这段代码逻辑简单,但一放进Spring容器就会出现几个绕不开的矛盾。
第一,SqlSessionFactoryBuilder是典型的一次性对象,真正需要长期持有的是构建出来的SqlSessionFactory,它应该作为全局单例存在,谁来管理它的生命周期?第二,openSession()每次都会打开一个新的SqlSession,连接何时提交、何时回滚完全靠手动控制,这和Spring的声明式事务机制是两套体系,硬凑在一起只会互相打架。第三,session.getMapper()获得的是Mapper代理对象,怎么把它变成Spring容器里的Bean,让Service层能直接@Autowired注入?
这三个问题,恰好就是SqlSessionFactoryBean存在的原因。
1.2 SqlSessionFactoryBean在整合体系中的位置
SqlSessionFactoryBean这个名字很有迷惑性,光看名字像是"一个生产SqlSessionFactory的工具Bean",但它在Spring语境里的真实身份要更微妙。它实现了Spring的FactoryBean接口,也实现了InitializingBean接口。这意味着Spring容器在启动时会做两件事:先把DataSource、mapperLocations这些属性注入进来,然后调用afterPropertiesSet()执行构建逻辑,最终通过getObject()把构建好的SqlSessionFactory暴露给整个容器。
你可以把它理解成一个"配置翻译器":Spring容器里的数据源、事务工厂、Mapper XML路径、类型别名包,原本都是分散的配置项,它负责把这些东西聚拢起来,翻译成MyBatis真正需要的Configuration对象,再生成一个DefaultSqlSessionFactory。
后面的链路才是我们熟悉的:SqlSessionTemplate拿这个工厂创建SqlSession,MapperFactoryBean通过它注册Mapper接口的代理,@Transactional通过DataSourceTransactionManager控制同一份连接。换句话说,整个MyBatis-Spring生态的根基,就是从SqlSessionFactoryBean这里长出来的。
这里可以用一张表把原生方式和Spring整合方式放在一起看:
| 对比维度 | 原生 MyBatis | SqlSessionFactoryBean 整合 |
|---|---|---|
| SqlSessionFactory 生命周期 | 由应用代码自行持有 | 由Spring容器创建并托管单例 |
| Session 管理 | 手动 openSession / close | SqlSessionTemplate 自动管理 |
| 事务控制 | 代码里手动 commit / rollback | 接入 Spring 声明式事务 |
| Mapper 获取 | session.getMapper() | 注入 MapperFactoryBean 代理对象 |
| 配置来源 | mybatis-config.xml | Spring 属性 + 可选的 XML 配置 |
这张表也是后面所有排错思路的地图:无论报错是Mapper找不到、事务不回滚,还是数据源没对上,你都可以沿着表格里的每一行去排查。
2. 必配项与选配项大梳理:每一个属性背后的设计意图
SqlSessionFactoryBean提供了一长串setter,第一次接触的人容易看花眼。我不打算按官方文档顺序罗列,而是按"为什么需要它"的逻辑来拆,这样你配置的时候能知道每个属性在解决什么问题。
2.1 dataSource、transactionFactory:数据从哪来,事务听谁的
dataSource是最核心的必配项。如果你忘记设置,构建阶段会在afterPropertiesSet()里直接抛异常,因为MyBatis的Configuration对象需要一个Environment,而Environment必须包含一个数据源才能拿到连接。常见的错误是把数据源定义在Spring里,但SqlSessionFactoryBean没有引用它,结果启动报错信息很含糊,需要花时间才能定位到是缺数据源。
transactionFactory则决定了SqlSession里的连接如何参与事务。这里有个容易踩的坑:SqlSessionFactoryBean在实例化时,默认已经装配了SpringManagedTransactionFactory,它做的事情是:获取连接时通过DataSourceUtils从Spring当前事务同步管理器里取,配合DataSourceTransactionManager就能让MyBatis的每一次SQL操作都落在Spring管控的事务里。但如果有人显式把它改成了JdbcTransactionFactory,MyBatis就会自己管理连接和提交回滚,与Spring事务彻底脱节,表现就是@Transactional明明标了,抛异常却不回滚。
所以你先记住一个结论:在Spring工程里,不要主动把transactionFactory设成JDBC工厂,除非你非常清楚自己在做什么。默认的Spring托管方式是正确的。
2.2 configLocation与configuration属性:二选一的配置入口
这两个属性是很多配置冲突的源头。configLocation指向的是MyBatis传统意义上的mybatis-config.xml,里面可以写settings、typeAliases、typeHandlers、plugins等。configuration则直接接收一个MyBatis的Configuration对象,相当于在Spring代码里把XML要干的事全部编程式完成。
关键在于:如果两个都设置了,构建时configLocation解析出的XML内容会覆盖configuration里的同名设置。我见过不少项目在XML里写了一套settings,又在JavaConfig里通过配置类设置了另一套,结果发现"为什么我代码里配的驼峰映射没生效"——原因就是XML优先级更高,它把代码里的设置覆盖掉了。
我的建议是:一个项目只保留一个配置入口。如果团队习惯XML,就统一用configLocation,把settings、typeAliases都写在XML里;如果团队习惯JavaConfig,就放弃mybatis-config.xml,所有设置都通过Configuration对象完成。两头都配置省不了多少事,却会在环境切换时埋下不可预期的雷。
2.3 mapperLocations与typeAliasesPackage:映射文件与别名的通配符诀窍
mapperLocations是排查"Mapper找不到"问题时第一个要看的地方。它接收一个Resource[]数组,最常见的写法是用Spring的PathMatchingResourcePatternResolver去解析classpath*:mapper/**/*.xml。这里有个细节值得注意:classpath:和classpath*:行为不一样。单个classpath:只扫描当前类路径下匹配的资源,多个Jar包时可能只命中一个;classpath*:会扫描所有依赖Jar里的匹配项。单模块工程用哪个都行,但一旦拆了多模块,建议用classpath*:,否则某个模块下的Mapper XML没被扫进来,你会在运行时才发现"有的接口能用、有的报空指针"。
typeAliasesPackage解决的是XML里别名问题。比如你在Mapper XML里写parameterType="User",如果没配别名包,MyBatis不认识User,必须写全限定名。设置了这个属性后,MyBatis会扫描指定包下的所有类,把类名注册成别名(大小写不敏感)。省事归省事,包路径一定要精确,别直接扫到整个com.example根包,类多了启动扫描会变慢,还会出现误注册。
2.4 plugins、databaseIdProvider等扩展属性与一份完整配置样例
剩下的属性大多是扩展点:plugins注册MyBatis拦截器,分页插件、审计插件都从这里进;databaseIdProvider用来判断当前连接的是MySQL还是Oracle,让SQL可以根据数据库方言走不同语句;typeHandlersPackage自动扫描自定义TypeHandler;objectFactory、objectWrapperFactory、vfs这些属于进阶定制,绝大多数项目用不上,知道有这回事即可。
下面给两份可以直接抄的完整配置,先看Spring XML方式:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath*:mapper/**/*.xml"/> <property name="typeAliasesPackage" value="com.example.model"/> <property name="plugins"> <array> <ref bean="paginationInterceptor"/> </array> </property> </bean>对应的JavaConfig方式:
@Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/**/*.xml")); factoryBean.setTypeAliasesPackage("com.example.model"); factoryBean.setPlugins(new Interceptor[]{paginationInterceptor()}); return factoryBean.getObject(); }注意JavaConfig写法里最后一定要return factoryBean.getObject(),返回真正的SqlSessionFactory给Spring容器。如果你直接返回factoryBean本身,Spring会以为你要注册的是SqlSessionFactoryBean类型的Bean,后面注入SqlSessionFactory时类型对不上,照样报NoSuchBeanDefinitionException。
3. 启动链路源码跟踪:Spring容器里到底发生了什么
知道属性怎么配还不够,遇到诡异问题时要能顺着源码想清楚"Spring到底在什么时候干了什么"。这节我带你把启动链路完整走一遍。
3.1 FactoryBean机制:Spring为工厂对象留的口子
Spring容器里有个专门处理FactoryBean的机制:当一个Bean实现了FactoryBean接口,容器不会直接暴露这个Bean本身,而是调用它的getObject()方法,把返回结果当作真正的Bean注册进容器。你在Service里注入SqlSessionFactory时,拿到的是getObject()返回的DefaultSqlSessionFactory。
SqlSessionFactoryBean同时实现了InitializingBean,所以Spring会在所有属性注入完成后调用afterPropertiesSet(),这个阶段就是构建SqlSessionFactory的地方。构建完成的实例会被缓存起来,后续每次getObject()都返回同一个对象,这正是我们想要的效果:整个容器共享一个SqlSessionFactory。
有个排查小技巧:如果某些场景下你需要拿到SqlSessionFactoryBean本身而不是它暴露的对象,可以用getBean("&sqlSessionFactory"),Spring会返回FactoryBean本体。这种写法平时用不上,但调试"FactoryBean配置异常"时很管用。
3.2 afterPropertiesSet():属性校验到SqlSessionFactory构建
SqlSessionFactoryBean的核心构建逻辑集中在buildSqlSessionFactory()方法里,执行顺序大致如下:
- 如果
configuration属性为空,先创建一个全新的Configuration对象。 - 如果指定了
configLocation,用XMLConfigBuilder解析XML,把解析结果填充进Configuration。这里会覆盖掉之前手动设置的许多项,所以要记住XML优先。 - 设置
Environment,包括数据源和事务工厂。 - 处理
typeAliasesPackage、typeHandlersPackage、plugins,把它们注册进Configuration。 - 解析
mapperLocations,逐个加载Mapper XML文件,把里面的SQL解析成MappedStatement注册进去。 - 最终返回
new DefaultSqlSessionFactory(configuration)。
注意第5步,如果mapperLocations为空,MyBatis不会做任何XML加载。有些项目只用注解写SQL,没问题;但如果你既有XML又没配mapperLocations,那么所有XML里的SQL都不会进入Configuration,运行时就等着收"Invalid bound statement"吧。
3.3 Mapper是如何找到归属的:与MapperFactoryBean配合
MapperScan扫描Mapper接口时,注册的其实是MapperFactoryBean。它持有一个SqlSessionFactory引用,getObject()内部会通过工厂拿到SqlSession,再从Configuration里注册的MapperRegistry中取出对应接口的Mapper代理。
这个链路的关键在于:MapperFactoryBean和SqlSessionFactoryBean必须是对应关系。多数据源场景下,如果有两个SqlSessionFactoryBean,但你只配了一组@MapperScan,某些Mapper接口可能会被注册到错误的工厂上。表现出来就是:当前数据源是对的,SQL也能查出来,但事务永远落在错误的数据源上。这类问题特别隐蔽,后面我会专门展开。
3.4 为什么事务能回滚:SpringManagedTransactionFactory接管Connection
默认的SpringManagedTransactionFactory会创建SpringManagedTransaction,它获取连接时走的是DataSourceUtils.getConnection(dataSource)。这意味着:如果当前线程正处于Spring管理的事务中,它会拿到事务同步管理器里缓存的连接;如果没有事务,它再从数据源新获取一个,而是否提交、关闭又会与Spring的事务同步机制配合。
所以MyBatis的SqlSession提交回滚,实际是调用这个Transaction对象对Connection执行操作。只要数据源是同一个、事务管理器配的是同一个数据源,@Transactional里的逻辑就会作用在MyBatis拿到的连接上,两者天然对齐。如果你换成了JdbcTransactionFactory,MyBatis自己从DataSource拿连接,Spring事务管理器感知不到这层连接,回滚自然管不住。
4. 比报错更值得看的是排查过程:Invalid bound statement 的完整链路
Invalid bound statement是MyBatis项目里出现频率最高的报错之一,但很多人排查时只盯着Mapper接口,忽略了背后的SqlSessionFactoryBean。我想用一次完整的排查思路,把这条链路上的关键点串起来。
4.1 现象与第一反应:先别急着怪MapperScan
报错形式通常是这样的:
Invalid bound statement (not found): com.example.mapper.UserMapper.selectById或是:
Mapped Statements collection does not contain value for com.example.mapper.UserMapper.selectById第一反应往往是检查接口上有没有@Mapper注解、@MapperScan有没有覆盖包路径、方法名有没有写错。这些检查没问题之后,思路很容易卡住。这时候你应该把视角从消费者切到生产者:Mapper接口只是消费者,它需要的MappedStatement是SqlSessionFactoryBean在启动时从XML或注解里注册进Configuration的。如果源头没加载,消费者自然找不到。
4.2 逐项排查SqlSessionFactoryBean:mapperLocations、namespace与statement id
排查顺序我建议是固定的:
第一步,确认工程里真的声明了SqlSessionFactoryBean。XML工程看applicationContext.xml,JavaConfig工程看配置类里有没有@Bean SqlSessionFactory。
第二步,确认mapperLocations能匹配到文件。这是最容易被忽视的一步。你可以在配置类里立刻验证:
@Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("classpath*:mapper/**/*.xml"); System.out.println("loaded mapper xml count = " + resources.length); for (Resource resource : resources) { System.out.println(" - " + resource.getFilename()); } // ... }如果count为0,问题就出在路径通配符。常见原因:XML放在了src/main/resources/mapper/下,但mapperLocations写成了classpath*:mappers/**/*.xml,一个字母的差异就会导致全部漏加载。
第三步,确认XML里的namespace与Mapper接口全限定名完全一致。如果namespace写错,报错信息往往不是"XML找不到",而是"statement not found",因为MyBatis是按namespace+id来索引MappedStatement的。
第四步,确认statement id与Mapper方法名一致。selectById对应<select id="selectById">,大小写、参数类型匹配都要看。
我按这个顺序排查过好几次类似的报错,90%以上的原因落在第二步和第三步。如果你只想记住一点,那就是:先看mapperLocations匹配了几个文件,再看namespace对不对。
4.3 多数据源下的"掩护性错误":多个FactoryBean并存时
多数据源场景更棘手,因为会有多个SqlSessionFactoryBean并存,报错可能互相掩护。比如你配置了两个FactoryBean,主库的Mapper XML正常加载,从库的Mapper XML路径写错,结果从库对应的接口也会报"Invalid bound statement"。
排查时需要确认每个FactoryBean各自加载了哪些XML,以及每个@MapperScan绑定的是哪个FactoryBean。推荐的做法是把Mapper接口按数据源分包:
com.example.mapper.primary -> 主库FactoryBean com.example.mapper.secondary -> 从库FactoryBean然后每组单独声明@MapperScan,指定各自的sqlSessionTemplateRef。在这个结构下,某个包里的Mapper报"statement not found",问题范围就能缩小到对应那个FactoryBean上,不会出现两个数据源互相甩锅的局面。
4.4 configLocation与configuration属性打架:另一种"配置无效"的坑
还有一种情况不报错,但更折磨人:XML和Configuration对象同时配置,结果某一个配置"莫名失效"。典型例子是代码里设置mapUnderscoreToCamelCase=true,XML里又没有这一项,最终行为却表现为XML优先,代码设置没生效。实际原因是构建时XML解析结果会覆盖手工设置。
这类坑的核心理念就一句话:同一个配置项,永远只留一个来源。我在工程里看到同时出现configLocation和configuration属性的写法,第一件事就是建议同事二选一。不要试图利用"覆盖顺序"玩出花活,那只会给人留下一个定时炸弹。
5. 事务、批量执行与Spring Boot:三个容易想岔的地方
5.1 事务不生效,八成不是@Transactional的问题
事务不生效时,很多人的第一反应是检查@Transactional注解位置、异常类型。但放在MyBatis场景下,还有三个跟SqlSessionFactoryBean直接相关的原因。
第一,transactionFactory被改成了JDBC事务工厂,导致MyBatis的连接脱离了Spring事务管理,这个前面已经说过。第二,Spring容器里要么没注册DataSourceTransactionManager,要么注册时用的dataSource和SqlSessionFactoryBean里的不是同一个实例。常见于自己new了一个DataSource,结果FactoryBean和事务管理器各用各的副本,事务管理器提交了,MyBatis那边连接又被关了。
第三,@Transactional标注的方法被同类内部调用,代理没有生效。这个跟SqlSessionFactoryBean无关,但排查时很容易和上面两个原因混在一起。判断方法很简单:在Service方法入口断点,看当前对象是不是被Spring代理过的CGLIB类。
MyBatis场景下的事务排查顺序,我建议这样:先确认transactionFactory是Spring托管的;再确认事务管理器和FactoryBean共用同一个DataSource;最后才去查注解和调用方式。
5.2 ExecutorType选择与批量插入的落地写法
SqlSessionFactoryBean构建出的Configuration有一个defaultExecutorType,取值分别是SIMPLE、REUSE、BATCH,默认是SIMPLE。字面意思不复杂:SIMPLE每条语句都新建PreparedStatement,REUSE复用同一条语句的PreparedStatement,BATCH把多条更新语句攒成批量。
实际项目里,很少有人会把全局默认类型改成BATCH,因为查询也会走批量执行器,行为会很奇怪。更合理的做法是:保持默认SIMPLE,需要批量插入时单独创建一个绑定BATCH执行器类型的SqlSessionTemplate:
SqlSessionTemplate batchSqlSession = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); UserMapper mapper = batchSqlSession.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } batchSqlSession.flushStatements();注意flushStatements()这步很关键,BATCH模式不会像SIMPLE那样立即执行,需要主动刷新才会把累积的批次真正发送给数据库。一旦忘记刷新,数据会迟迟不落库,排查起来比报错还痛苦。另外,批量SqlSession要独立管理,用完关闭,别混进普通请求链路里。
5.3 Spring Boot里的"等价但又不完全等价"
Spring Boot项目里通常看不到SqlSessionFactoryBean,因为启动器自动配置了它。MybatisAutoConfiguration做的事情和手写几乎一样:构建SqlSessionFactoryBean、设置数据源和mapperLocations、获取SqlSessionFactory,然后注入SqlSessionTemplate。这也是为什么很多人第一次做多数据源时,会惊讶地发现"Boot里竟然也要手写FactoryBean"。
一个容易被忽略的差异是:Boot自动配置带有条件判断,如果你自己定义了一个@Bean SqlSessionFactory,自动配置通常会让位给你自定义的那个。这既是好事也是隐患,尤其是在你只定义了一半配置、另一半还在依赖自动配置的情况下,行为可能和你预想的不同。
我的经验是:单数据源项目尽量用Boot自动配置,少手写;多数据源或需要深度定制时才手写SqlSessionFactoryBean,并且一旦手写,就要把数据源、事务管理器、SqlSessionTemplate这一整套链条全部接管清楚。
5.4 多数据源下的FactoryBean隔离策略
多数据源配置的规范套路并不复杂,核心就是隔离。每个数据源对应一个SqlSessionFactoryBean,对应一个SqlSessionTemplate,Mapper接口按包分开,各自绑到对应的Template上。
@Bean("primarySqlSessionFactory") public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/primary/**/*.xml")); return factoryBean.getObject(); } @Bean("primarySqlSessionTemplate") public SqlSessionTemplate primarySqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } @MapperScan(basePackages = "com.example.mapper.primary", sqlSessionTemplateRef = "primarySqlSessionTemplate")同一个类的重复配置换DataSource即可。这里最需要注意的是:mapperLocations的路径不能重叠,两个FactoryBean不要扫描同一个XML文件目录,否则会重复注册MappedStatement,启动时会报"already contains value for"之类的冲突,非常难排查。我建议从目录上就彻底分开,比如mapper/primary/和mapper/secondary/,之后维护时也一目了然。
6. 进阶配置心得:把SqlSessionFactoryBean配置成顺手的形状
把标准属性配对之后,还有几个能明显提升开发体验、减少排查时间的配置建议。这节内容不是必选项,但都是我实际用下来觉得值得保留的实践。
6.1 基础settings里最值得开的两个开关
第一个是mapUnderscoreToCamelCase。数据库字段习惯用user_name,Java对象属性习惯用userName,不开启这个映射的话,查询结果集中下划线字段无法自动映射到驼峰属性,你会看到一堆null。明明数据在数据库里放着,Java对象里拿不到,这个问题很常见。配置方式可以是:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean>第二个是lazyLoadingEnabled。开启后,关联查询的嵌套对象会按需加载。配合aggressiveLazyLoading使用时稍麻烦,我一般保留lazyLoadingEnabled=true,把aggressiveLazyLoading关掉,避免一次查询把不该加载的关联对象全部拉出来。这两个开关的取舍没有绝对标准,取决于你的查询模型,但了解它们的存在,至少调优时知道往哪个方向走。
6.2 插件注册顺序:拦截器生效的层级与实测
plugins属性接收Interceptor数组,数组顺序就是MyBatis责任链的包装顺序。分页插件、数据权限插件、SQL审计插件之间如果有依赖,注册顺序会直接影响执行顺序。我踩过的一个坑是:想在自定义插件里打印最终执行的SQL,拦截Executor.query()时拿到的只是原始SQL,分页插件还没拼上LIMIT、参数还没展开,打印出来根本不是真正发给数据库的那条。后来把拦截目标改成PreparedStatementHandler.processParameterized(),才拿到带占位符预处理后的完整执行语句。
这个体会说明:插件注册进SqlSessionFactoryBean只是第一步,真正工作时要清楚MyBatis四大对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的调用层次。如果你想追踪实际SQL,去拦截StatementHandler层会更有用;如果你想控制事务边界,才需要碰Executor层。实践时可以先写一个只打日志的测试插件,一步步验证插件是否按预期拦截,再上正式逻辑。
6.3 大项目里的三个建议:分环境、启动校验、统一管理
工程变大之后,SqlSessionFactoryBean的配置往往不再是一行代码的事,它牵扯到环境、模块、多数据源多个维度。我有三个经验想分享。
第一,分环境控制配置。开发、测试、生产环境的数据库不同,mapperLocations可以保持一致,但数据源信息和一些开关型配置要用环境变量或配置中心区分。千万别把某个环境的数据库密码直接写死在SqlSessionFactoryBean附近的代码里,这是很多事故的源头。
第二,启动时做一次"对账"。加载了多少Mapper XML、注册了多少MappedStatement,最好在启动日志里能看到。很多项目起步时没问题,后来模块多了、路径改了几次,某个文件夹没有被classpath*:扫到,启动日志里没有明显报错,但线上接口慢慢开始报"statement not found"。
第三,统一约定管理。如果团队里有多个工程师各自往里加Mapper XML,最好约定文件命名和存放目录,避免同样的XML被两个模块重复扫描。这类问题在模块拆分时最容易发生,而且越晚发现越难查。
6.4 一个启动期的自检小技巧
最后分享一个我每次改完SqlSessionFactoryBean配置后都会跑的启动自检。在配置类里临时加一个ApplicationRunner,把当前工厂里真正注册的MappedStatement数量打印出来:
@Bean public ApplicationRunner mapperCheckRunner(SqlSessionFactory sqlSessionFactory) { return args -> { long count = sqlSessionFactory.getConfiguration().getMappedStatements().stream() .filter(s -> s.getId().contains("com.example.mapper")) .count(); System.out.println("[mybatis] registered mapper statements size = " + count); }; }如果数量和Mapper方法总数对不上,说明有XML没被加载,或者某个接口只有方法没有对应SQL,等到运行期才暴露的问题,这一下就在启动阶段拦住了。有一次我在一个多模块工程里排查线上偶发的"statement not found",就是这个自检日志帮我定位到了漏加载的Mapper文件夹。从此以后,凡是手写SqlSessionFactoryBean的工程,我都会保留这个Runner,习惯成自然,省下过不少定位时间。