☰
MyBatis与Spring整合的幕后基石:SqlSessionFactoryBean配置与排查详解
2026/10/15 15:24:36 网站建设 项目流程

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整合方式放在一起看:

对比维度原生 MyBatisSqlSessionFactoryBean 整合
SqlSessionFactory 生命周期由应用代码自行持有由Spring容器创建并托管单例
Session 管理手动 openSession / closeSqlSessionTemplate 自动管理
事务控制代码里手动 commit / rollback接入 Spring 声明式事务
Mapper 获取session.getMapper()注入 MapperFactoryBean 代理对象
配置来源mybatis-config.xmlSpring 属性 + 可选的 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()方法里,执行顺序大致如下:

  1. 如果configuration属性为空,先创建一个全新的Configuration对象。
  2. 如果指定了configLocation,用XMLConfigBuilder解析XML,把解析结果填充进Configuration。这里会覆盖掉之前手动设置的许多项,所以要记住XML优先。
  3. 设置Environment,包括数据源和事务工厂。
  4. 处理typeAliasesPackage、typeHandlersPackage、plugins,把它们注册进Configuration。
  5. 解析mapperLocations,逐个加载Mapper XML文件,把里面的SQL解析成MappedStatement注册进去。
  6. 最终返回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,习惯成自然,省下过不少定位时间。

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

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

立即咨询