在 Spring Boot 项目里,启动类上写一行@MapperScan,所有 Mapper 接口就像变魔法一样能被注入了。我一开始只是照着模板抄,直到某天遇到一个诡异的问题:一个明明存在且包路径也正确的 Mapper 接口,启动后怎么都注入不进来。排查时翻到 mybatis-spring 的源码才意识到,自己连 Mapper 是怎么进 Spring 容器的都没搞明白。弄清楚这个问题之后,再看@MapperScan相关的报错和配置,基本都是一眼定位。
这篇文章只讲一件事:@MapperScan从注解生效到 Mapper 接口的 BeanDefinition 被注册进容器的完整过程。我会把 Spring 容器里发生了什么、BeanDefinition 是怎么被“偷梁换柱”成MapperFactoryBean的、最终又如何变成我们注入的代理对象,一条链路讲透。适合正在用 MyBatis 做开发、对 Spring 底层机制感兴趣,或者被 Mapper 注入问题困扰的同学。看懂这一篇,后面再深入 MyBatis 的执行器、SQL 绑定等源码会轻松很多。
1. 背景铺垫:为什么 Spring 原生扫描对 Mapper 接口没辙
在拆解@MapperScan之前,得先理解一个基础问题:为什么 MyBatis 不能像普通 Bean 一样,直接让 Spring 把 Mapper 接口扫成一个组件?
1.1 接口既不能实例化,也不是组件
Spring 最常用的组件扫描机制,是扫描@Component、@Service、@Repository、@Controller这几个注解。一个普通的接口,上面没有任何 Spring 注解,就算它被扫描到了,Spring 也不会把它注册成 BeanDefinition。这是第一层障碍。
第二层障碍更关键:接口本身是不能实例化的。Spring 拿到一个具体类,可以通过Class.getDeclaredConstructor()找到无参构造器,然后newInstance()创建对象。但接口没有构造器,Spring 没法直接new出一个接口实现。MyBatis 的 Mapper 接口真正可用,是因为框架在运行时给它创建了 JDK 动态代理对象。这个代理对象是MapperProxy,它需要拿到SqlSession、Configuration等 MyBatis 内部组件才能工作。所以 Mapper 接口的创建逻辑,必须由 MyBatis 自己接管,而不是交给 Spring 默认的实例化策略。
用一个生活化的类比:Spring 像是食堂的厨师,他只会炒现成的菜。普通 Bean 是已经洗好切好的食材,下锅就行。但 Mapper 接口是一张菜谱,菜谱本身不能吃,需要有专门的面点师傅(MyBatis)按照菜谱上的流程,先和面、发面、烘烤才能出成品。所以 MyBatis 必须找一种方式告诉 Spring:“这个 Bean 你别直接 new,让我的MapperFactoryBean来代劳。”
1.2 整条链路先画个地图
看源码最怕一头扎进细节出不来。我的习惯是先画宏观链路,知道每个类大概扮演什么角色,再逐段深入。
@MapperScan的完整执行链路大致是这样的:
- 启动类上标注
@MapperScan("com.example.mapper")。 - Spring 解析启动类上的
@Import(MapperScannerRegistrar.class),把注册逻辑交给MapperScannerRegistrar。 MapperScannerRegistrar创建ClassPathMapperScanner,传入包路径,执行扫描。- 扫描器在指定包下找到所有符合条件的接口文件,生成候选的 BeanDefinition。
- 关键一步:扫描器把每个候选 BeanDefinition 的
beanClass从“原 Mapper 接口”替换成MapperFactoryBean,同时把原接口类型作为构造参数存进去。 - Spring 正常实例化这个 BeanDefinition,得到
MapperFactoryBean对象。 - 当其他代码注入 Mapper 接口时,Spring 通过
MapperFactoryBean.getObject()拿到 JDK 动态代理对象。
整条链路上,@MapperScan只是入口,真正的幕后核心是MapperScannerRegistrar和ClassPathMapperScanner。名字很像,但职责不同:前者处理注解元数据、设置扫描器参数;后者负责真正的类路径扫描和 BeanDefinition 加工。
1.3 读源码前必须认识的三个 Spring 扩展点
这段代码涉及 Spring 的几个扩展接口。不懂它们,@MapperScan的源码很容易看晕。
ImportBeanDefinitionRegistrar是 Spring 提供的一个回调接口。当某个@Configuration类被处理时,Spring 会把该类上@Import引入的类做处理。如果引入的类实现了ImportBeanDefinitionRegistrar,Spring 就会回调它的registerBeanDefinitions()方法,让你有机会手动向容器注册 BeanDefinition。@MapperScan走的就是这条路。
BeanDefinitionRegistry是 Spring 内部的“户籍系统”。所有 Bean 的注册信息都记录在这里。BeanDefinition是“户籍信息”,Bean才是后来根据户籍信息创建出来的“真人”。registerBeanDefinitions()方法的第二个参数就是BeanDefinitionRegistry,拿到它,就能手动把自定义的 BeanDefinition 塞进去。
ClassPathBeanDefinitionScanner是 Spring 自带的类路径扫描器。它负责遍历指定包下所有.class文件,读取类的元数据,判断哪些符合候选条件,然后生成 BeanDefinition。MyBatis 的ClassPathMapperScanner就是继承并扩展了这个类,所以@MapperScan没有另起炉灶,是在 Spring 已有的扫描机制上做改造。
这三个概念串起来后,再看@MapperScan,它其实不是“扫描”本身,而是一把钥匙,打开的是“让 MyBatis 自定义扫描和注册逻辑”这扇门。
2. 注解拆解:@MapperScan 每个参数到底管什么
很多人用@MapperScan的时候只写一个包路径,但实际这个注解上有十多个属性,每个属性都对应扫描器的一条配置。源码看多了就会发现,很多诡异的问题不是扫描本身出了问题,而是注解上的某个参数和实际预期不一致。
2.1 注解属性逐个说清楚
先贴一份比较完整的注解属性对照表,后面再展开讲重点。
| 属性 | 作用 | 典型使用场景 |
|---|---|---|
value | 指定扫描的基础包路径,和basePackages等价 | @MapperScan("com.example.mapper") |
basePackages | 同value,区别在于语义更明确 | 多包时:basePackages = {"pkg1", "pkg2"} |
basePackageClasses | 以指定类的包路径作为扫描根路径 | 避免字符串写错,用类引用更安全 |
nameGenerator | 自定义 Bean 名称生成器 | 需要控制 Mapper Bean 的命名规则时 |
annotationClass | 只注册标注了指定注解的接口 | 限制某个 Mapper 必须标有自定义注解 |
markerInterface | 只注册实现了指定标记接口的 Mapper | 做 Mapper 分组区分时常用 |
sqlSessionTemplateRef | 指定注入的SqlSessionTemplateBean 名称 | 多数据源场景下必用 |
sqlSessionFactoryRef | 指定注入的SqlSessionFactoryBean 名称 | 多数据源场景下必用 |
factoryBean | 自定义MapperFactoryBean实现类 | 极少用,有特殊扩展需求才动 |
defaultScope | 设置 Mapper Bean 的默认作用域 | Spring 6 / Boot 3 时代控制单例或原型 |
lazyInitialization | 设置 Mapper Bean 是否懒加载 | 配合 Spring Boot 的懒加载机制使用 |
这里最容易被忽略的是value和basePackages都没写的情况。源码里有一段逻辑:如果两个都没配置,就用@MapperScan所在类(通常是启动类)的包路径作为默认扫描包。曾经有同事把启动类放在com.example,但 Mapper 放在com.example.module.mapper,结果@MapperScan什么都不写也能扫到一部分,但另一部分在更深层级包里的接口没扫到,排查了半天才怀疑到默认包路径的问题上。
2.2 @Import 机制:注解如何从定义变成行动
@MapperScan的上方有一个关键元注解:
@Import(MapperScannerRegistrar.class)@Import是 Spring 导入配置的“快速通道”。它可以导入三类东西:
| 类型 | 行为 | 典型例子 |
|---|---|---|
普通@Configuration类 | 直接作为配置类加载 | 业务配置类 |
ImportSelector实现类 | 回调后返回一组需要导入的类名 | Spring Boot 自动配置 |
ImportBeanDefinitionRegistrar实现类 | 回调方法里直接向容器注册 BeanDefinition | @MapperScan走的就是这条路 |
MapperScannerRegistrar属于第三种。ImportBeanDefinitionRegistrar的特点是“你完全控制注册逻辑”,不依赖 Spring 自动推断。MyBatis 选择这种方式的原因很直接:它需要把@MapperScan上各种属性的值读出来,动态决定扫描器怎么配、扫哪些包、过滤哪些接口。用ImportSelector只能决定“加载哪个类”,做不了这么细的定制。
2.3 没有 @MapperScan 时的另一条路:自动配置扫描器
很多老项目并不用@MapperScan,而是在接口上标@Mapper注解。这两种方式其实对应两套不同的注册逻辑。
只标@Mapper注解、不写@MapperScan时,MyBatis-Spring Boot 的自动配置类中会加入一个AutoConfiguredMapperScannerRegistrar。这个 registrar 创建的扫描器默认扫描启动类所在包及其子包,但只认标注了@Mapper注解的接口。
@MapperScan一旦出现,自动配置的扫描器就会被“替换掉”。判断方式也很简单:@MapperScan引起的注册器已经注册了 Mapper Bean,MyBatis 自动配置如果发现容器中已经有MapperFactoryBean或MapperScannerConfigurer之类的 Bean,就会放弃执行自己的扫描逻辑。这也是很多项目里既标@Mapper又加@MapperScan,依然正常工作的原因:后者覆盖了前者,重复扫描被框架内部规避了。
3. 核心走读:BeanDefinition 是怎么被扫描和替换的
现在进入正题。看MapperScannerRegistrar.registerBeanDefinitions()和ClassPathMapperScanner的关键逻辑,把“接口变成 BeanDefinition”这条主线的每个细节都过一遍。
3.1 registerBeanDefinitions:入口处的编排
MapperScannerRegistrar的registerBeanDefinitions()方法,逻辑非常直白,就是读注解、建扫描器、配参数、开扫。
我把核心流程简化成伪代码:
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 1. 从启动类的元数据里取出 @MapperScan 上的所有属性 AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap( importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); if (mapperScanAttrs != null) { registerBeanDefinitions(mapperScanAttrs, registry, importingClassMetadata.getClassName()); } } protected void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String importBeanName) { // 2. 创建 MyBatis 自定义扫描器 ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); // 3. 把注解上的配置逐项设置给扫描器 scanner.setAnnotationClass(annoAttrs.getClass("annotationClass")); scanner.setMarkerInterface(annoAttrs.getClass("markerInterface")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setResourceLoader(resourceLoader); scanner.setBeanNameGenerator(...); scanner.setMapperFactoryBeanClass(annoAttrs.getClass("factoryBean")); // 4. 注册过滤器,决定哪些接口可以被扫描到 scanner.registerFilters(); // 5. 执行真正的扫描 scanner.doScan(StringUtils.toStringArray(basePackages)); }这段代码有个值得注意的细节:registerFilters()必须在doScan()之前执行。ClassPathMapperScanner扫描时能不能选中 Mapper 接口,完全依赖于过滤器。如果漏掉registerFilters(),扫描器会沿用 Spring 父类默认的过滤规则,只认@Component一族注解,自然一个 Mapper 都扫不到。源码阅读时,这种调用顺序往往比方法内部逻辑更重要。
3.2 registerFilters:扫描器怎么筛选出“接口”
ClassPathMapperScanner的registerFilters()方法,决定了一个.class文件经过类路径扫描后能不能成为候选。它的逻辑值得细看:
public void registerFilters() { // 场景一:配置了 annotationClass,选中所有标注了该注解的接口 if (this.annotationClass != null) { addIncludeFilter(new AnnotationTypeFilter(this.annotationClass)); } // 场景二:配置了 markerInterface,选中所有实现了该接口的 Mapper if (this.markerInterface != null) { addIncludeFilter(new AssignableTypeFilter(this.markerInterface) { @Override protected boolean matchClassName(String className) { return false; } }); } // 场景三:兜底过滤器,所有接口都作为候选 addIncludeFilter((metadataReader, metadataReaderFactory) -> metadataReader .getClassMetadata().isInterface()); }默认情况下annotationClass是Annotation.class的占位值,markerInterface是Class.class的占位值。所以实际操作中绝大多数项目走的是第三种情况:包下所有接口都成为候选。
但光有 includeFilter 还不够。扫描器在生成 BeanDefinition 之前,还会调用isCandidateComponent()再做一次资格校验。ClassPathMapperScanner重写了这个方法:
@Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { boolean isCandidate = false; if (beanDefinition.getMetadata().isIndependent()) { if (beanDefinition.getMetadata().isInterface()) { isCandidate = true; } } return isCandidate; }这个重写非常关键。isIndependent()过滤掉了内部类、匿名类;isInterface()确保只处理接口。而 Spring 父类的默认实现里,对接口的态度更加保守,需要配合其他条件才可能放行。MyBatis 重写之后,意思就变成了:只要是独立的接口,就收进来。所以如果你的 Mapper 定义成了某个类的内部接口,或者不是 public 访问级别,就很可能在这里被悄悄过滤掉。
3.3 doScan 与 processBeanDefinitions:偷梁换柱的魔术时刻
筛选完成后,扫描器拿到了一批候选 BeanDefinition。这时它们的beanClassName都还指向原始接口。接下来processBeanDefinitions()开始做真正的“换芯”操作。
public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { logger.warn(...); // 扫描不到时的警告 } else { processBeanDefinitions(beanDefinitions); } return beanDefinitions; }super.doScan()是 Spring 提供的标准流程,负责遍历包路径下的 class 文件、读取元数据、套用过滤器。真正的 MyBatis 逻辑集中在processBeanDefinitions()里,我用关键代码演示:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); // 拿到原本的接口全限定名 String beanClassName = definition.getBeanClassName(); // 把接口类型存进构造参数,后面MapperFactoryBean要用 definition.getConstructorArgumentValues() .addGenericArgumentValue(beanClassName); // 核心操作:把beanClass替换成MapperFactoryBean definition.setBeanClassName(this.mapperFactoryBeanClass.getName()); // 构造器参数不用XML指定,Spring按类型自动注入 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); // 处理懒加载、默认作用域等扩展属性 ... } }这里面的每一行都有明确意图,我来逐个解释。
第一,把接口类型记录到构造参数里。MapperFactoryBean有一个构造函数接收Class<T>类型,Spring 通过 BeanDefinition 的构造参数列表,最终会调用new MapperFactoryBean(UserMapper.class)这样的代码。接口类型是后面创建代理的前提。
第二,把beanClass替换成MapperFactoryBean。这是一次典型的“李代桃僵”。Spring 原本以为自己要创建的是UserMapper接口的实例,但接口没法实例化。MyBatis 的做法是把目标换成一个能生产代理的工厂类。Spring 的实例化流程并不关心这个工厂类叫不叫UserMapper,它只负责按 BeanDefinition 的描述把对象创建出来。
第三,AUTOWIRE_BY_TYPE自动装配模式。MapperFactoryBean内部依赖SqlSessionFactory或SqlSessionTemplate,但这些依赖不是通过 BeanDefinition 显式指定的,而是让 Spring 在创建工厂 Bean 时,按类型自动从容器中找。这样设计的好处是解耦,MapperFactoryBean不需要关心具体数据源配置,Spring 容器里有哪个SqlSessionFactory就注入哪个。多数据源环境下,@MapperScan里的sqlSessionFactoryRef也会在这里转成具体名称的限定。
到这一步,一个普通的接口 BeanDefinition,就变成了“构造函数参数是接口类型、beanClass 是工厂类”的特殊 BeanDefinition。这个过程可以类比成:食堂菜谱上原本写着“清蒸鱼”,后厨拿到手后,把菜谱替换成了“请找专门做鱼的师傅,以这条鱼为原料,重新做一份清蒸鱼”。Spring 不认识鱼,但它认识师傅。
4. 后续接力:MapperFactoryBean 如何把接口变成可注入对象
BeanDefinition 注册完成后,Spring 容器开始创建 Bean。MapperFactoryBean作为整个流程的接力者,它的实例化过程决定了最终拿到的是什么。
4.1 MapperFactoryBean 的双重身份
MapperFactoryBean继承了SqlSessionDaoSupport,实现了FactoryBean<T>接口。FactoryBean<T>是 Spring 里一类特殊的 Bean:容器里注册的虽然是你定义的那个对象,但别人注入时,实际拿到的是getObject()方法的返回值。
MapperFactoryBean的getObject()方法内容很简单:
@Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }也就是从SqlSession里取出 Mapper 接口对应的动态代理对象。这一步封装得很薄,真正的逻辑在 MyBatis 内部。但这里有个前置条件:getSqlSession()返回的SqlSession必须已经初始化好,而且Configuration里必须已经注册过这个 Mapper 接口。谁来完成这个注册?答案是afterPropertiesSet()。
4.2 afterPropertiesSet 里的隐藏动作
因为MapperFactoryBean实现了InitializingBean,所以 Spring 在创建完对象、完成属性填充后,会调用afterPropertiesSet()方法。它的核心逻辑是checkDaoConfig()。
@Override protected void checkDaoConfig() { // 从SqlSessionTemplate里拿到MyBatis的全局配置 Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { // 关键动作:把Mapper接口注册进MyBatis的MapperRegistry configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error(...); throw new IllegalArgumentException(...); } } }这一步非常容易被忽略,没有它后面什么都跑不起来。configuration.addMapper()会在 MyBatis 的MapperRegistry中建立“接口类型 → 代理工厂”的映射,同时创建MapperAnnotationBuilder,解析接口上的注解 SQL 和 XML SQL。换句话说,MyBatis 是到这一步才把UserMapper接口真正变成自己的“内部资产”。
所以@MapperScan链路完整走完后,容器里的流程是:Spring 创建MapperFactoryBean→ 初始化时把接口注册进 MyBatis 的Configuration→ 后续调用getObject()时,MyBatis 已经准备好一切,直接返回代理对象。
4.3 动态代理与最终调用
当你在任意 Service 里写@Autowired UserMapper userMapper,Spring 按照类型寻找候选 Bean。因为MapperFactoryBean的getObjectType()返回了接口类型,Spring 能从工厂中识别出它生产的是UserMapper。注入时调用getObject(),得到MapperProxyFactory.newInstance()创建的代理对象。
我来演示一下代理对象的结构:
public class MapperProxyFactory<T> { public T newInstance(SqlSession sqlSession) { MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); } }MapperProxy实现了InvocationHandler。当你调用userMapper.selectById(1)时,代理对象把这次调用交给invoke()方法:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // equals/hashCode/toString直接透传,不经过SQL if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 拿到或创建MapperMethod,最终按SQL指令选择执行方式 MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }MapperMethod.execute()根据 SQL 类型,调用sqlSession.selectOne()、insert()、update()、delete()等对应方法,往下走就是 MyBatis 的Executor、JDBC 驱动,最终完成数据库操作。从接口定义到数据库查询,这条链路全部打通。
5. 自定义验证与踩坑排查实战
光看源码不验证,印象总是浅的。我在实际排查问题时,会自己写几个小工具把这条链路“看”出来。这里分享一些实测可行的验证方法和踩过的坑。
5.1 写个 BeanFactoryPostProcessor 偷看 BeanDefinition
想要确认某个 Mapper 接口有没有被扫描成预期的 BeanDefinition,最直接的方法是注册一个BeanFactoryPostProcessor,在 Spring 完成所有 BeanDefinition 注册之后、开始实例化之前,把注册表翻一遍。
@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { String[] names = beanFactory.getBeanDefinitionNames(); Arrays.stream(names) .filter(name -> name.contains("UserMapper")) .forEach(name -> { BeanDefinition bd = beanFactory.getBeanDefinition(name); System.out.println("beanName = " + name); System.out.println("beanClass = " + bd.getBeanClassName()); System.out.println("constructor = " + bd.getConstructorArgumentValues()); }); } }启动项目时,输出结果会让链路一目了然:
beanName = userMapper beanClass = org.mybatis.spring.mapper.MapperFactoryBean constructor = ConstructorArgumentValues: [UserMapper]beanClass是MapperFactoryBean,构造参数是UserMapper,说明扫描器的“偷梁换柱”已经生效。如果这里显示的beanClass还是UserMapper,说明根本没有走到processBeanDefinitions(),扫描逻辑有问题。
5.2 常见问题一:扫描包路径正确但接口没有注册
这种情况最隐蔽。某个 Mapper 接口明明在扫描包范围内,项目也能启动,但注入时始终报找不到 Bean。我在生产环境排查过一次,最后定位到原因是:接口被定义成了内部接口。
假设有一个类:
public class Foo { public interface UserMapper { User selectById(Integer id); } }ClassPathMapperScanner.isCandidateComponent()判断这个内部接口时,isIndependent()返回 false,于是直接排除。解决办法很简单:把接口从内部类中挪出来,独立成文件,保证它是 top-level 接口,访问权限是 public。
另一个可能原因是包名写错级别。@MapperScan("com.example.mapper")只扫描该包及其直接子包。如果接口在com.example.mapper.sub下,那没问题;但如果接口在com.example.dao下,就扫不到。使用basePackageClasses用类引用来代替字符串,可以从根本上避免字符串笔误。
5.3 常见问题二:多个扫描源导致 Bean 定义冲突
@MapperScan与@Mapper同时使用、或者多个@MapperScan扫描到同一批接口时,可能出现 BeanDefinition 冲突。MyBatis 在classPathMapperScanner中做了checkCandidate()判断:如果已经存在一个同名的 BeanDefinition,且和新的不一致,就直接抛出Invalid bean definition异常。
多次扫描本身不一定有问题。如果两次扫描注册的 BeanDefinition 完全一致,MyBatis 会选择“已经有合适定义就跳过”。真正会报错的是扫描后生成的 BeanDefinition 细节不同,比如设置了不同的sqlSessionTemplateRef、不同的nameGenerator。解决方案是收敛扫描入口:尽量只保留一个@MapperScan,或者统一用@Mapper注解配合自动配置,避免两套扫描机制互相干扰。
5.4 常见问题三:MapperFactoryBean 类型对不上
还有一个场景很值得提:项目引入 MyBatis 后手写了一个自定义的factoryBean,或者改了@MapperScan的factoryBean属性,但新类没有正确实现FactoryBean<T>。这种情况下 Spring 在解析 Bean 类型时拿不到正确的getObjectType(),@Autowired UserMapper会直接报No qualifying bean of type 'UserMapper'。
排查时可以先用BeanFactoryPostProcessor打印出所有 Mapper 接口的 BeanDefinition,如果beanClass显示的不是MapperFactoryBean,而是某个自定义类,马上就能意识到是这个属性配置的问题。多数据源场景下,如果同时存在多个SqlSessionFactory,但@MapperScan没有指定sqlSessionFactoryRef,AUTOWIRE_BY_TYPE会因找不到唯一候选而报错。配置多数据源时,每个@MapperScan最好都显式指定对应的工厂引用。
5.5 源码阅读与排查的经验心得
在翻这些源码的时候,我个人最大的收获不是背下了类名和行号,而是养成了一套“从现象反推代码”的习惯。遇到 Mapper 注入问题,先通过BeanFactoryPostProcessor看 BeanDefinition 是否存在,再看 BeanDefinition 的beanClass和构造参数是否符合预期,最后才检查 MyBatis 的Configuration中hasMapper的状态。三步定位,基本不会走偏。
断点也有讲究。ClassPathMapperScanner.processBeanDefinitions()是一个很好的断点位置,能观察扫描结果和 BeanDefinition 的修改过程。另一个关键断点是MapperFactoryBean.checkDaoConfig(),能确认接口注册进 MyBatis 的时机。我会在启动的主线程上打断点,不走到业务代码,只看容器初始化这一段。
最后留一句体会:读这种框架源码,不要试图一次性理解所有分支,先抓住“接口如何被 Spring 接纳”这条主干,再看各属性如何被消费,最后才是异常和边界情况。主干通了,剩下的细节都只是在这条主线上加装饰而已。