前言
在日常开发中,我们经常使用@MapperScan来自动扫描 MyBatis 的 Mapper 接口,例如:
@SpringBootApplication@MapperScan("com.demo.mapper")publicclassApp{}它可以自动将 Mapper 接口注册为 Spring Bean,避免逐个编写:
@MapperpublicinterfaceUserMapper{}很多人知道它“能用”,但不了解它内部原理,尤其是:
为什么 MyBatis-Spring 早期版本
@MapperScan是直接扫描 Mapper,后续版本却改成先注册MapperScannerConfigurer,再通过BeanDefinitionRegistryPostProcessor完成扫描?
本文就从源码角度,详细分析新版@MapperScan的执行原理与官方改造原因。
一、@MapperScan 注解入口分析
先看@MapperScan源码(简化):
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(MapperScannerRegistrar.class)public@interfaceMapperScan{String[]basePackages()default{};}核心就一句:
@Import(MapperScannerRegistrar.class)说明:
Spring 在启动解析配置类时,会导入
MapperScannerRegistrar。
也就是说:
@MapperScan ↓ @Import ↓ MapperScannerRegistrar二、旧版本原理(直接扫描)
早期版本MapperScannerRegistrar中逻辑比较直接:
publicvoidregisterBeanDefinitions(...){ClassPathMapperScannerscanner=newClassPathMapperScanner(registry);scanner.registerFilters();scanner.scan(basePackages);}执行流程:
Spring启动 ↓ 解析@MapperScan ↓ 执行MapperScannerRegistrar ↓ 立即扫描Mapper接口 ↓ 注册MapperFactoryBean例如:
publicinterfaceUserMapper{}会被注册成:
BeanName=userMapper BeanClass=MapperFactoryBean三、新版本原理(延迟扫描)
后续版本官方改造后,逻辑变成:
BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);也就是说:
MapperScannerRegistrar不再直接扫描 Mapper,而是先向 Spring 注册一个MapperScannerConfigurerBeanDefinition。
1. 注册流程图
@MapperScan ↓ MapperScannerRegistrar ↓ 注册 MapperScannerConfigurer ↓ Spring 容器刷新 ↓ 执行 postProcessBeanDefinitionRegistry() ↓ 真正扫描 Mapper2. 关键源码
MapperScannerRegistrar
@OverridepublicvoidregisterBeanDefinitions(...){BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);builder.addPropertyValue("basePackage","com.demo.mapper");registry.registerBeanDefinition("mapperScannerConfigurer",builder.getBeanDefinition());}MapperScannerConfigurer
publicclassMapperScannerConfigurerimplementsBeanDefinitionRegistryPostProcessor{@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry){ClassPathMapperScannerscanner=newClassPathMapperScanner(registry);scanner.scan(basePackage);}}四、为什么官方要这样改造?
这是本文重点。
五、原因一:解决${}占位符问题(最核心)
很多项目这样写:
@MapperScan("${mybatis.mapper.package}")配置文件:
mybatis:mapper:package:com.demo.mapper旧版本问题
旧版本直接扫描时机太早:
ImportBeanDefinitionRegistrar 执行阶段此时 Spring 的:
- Environment
- PropertySources
- Placeholder解析器
可能还没准备完成。
导致:
"${mybatis.mapper.package}"仍然是字符串本身,无法解析。
新版本优势
改成:
MapperScannerConfigurerimplementsBeanDefinitionRegistryPostProcessor执行时机更晚:
Spring 容器 refresh 阶段此时:
- application.yml 已加载
- 占位符可解析
- Environment 就绪
所以:
@MapperScan("${xx}")可以稳定运行。
六、原因二:更符合 Spring 生命周期设计
Spring 提供专门扩展点:
BeanDefinitionRegistryPostProcessor作用:
在 Bean 实例化前,动态注册 BeanDefinition。
Mapper 扫描本质就是:
扫描接口 → 注册BeanDefinition所以放在这个阶段最合理。
生命周期对比
旧版:
@Configuration解析阶段新版:
BeanDefinitionRegistryPostProcessor阶段显然新版更标准。
七、原因三:兼容更多配置项
后续 MyBatis-Spring 支持很多参数:
@MapperScan(lazyInitialization="true",sqlSessionFactoryRef="sqlSessionFactory",sqlSessionTemplateRef="sqlSessionTemplate")如果直接扫描:
很多依赖Bean还没准备好例如:
- SqlSessionFactory
- 多数据源
- BeanNameGenerator
- Scope
新架构先保存配置:
MapperScannerConfigurer等容器准备完成后再统一扫描,兼容性更强。
八、原因四:统一 XML 与 注解实现
早期 XML 写法:
<beanclass="org.mybatis.spring.mapper.MapperScannerConfigurer"><propertyname="basePackage"value="com.demo.mapper"/></bean>注解写法:
@MapperScan("com.demo.mapper")如果两者底层逻辑不同,维护成本高。
新版统一为:
XML配置 ↓ MapperScannerConfigurer @MapperScan ↓ MapperScannerConfigurer这样只维护一套扫描逻辑。
九、最终执行流程(新版完整链路)
1. Spring启动 2. 解析@Configuration 3. 发现@MapperScan 4. @Import 导入 MapperScannerRegistrar 5. Registrar 注册 MapperScannerConfigurer 6. Spring refresh() 7. 执行 BeanDefinitionRegistryPostProcessor 8. MapperScannerConfigurer 开始扫描包路径 9. 找到 Mapper 接口 10. 注册 MapperFactoryBean 11. 注入成功十、Mapper 最终为什么能注入?
比如:
@ResourceprivateUserMapperuserMapper;实际上 Spring 容器里并不是接口实例,而是:
MapperFactoryBean<UserMapper>它在getObject()时通过 MyBatis 动态代理生成:
sqlSession.getMapper(UserMapper.class)最终得到代理对象注入。
十一、总结
新版@MapperScan的核心思想:
自己不扫描,而是把扫描动作交给 Spring 生命周期更合适的阶段执行。
一句话理解
旧版:
@MapperScan -> 立刻扫描新版:
@MapperScan -> 注册扫描器Bean -> 容器后期再扫描官方这样改造的根本原因
- 支持
${}占位符 - 兼容 Spring Boot 自动配置
- 支持多数据源等复杂场景
- 统一 XML 与注解实现
- 更符合 Spring 扩展规范
十二、延伸思考(高级开发者必懂)
为什么实现的是:
BeanDefinitionRegistryPostProcessor而不是:
BeanFactoryPostProcessor答案是:
因为 Mapper 扫描要“新增 BeanDefinition”,而不是修改 Bean 实例。
总结
以上就是@MapperScan新版本底层原理分析。
表面看只是代码重构,实际上体现了 Spring 框架级设计思想:
复杂功能不要抢跑,交给正确生命周期执行。
这也是很多优秀中间件(MyBatis、Dubbo、Feign、Spring Cloud)的共同设计理念。