浅谈MapperScan

前言

在日常开发中,我们经常使用 @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 注册一个 MapperScannerConfigurer BeanDefinition。


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 -> 容器后期再扫描

官方这样改造的根本原因

  1. 支持 ${} 占位符
  2. 兼容 Spring Boot 自动配置
  3. 支持多数据源等复杂场景
  4. 统一 XML 与注解实现
  5. 更符合 Spring 扩展规范

十二、延伸思考(高级开发者必懂)

为什么实现的是:

java 复制代码
BeanDefinitionRegistryPostProcessor

而不是:

java 复制代码
BeanFactoryPostProcessor

答案是:

因为 Mapper 扫描要"新增 BeanDefinition",而不是修改 Bean 实例。


总结

以上就是 @MapperScan 新版本底层原理分析。

表面看只是代码重构,实际上体现了 Spring 框架级设计思想:

复杂功能不要抢跑,交给正确生命周期执行。

这也是很多优秀中间件(MyBatis、Dubbo、Feign、Spring Cloud)的共同设计理念。


相关推荐
wljt12 小时前
公司二-面试题
java·开发语言·mybatis
VimorphTech16 小时前
文本已经改了,旧的音频生成结果还能回写吗?
java·数据库·mybatis
麻瓜code20 小时前
双写一致性:为什么是“删缓存“,而不是“改缓存“
java·缓存·mybatis
郭小李2 天前
mybatis 动态 sql
java·sql·mybatis
知守观2 天前
技术考古:iBATIS 2.3.4 时代的持久层,是如何用 Map 玩转 307 个存储过程的?
java·后端·mybatis
captain3762 天前
MyBatis Plus(1)
java·服务器·mybatis
代码不停2 天前
MyBatis进阶
spring boot·mybatis
captain3762 天前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
captain3762 天前
MyBatis(进阶1)
mybatis
郭小李3 天前
mybatis的 Mapper.xml语法
xml·java·mybatis