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