news 2026/10/15 12:06:01

Spring Boot与MyBatis自动装配原理:从SqlSessionFactory到Mapper扫描

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot与MyBatis自动装配原理:从SqlSessionFactory到Mapper扫描

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。

它的执行过程大致是:

  1. Spring容器启动前期,ConfigurationClassPostProcessor处理完所有配置类后,会触发所有BeanDefinitionRegistryPostProcessor。
  2. MapperScannerConfigurer.postProcessBeanDefinitionRegistry()被调用。
  3. 它构建一个ClassPathMapperScanner,设置好basePackage、annotationClass、markerInterface等属性。
  4. 调用scanner.scan(basePackage),扫描指定包下所有符合条件的接口。
  5. 对每个接口,生成一个BeanDefinition,beanClass 设置为MapperFactoryBean,同时在构造器参数或属性中记录该接口类型。
  6. 最后把这个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都找不到。

解决办法有三种:

  1. 指定一个主数据源,加上@Primary。这样@ConditionalOnSingleCandidate看到的是唯一主候选,自动配置继续生效。
  2. 手动创建SqlSessionFactory,把两个数据源分别配置成两个SqlSessionFactoryBean,并分别指定扫描器。
  3. 如果你只需要MyBatis操作其中一个库,而另一个库仅是辅助,就只让自动配置使用主数据源,辅助库用其他框架访问。

我这里更推荐第一种和第二种结合。如果两个库都需要MyBatis,那自动配置已经不适合你了,你必须手动接管。手动接管的模范写法是:为每个数据源写一套独立的SqlSessionFactoryBean、SqlSessionTemplate和MapperScannerConfigurer,各自指定SQL SessionTemplate和Mapper包。这样虽然代码多一点,但逻辑清晰,不会出现“明明配了但MySQL和Oracle的SQL乱串”的诡异问题。

4.4 常见问题速查表

下面整理了一张速查表,基本覆盖日常开发常见问题,都是我实际调试中遇到过的:

异常现象常见原因解决办法
启动报No qualifying bean of type 'SqlSessionFactory'多数据源导致自动配置过期为数据源加@Primary或手动创建SqlSessionFactory
Service注入Mapper报NoSuchBeanDefinitionExceptionMapper接口不在默认扫描包内显式配置@MapperScan并指向Mapper包
启动正常,调用Mapper时报Invalid bound statementXML路径没配置设置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相关报错,先看报告再动手,效率会高很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:05:42

ECharts世界地图js资源:GeoJSON注册与飞线配置实战指南

简介&#xff1a;面向需要在地图上展示全球数据的前端开发者与数据可视化爱好者&#xff0c;这份zip压缩包提供了一套可直接集成的ECharts世界地图资源&#xff0c;合计77个文件&#xff0c;其中39个JS文件用于图表逻辑与交互控制&#xff0c;38个JSON文件保存了GeoJSON格式的国…

作者头像 李华
网站建设 2026/10/15 12:05:32

CodeGraph实操:从零剖析大型工程的模块依赖与循环依赖

1. CodeGraph帮我解决了什么真实痛点1.1 从"读代码头痛"到"先看地图"先说个我亲身经历的场景。半年前我接手了一个为公司内部运营系统做维护的老项目&#xff0c;PHP为主&#xff0c;掺杂着几段Node.js写的命令行脚本&#xff0c;代码量大概十多万行。前任…

作者头像 李华
网站建设 2026/10/15 12:05:19

C++调用C# dll实战:C++/CLI桥接方案与避坑指南

简介&#xff1a;本资源面向需要在C工程中复用C#类库的开发者&#xff0c;聚焦跨语言互操作这一常见难题&#xff0c;提供一套可在Visual Studio 2010环境下直接运行的完整示例。内容围绕C#端定义公共类与导出方法、C端通过P/Invoke与GetProcAddress动态获取函数地址并调用展开…

作者头像 李华
网站建设 2026/10/15 12:05:17

STM32循迹避障小车代码实战:从传感器采样到电机差速的完整工程

简介&#xff1a;这份资源是面向嵌入式初学者与电子竞赛选手的STM32循迹避障小车完整工程代码&#xff0c;围绕路径跟踪与障碍物避障两大核心功能展开&#xff0c;适合已掌握C语言基础、希望从外设驱动进阶到综合项目实战的开发者。压缩包共223个文件&#xff0c;约7.83MB&…

作者头像 李华
网站建设 2026/10/15 12:04:19

参考资源如何成为团队知识库的基石?从筛选到维护

我翻出那本早年的技术书&#xff0c;"第32章&#xff1a;参考资源"几个字印在目录最后&#xff0c;很多读者都直接跳过它。做了多年开发后&#xff0c;我反而越来越看重这类章节——项目做不下去的时候&#xff0c;最后能拉你一把的不是某个高深算法&#xff0c;而是…

作者头像 李华