Spring Boot 加 MyBatis 大概是Java后端最常见的组合之一,但大多数时候我们只是在写Mapper接口、XML文件,然后Service里直接注入。真正驱动这一切的,是包里那几个不起眼的类,其中两个最关键:MybatisAutoConfiguration和AutoConfiguredMapperScannerRegistrar。我之前很长一段时间也只停留在“会用”的层面,直到有一次遇到Mapper扫描失效的问题,被逼着把源码翻了一遍,才把这套自动装配的逻辑完整串起来。这篇文章想把这两条主线的原理讲清楚,包括 Spring Boot 是怎么知道要加载MyBatis配置的、SqlSessionFactory在哪一步被创建、Mapper接口又是在什么时机变成可注入的Bean。适合已经写过 Spring Boot + MyBatis 项目、但想深入理解底层逻辑的开发者,也适合正在准备面试、想把这个经典机制讲明白的同学。
1. 从自动配置到MyBatis的装配主线
1.1 Spring Boot 是怎么把第三方组件拉进来的
Spring Boot 最核心的魔法是@EnableAutoConfiguration。它不是一个简单的注解,背后是一个ImportSelector,会去classpath下寻找所有自动配置类的名字,然后交给Spring容器去实例化。老版本里这些名字写在META-INF/spring.factories文件里,Key是org.springframework.boot.autoconfigure.EnableAutoConfiguration;新版本,比如Spring Boot 3.x,则改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,每一行一个自动配置类的全限定名。
这个机制决定了:只要你的classpath里有某个starter的jar包,Spring Boot启动的时候就会自动把这些配置类当成普通@Configuration类处理。注意,这里说的是“当成普通配置类处理”,并不意味着里面所有Bean都会创建。每个自动配置类内部通常都有大量条件注解,比如@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等。这些条件才是真正的闸门。
所以,当你引入mybatis-spring-boot-starter时,真正起作用的并不是starter本身,而是它依赖的mybatis-spring-boot-autoconfigure包。这个包里的MybatisAutoConfiguration会被Spring Boot通过上述机制加载,再根据当前应用的条件决定要不要创建SqlSessionFactory和SqlSessionTemplate。
1.2 为什么MyBatis需要单独做自动配置
在Spring里使用MyBatis,原本需要你手动做几件事:创建SqlSessionFactoryBean、传入DataSource、设置 XML 位置、注册SqlSessionTemplate、再配置 Mapper 扫描器。这一套操作每个项目几乎都是重复的,而且很容易出错。
如果没有自动配置,你在每个Spring Boot项目里都要写一段类似下面这样的代码:
@Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return factoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }这还只是最基础的两个Bean。如果你要配置驼峰映射、类型别名、插件拦截器,代码会更多。自动配置的价值就在于此:它把整个MyBatis容器化过程标准化,同时通过条件注解给用户留下覆盖口子。你想要的做得更精细,可以替换掉它创建的Bean;你没有特殊需求,它直接用一套合理默认值把事情做完。
1.3 Starter包结构带来的依赖顺序
mybatis-spring-boot-starter本身几乎不写代码,它的核心作用就是传递依赖。它同时依赖mybatis-spring-boot-autoconfigure、mybatis和mybatis-spring,让你无需手动管理版本。有一点容易踩坑:mybatis-spring-boot-starter的版本必须和Spring Boot版本兼容。比如Spring Boot 3.x对应mybatis-spring-boot-starter3.x,它基于Jakarta EE规范;Spring Boot 2.x则对应2.x。如果你把2.x的starter塞进3.x工程,会碰到javax和jakarta包名冲突,启动时直接NoClassDefFoundError。
我见过不少人项目里同时出现多个版本的mybatis相关jar,最后SqlSessionFactory构造器里报ClassNotFoundException。排包时的原则很简单:只保留一个starter,让它统一管理版本,不要额外引入零散的mybatis-spring包。
2. MybatisAutoConfiguration:条件装配的艺术
2.1 生效条件逐个看
打开MybatisAutoConfiguration源码,类上挂着好几个注解,很多人都没仔细看过,但每一个都在回答同一个问题:当前应用适不适合让MyBatis自动配置接管。
首先是@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })。这个条件要求classpath下必须同时存在MyBatis核心类SqlSessionFactory和Spring整合类SqlSessionFactoryBean。如果只是引入了mybatis但没有引入mybatis-spring,这个条件不成立,自动配置直接跳过。
然后是@ConditionalOnSingleCandidate(DataSource.class)。这个条件比较有意思,它要求容器中DataSource类型的候选Bean只有一个,或者有且只有一个被@Primary标记。为什么这么设计?因为SqlSessionFactory必须绑定一个数据源,如果容器里有多个DataSource且没有明确主选,自动配置就不知道该用哪个。很多多数据源项目启动时报DataSource相关错误,根源往往在这里。
接着是@EnableConfigurationProperties(MybatisProperties.class),它把配置文件中mybatis.*前缀的内容绑定到属性类。例如mybatis.mapper-locations、mybatis.type-aliases-package、mybatis.configuration.map-underscore-to-camel-case等。这个机制让你可以不写一行Java配置,完全靠配置文件驱动MyBatis。
最后还有@AutoConfigureAfter(DataSourceAutoConfiguration.class),确保MybatisAutoConfiguration在数据源自动配置完之后再执行。如果数据源都还没有,创建SqlSessionFactory就无从谈起。
这些条件组合在一起形成的效果是:只有当Spring Boot认为MyBatis基础设施完整、数据源唯一且可用、没有其他Bean需要优先接管时,自动配置才真正生效。
2.2 SqlSessionFactory的创建过程
当条件全部满足后,MybatisAutoConfiguration会执行一个核心方法,方法名一般就叫sqlSessionFactory。它接收DataSource参数,这个参数由Spring容器自动注入,注入的就是前面单候选条件筛选出的数据源。
方法内部第一件事是new SqlSessionFactoryBean()。这个类并不是MyBatis核心库里的,而是mybatis-spring提供的桥接类。它实现了Spring的FactoryBean和InitializingBean接口,能够在Spring Bean生命周期中构建真正的SqlSessionFactory。
随后,自动配置会把属性对象里的值一条条设置进去:
setDataSource(dataSource):绑定数据源。setTypeAliasesPackage(properties.getTypeAliasesPackage()):设置实体类别名扫描包。setTypeHandlersPackage(properties.getTypeHandlersPackage()):设置类型处理器扫描包。setMapperLocations(properties.resolveMapperLocations()):加载XML映射文件路径。setConfiguration(properties.getConfiguration()):如果用户通过mybatis.configuration设置了大量全局参数,直接设置整个Configuration对象。- 还会把容器中收集到的
Interceptor数组、DatabaseIdProvider、ConfigurationCustomizer等扩展点注入。
设置完成后,调用factoryBean.afterPropertiesSet(),让SqlSessionFactoryBean内部完成XML解析、插件加载、类型别名注册等动作。最后通过factoryBean.getObject()返回真正的SqlSessionFactory。
这里有个细节很多人不知道:mybatis.mapper-locations如果不配置,自动配置并不会帮你加载任何XML。意味着如果你的SQL写在XML里,就会在运行时报Invalid bound statement (not found)。常见的默认路径classpath:mapper/*.xml也需要你显式写出来。所以一个合格的项目里,mybatis.mapper-locations通常是必须配置的。不过MyBatis也支持注解SQL,那种情况可以不用XML。
2.3 SqlSessionTemplate 与 Mapper 执行
SqlSessionTemplate是SqlSessionFactory的同胞兄弟。MybatisAutoConfiguration中会继续创建它:
@Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }SqlSessionTemplate实现了MyBatis的SqlSession接口,但它是线程安全的。原生DefaultSqlSession不是线程安全的,它代表一次数据库连接会话,不能在多线程环境共享。Spring辅助包用一个动态代理包裹了真实的SqlSession创建逻辑:每次调用SQL操作时,都会从Spring容器或事务同步管理器里取得一个真正的SqlSession,执行完提交或关闭。
正是这个SqlSessionTemplate在后面支撑了Mapper代理的运行。当你从Spring容器中取出Mapper接口时,实际执行逻辑都会经过它。理解了这条链路,再回头看为什么MyBatis和Spring整合后不需要手动管理SqlSession,就顺理成章了。
2.4 外部扩展点与自定义覆盖
MybatisAutoConfiguration不是一堵密不透风的墙,它预留了几个扩展口子。
第一,SqlSessionFactory和SqlSessionTemplate的方法都标注了@ConditionalOnMissingBean。这意味着你只要在某个配置类里自己定义同名Bean,自动配置就会退让。比如你想把SqlSessionFactoryBean换成基于MyBatis-Plus的实现,或者想给Configuration加自定义对象工厂,就可以自己新建一个SqlSessionFactory,自动配置不会再覆盖。
第二,拦截器可以被自动收集。MybatisAutoConfiguration的构造器会接收一个ObjectProvider<Interceptor[]>,然后把这些拦截器统一设置到SqlSessionFactoryBean的plugins属性中。所以你只需要在Spring容器中声明@Bean返回一个Interceptor实现类,MyBatis在执行SQL时就会自动应用这个插件。这种自动收集的体验非常Spring Boot,但同样有陷阱:如果同一个插件被配置成多个实例,或者拦截器本身有状态,可能会出现重复执行的问题。
第三,ConfigurationCustomizer是专门留给配置后处理的接口。实现它可以拿到已经初始化好的org.apache.ibatis.session.Configuration对象,在里面补充自定义逻辑,比如注册TypeHandler、修改缓存策略等。这个API解决了很多“我想在自动配置基础上微调”的场景。
还有一类覆盖场景需要注意:配置文件里的mybatis.configuration和mybatis.config-location不能同时使用。config-location是让你指定一个独立的mybatis-config.xml配置文件,configuration是让你用Spring Boot的properties直接配置。两者都会尝试设置Configuration对象,同时配置会引发冲突,启动阶段MybatisAutoConfiguration会直接抛异常提醒你。
3. AutoConfiguredMapperScannerRegistrar:Mapper接口如何变成Bean
3.1 注册入口与内部类结构
MybatisAutoConfiguration里面还藏着一个静态内部类MapperScannerRegistrarNotFoundConfiguration,它的定义大致如下:
@Configuration @Import(AutoConfiguredMapperScannerRegistrar.class) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { @Override public void afterPropertiesSet() { // 如果这里没有被跳过,说明没有手动配置Mapper扫描 } }这个内部类的名字很有意思:MapperScannerRegistrarNotFoundConfiguration,翻译过来就是“当没找到Mapper扫描注册器时,用一个后备配置顶上”。它是一个条件配置类,当Spring容器中不存在MapperFactoryBean或MapperScannerConfigurer时,才导入AutoConfiguredMapperScannerRegistrar。如果用户已经通过@MapperScan或XML方式配置了扫描器,这个内部类就不会导入自动扫描逻辑,从而避免重复注册Mapper。
AutoConfiguredMapperScannerRegistrar的父类是MapperScannerRegistrar,这是一个实现了ImportBeanDefinitionRegistrar的类。Spring在处理@Import注解时,如果发现导入的是一个ImportBeanDefinitionRegistrar,就会调用它的registerBeanDefinitions方法,让它在运行时主动向容器注册新的BeanDefinition。这是Spring一个非常重要的扩展入口,MyBatis的自动扫描就是从这里开始的。
3.2 拿到启动类所在包
AutoConfiguredMapperScannerRegistrar要扫描Mapper接口,第一步必须知道自己该扫哪个包。它的默认策略是:启动类所在的包及其子包。
这个信息从哪来?答案是AutoConfigurationPackages。当你使用@SpringBootApplication注解启动应用时,这个组合注解中包含了@AutoConfigurationPackage,Spring Boot会注册一个AutoConfigurationPackages.Registrar,把当前注解所在类(也就是启动类)的包名记录下来。这个包名会以一个内部Bean的形式存在容器中,后续任何组件都可以通过AutoConfigurationPackages.get(beanFactory)拿到。
AutoConfiguredMapperScannerRegistrar实现了BeanFactoryAware接口,所以它能拿到BeanFactory实例,然后调用:
AutoConfigurationPackages.get(this.beanFactory)返回的是一个String数组,通常只有启动类所在包这一个元素。拿到这个包名后,它再创建一个MapperScannerConfigurer的BeanDefinition,并把该包名作为属性传进去。这样,默认扫描范围就被确定下来了。
这里有个非常常见的坑:如果你的Mapper接口放在启动类所在包之外,比如启动类在com.example.app,而Mapper在com.example.mapper,自动扫描是扫不到的。因为自动扫描只盯着com.example.app及其子包。很多人遇到这种情况的第一反应是给启动类加@MapperScan("com.example.mapper"),这确实能解决,但背后真正的原因是自动扫描范围已经被启动类位置锁死了。
3.3 MapperScannerConfigurer 的扫描动作
MapperScannerConfigurer是mybatis-spring包里的核心类,它实现了BeanDefinitionRegistryPostProcessor。这个接口允许它在Spring容器已经注册了用户自定义BeanDefinition之后、Bean实例化之前,再向容器注册一批新的BeanDefinition。
它的执行过程大致是:
- Spring容器启动前期,
ConfigurationClassPostProcessor处理完所有配置类后,会触发所有BeanDefinitionRegistryPostProcessor。 MapperScannerConfigurer.postProcessBeanDefinitionRegistry()被调用。- 它构建一个
ClassPathMapperScanner,设置好basePackage、annotationClass、markerInterface等属性。 - 调用
scanner.scan(basePackage),扫描指定包下所有符合条件的接口。 - 对每个接口,生成一个
BeanDefinition,beanClass 设置为MapperFactoryBean,同时在构造器参数或属性中记录该接口类型。 - 最后把这个BeanDefinition注册到容器,beanName默认是接口名首字母小写。
这里有一步值得展开:为什么扫描器扫描到的是接口,而不是普通类?因为MyBatis的Mapper接口不能直接实例化,它需要被MapperFactoryBean包装。MapperFactoryBean是Spring的FactoryBean,容器在获取该Bean时,调用它的getObject()方法返回一个真正的Mapper代理对象。所以容器里每个Mapper对应的是一个MapperFactoryBean,而不是Mapper本身。你注入Mapper接口时,Spring通过工厂Bean的泛型信息知道生产出来的对象适合哪个接口。
3.4 动态代理生成过程
MapperFactoryBean继承了SqlSessionDaoSupport,在它的afterPropertiesSet方法里会强制要求SqlSession非空,并在getObject()方法中执行:
return getSqlSession().getMapper(this.mapperInterface);这里的SqlSession实际就是前面创建的SqlSessionTemplate。SqlSessionTemplate.getMapper()最终会调用Configuration.getMapper(Class, SqlSession),而Configuration内部维护了一个MapperRegistry,里面保存了每个Mapper接口对应的MapperProxyFactory。
MapperProxyFactory.newInstance()会利用JDK动态代理创建代理对象:
Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, new MapperProxy<>(sqlSession, mapperInterface, methodCache));MapperProxy实现了InvocationHandler。当你调用Mapper接口的任意一个方法时,invoke()方法会被触发。它先把接口方法转换为MappedStatementId,查找MyBatisConfiguration中对应的MappedStatement,然后根据SQL操作类型调用SqlSession的selectOne、selectList、insert、update、delete等方法。
这也是为什么MyBatis的Mapper必须是接口而不能是普通类。JDK动态代理只能代理接口,无法代理没有抽象方法的普通类。如果你用CGLIB确实可以代理类,但MyBatis的整个设计都围绕接口展开,强行改成类会让你失去很多便利。
3.5 @MapperScan 和默认扫描器如何共存
有读者会问:@MapperScan也是做同样的事情,那它和AutoConfiguredMapperScannerRegistrar会不会冲突?答案是通常情况下不会,这正是条件注解发挥的作用。
当你使用@MapperScan时,该注解内部会@Import(MapperScannerRegistrar.class),而MapperScannerRegistrar会在容器中注册一个MapperScannerConfigurer。既然容器里已经有MapperScannerConfigurer了,那上面提到的@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})就会生效,自动扫描器被跳过。
所以这两个机制是互斥的:要么用系统默认的自动扫描,要么用你手动指定的@MapperScan,不会同时扫描两遍。从代码维护角度看,你也没必要同时使用这两种方式,显式指定反而更可控。
4. 排查与实战:让原理落地
4.1 启动自动配置报告
原理说得再多,最后还是要落到实际项目里。遇到MyBatis相关启动问题时,我建议先看Spring Boot的自动配置报告。在application.properties或application.yml中加一行:
debug=true启动后控制台会打印类似:
Positive matches: MybatisAutoConfiguration matched: - @ConditionalOnClass found required classes 'org.apache.ibatis.session.SqlSessionFactory', 'org.mybatis.spring.SqlSessionFactoryBean' - @ConditionalOnSingleCandidate found a primary candidate 'dataSource' - @ConditionalOnMissingBean found no 'SqlSessionFactory' bean Negative matches: MybatisAutoConfiguration: - @ConditionalOnClass did not find required class 'org.apache.ibatis.session.SqlSessionFactory'这个报告会把所有自动配置类的匹配和未匹配原因逐条列出来。你一眼就能看出MybatisAutoConfiguration是生效了还是被跳过了,以及是哪一环条件没满足。这个信息远比你在代码里加日志排查要快。
另外,Spring Boot还提供了autoconfigure端点,可以通过Actuator查看,不过对日常排障来说debug=true足够了。
4.2 Mapper扫描不到的典型场景
我当年踩过的一个坑,现在总结下来就是因为启动类包位置导致默认扫描失败。项目结构是这样的:
- 启动类:
com.xxx.web.Application - Mapper接口:
com.xxx.dal.mapper.UserMapper - Service:
com.xxx.biz.service.UserService
启动类在web包下,默认Mapper扫描只会扫com.xxx.web以及它的子包。com.xxx.dal.mapper不在扫描范围内,容器里根本不存在UserMapper对应的Bean。当时项目里Service注入UserMapper一直报NoSuchBeanDefinitionException,我第一反应是加@MapperScan("com.xxx.dal.mapper"),加了确实就好了。但当时没想明白为什么默认扫描不生效,直到后来看了源码。
还有另一种容易搞混的场景:启动类上的@SpringBootApplication(scanBasePackages = "com.xxx")看起来把扫描范围扩大了,但scanBasePackages只影响Spring组件扫描,不影响AutoConfigurationPackages记录的包。所以如果Mapper接口恰好在scanBasePackages范围内、启动类包之外,依然可能扫不到。遇到这种情况,最清晰的做法就是显式给启动类加上@MapperScan,指定你实际的Mapper包路径。这也是许多项目规范里要求每个工程必须显式配置@MapperScan的原因。
4.3 多数据源场景的自动配置失效
多数据源是MybatisAutoConfiguration最常见的失效场景。因为@ConditionalOnSingleCandidate(DataSource.class)要求容器中只有一个候选DataSource。如果你配置了两个数据源,且都没有用@Primary标记,自动配置认为无法决定,直接跳过,项目启动时连SqlSessionFactory都找不到。
解决办法有三种:
- 指定一个主数据源,加上
@Primary。这样@ConditionalOnSingleCandidate看到的是唯一主候选,自动配置继续生效。 - 手动创建
SqlSessionFactory,把两个数据源分别配置成两个SqlSessionFactoryBean,并分别指定扫描器。 - 如果你只需要MyBatis操作其中一个库,而另一个库仅是辅助,就只让自动配置使用主数据源,辅助库用其他框架访问。
我这里更推荐第一种和第二种结合。如果两个库都需要MyBatis,那自动配置已经不适合你了,你必须手动接管。手动接管的模范写法是:为每个数据源写一套独立的SqlSessionFactoryBean、SqlSessionTemplate和MapperScannerConfigurer,各自指定SQL SessionTemplate和Mapper包。这样虽然代码多一点,但逻辑清晰,不会出现“明明配了但MySQL和Oracle的SQL乱串”的诡异问题。
4.4 常见问题速查表
下面整理了一张速查表,基本覆盖日常开发常见问题,都是我实际调试中遇到过的:
| 异常现象 | 常见原因 | 解决办法 |
|---|---|---|
启动报No qualifying bean of type 'SqlSessionFactory' | 多数据源导致自动配置过期 | 为数据源加@Primary或手动创建SqlSessionFactory |
Service注入Mapper报NoSuchBeanDefinitionException | Mapper接口不在默认扫描包内 | 显式配置@MapperScan并指向Mapper包 |
启动正常,调用Mapper时报Invalid bound statement | XML路径没配置 | 设置mybatis.mapper-locations: classpath:mapper/**/*.xml |
出现BindingException: Mapper method ... has no SQL | 注解SQL写错位置或XMLStatement未加载 | 检查方法名与StatementId是否一致、XML路径是否匹配 |
java.lang.ClassCastException代理转接口失败 | Mapper定义成了普通类 | 改回接口 |
| 同一个Mapper方法被拦截器执行两次 | 自定义拦截器注册了多个实例 | 检查Spring容器中InterceptorBean数量 |
configuration和config-location同时配置启动报错 | 两种方式冲突 | 只保留一种 |
mybatis-plus和mybatis-spring版本不兼容 | 包冲突 | 统一用官方starter管理依赖,排除多余传递依赖 |
5. 扩展:从零复刻一个精简的Mapper扫描器
5.1 自定义一个开启扫描的注解
理解了上面的原理后,可以动手写一个极简版扫描器来验证思路。我们不引入MyBatis,只用Spring的ClassPathBeanDefinitionScanner和JDK动态代理,模拟“扫描接口→注册FactoryBean→生成代理”的完整过程。
先定义一个注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(MyMapperScannerRegistrar.class) public @interface EnableMyMapperScan { String[] basePackages() default {}; }这个注解的作用类似@MapperScan,它通过@Import引入注册器。
5.2 实现 ImportBeanDefinitionRegistrar
接下来实现一个ImportBeanDefinitionRegistrar,在类中获取@EnableMyMapperScan的属性值。如果用户没指定包名,就自动取启动类所在包。这个逻辑可以通过AutoConfigurationPackages获得,可以顺便体会一下Spring Boot在这方面的便利。
public class MyMapperScannerRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { Map<String, Object> attributes = importingClassMetadata .getAnnotationAttributes(EnableMyMapperScan.class.getName()); String[] basePackages = (String[]) attributes.get("basePackages"); if (basePackages.length == 0 && registry instanceof BeanDefinitionRegistry) { // 获取AutoConfigurationPackages,取第一个包名 ... } // 创建一个扫描器,在指定包下扫出所有接口 ... // 为每个接口注册一个MyMapperFactoryBean的BeanDefinition } }关键在于怎么为接口注册BeanDefinition。正常注册接口会被Spring拒绝,因为接口无法实例化。但如果给它一个FactoryBean,让FactoryBean来生产对象,Spring就不关心目标对象到底是不是接口了。
5.3 用FactoryBean和JDK代理完成动态代理
定义MyMapperFactoryBean<T>,实现FactoryBean<T>:
public class MyMapperFactoryBean<T> implements FactoryBean<T> { private Class<T> mapperInterface; @Override public T getObject() { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[]{mapperInterface}, (proxy, method, args) -> { System.out.println("执行Mapper方法: " + method.getName()); return null; }); } @Override public Class<?> getObjectType() { return mapperInterface; } @Override public boolean isSingleton() { return true; } }然后把这个MyMapperFactoryBean注册到BeanDefinition中,并通过constructorArgs设置mapperInterface。这样容器中每个Mapper接口都对应一个MyMapperFactoryBean,你注入接口时得到的就是JDK代理对象。
亲手跑一遍这个Demo后,你会对MapperFactoryBean、ImportBeanDefinitionRegistrar和FactoryBean这三者的配合有非常直观的认识。比如你会看到Spring在启动时如何打印“执行Mapper方法: xxx”,也会发现在代理对象的invoke方法里返回null并不会导致接口调用失败,除非调用方真的拿返回值做业务操作。
5.4 这个Demo能帮你理清什么
这个简化版没有实现SQL解析和执行,但它复刻了MyBatis-Spring整合中最核心的“接口即Bean”魔法。很多面试题其实就在考这个链路:
- Spring容器怎么把一个接口注册成一个Bean?
FactoryBean在Spring里扮演什么角色?- 条件注解和
ImportBeanDefinitionRegistrar的优先级和触发时机? - JDK动态代理和CGLIB在这个场景中为什么必须选前者?
真正读懂这段链路后,再去看AutoConfiguredMapperScannerRegistrar的源码,你就能把每一行代码和Spring容器生命周期对应起来,而不是死记硬背。
我自己在实际调试中最深的体会是:不要怕读源码,也不要想着一次把所有细节都读完。先关注两条主线,一条是SqlSessionFactory怎么来的,一条是Mapper接口怎么变成代理对象。把这两条主线串起来,遇到大多数MyBatis集成问题,你已经能很快定位方向。建议你在自己的项目里试一下debug=true看自动配置报告,下次再遇到Mapper相关报错,先看报告再动手,效率会高很多。