Mybatis通过MapperScannerConfigurer扫描basePackage源码分析

mybatis 版本:3.4.6
mybatis-spring 版本:1.3.2

首先需要Spring配置文件中添加以下配置:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="org.mybatis.spring.sample.mapper" />
	<!--可选项:指定在Spring上下文中有多个sqlSessionFactory的情况下使用哪个sqlSessionFactory。通常只有当有多个datasource时才需要-->
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

我们先看下MapperScannerConfigurer的类图:
MapperScannerConfigurer类图
MapperScannerConfigurer类图上我们可以看到它实现了四个接口:BeanDefinitionRegistryPostProcessorInitializingBeanApplicationContextAwareBeanNameAware

  1. 实现BeanDefinitionRegistryPostProcessor接口中的postProcessBeanDefinitionRegistry方法,扫描 basePackage 路径下的类,并注册到 Spring 容器中。
  2. 实现 InitializingBean 中的 afterPropertiesSet 方法,校验 basePackage 属性不能为空。
  3. 实现 ApplicationContextAware 中的 setApplicationContext 方法,用于获取 Spring 容器 applicationContext
  4. 实现 BeanNameAware 中的 setBeanName 方法动态的设置 beanName

应用启动时的方法运行顺序

MapperScannerConfigurer 中实现的接口方法的运行顺序如下:

  1. 调用 setBeanName 方法设置名称。
  2. 调用 setApplicationContext 方法设置 applicationContext 容器对象。
  3. 调用 afterPropertiesSet 方法检查属性 basePackage 不能为空。
  4. 调用 postProcessBeanDefinitionRegistry 方法开始扫描 basePackage 路径,并将路径下的类注册到 Spring 容器中。

前三步都非常简单,只是进行简单的配置检查和容器对象的注入。而 mapper 文件主要的注册过程是在第四步中完成,即在 postProcessBeanDefinitionRegistry方法中将 mapper 接口解析,并交由 Spring 容器管理。

scanner 扫描 basePackage 分析

我们来看下 postProcessBeanDefinitionRegistry 方法中做了哪些事情:

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        //将占位符替换为真正的值
        if (this.processPropertyPlaceHolders) {
            processPropertyPlaceHolders();
        }

        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        //默认true,将还未注册的mapper添加到Mybatis中
        scanner.setAddToConfig(this.addToConfig);
        //scanner 会注册拥有此注解的所有接口
        scanner.setAnnotationClass(this.annotationClass);
        //scanner 会注册具有指定父接口的接口类
        scanner.setMarkerInterface(this.markerInterface);
        //指定具体的session工厂,有多数据源时需要配置
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        //指定具体的session模板,有多数据源时需要配置
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        //指定具体的session工厂,有多数据源时需要配置,和setSqlSessionFactory作用一样
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        //指定具体的session模板,有多数据源时需要配置,和setSqlSessionTemplate作用一样
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        //指定 Spring 容器
        scanner.setResourceLoader(this.applicationContext);
        //指定 beanName 生成器
        scanner.setBeanNameGenerator(this.nameGenerator);
        //配置父scanner接口过滤全部符合条件的mapper接口(根据上面annotationClass、markerInterface属性)
        scanner.registerFilters();
        //扫描并注册basePackage路径下的mapper接口
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }

从上面方法的运行顺序可以看到:

  1. 该方法首先根据 processPropertyPlaceHolders 属性来判断是否需要加载配置文件来替换占位符。
  2. 接着设置 scanner 对象的属性值,包括指定 session 工厂、session 模板、设置 Spring 容器对象。
  3. 然后根据 annotationClassmarkerInterface 属性过滤符合条件的所有接口。
  4. 扫描 basePackage 路径下的 mapper 接口,并完成 mapper 接口的注册。

先来解释下第一步中 processPropertyPlaceHolders 替换占位符的含义。
有时候,我们会这样配置,basePackage 属性使用占位符代替,将实际包名配置在config.properties中。
Spring中配置改为以下形式:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="${basePackage}" />
</bean>

config.properties中有如下属性:

basePackage=org.mybatis.spring.sample.mapper

在Spring配置文件中有如下配置来读取config.properties:

<bean id="propertyPlaceholderConfigurer"
	  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
	<property name="locations">
		<list>
			<value>classpath:config.properties</value>
		</list>
	</property>
	<property name="ignoreUnresolvablePlaceholders" value="true" />
</bean>

但在启动的时候却发现占位符${basePackage}并没有被替换,还给你丢了个异常java.lang.IllegalArgumentException: Could not resolve placeholder ‘basePackage’
经过分析,我们从 processPropertyPlaceHolders 方法的注释上可以发现原因:
BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors.
This means that PropertyResourceConfigurers will not have been loaded and any property substitution of this class’ properties will fail.
To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class’ bean definition. Then update the values.
简单翻译下:
BeanDefinitionRegistries 是在应用启动的早期,且在BeanFactoryPostProcessors之前运行的。
这意味着PropertyResourceConfigurers还不会被加载,并且此类的所有属性的占位符替换都将会失败。
为了避免这种情况,需要查找在上下文中定义的PropertyResourceConfigurers配置,并且在此类的定义过程中运行它们,然后更新 basePackagesqlSessionFactoryBeanNamesqlSessionTemplateBeanName的值。

解决方案:我们修改下scanner扫描器,并添加新的配置processPropertyPlaceHolderstrue

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="${basePackage}"/>
	<property name="processPropertyPlaceHolders" value="true"/>
</bean>

这个时候,重新运行,会发现${basePackage}占位符已被成功替换。以下是 processPropertyPlaceHolders 方法的源码:

/*
     * BeanDefinitionRegistries are called early in application startup, before
     * BeanFactoryPostProcessors. This means that PropertyResourceConfigurers will not have been
     * loaded and any property substitution of this class' properties will fail. To avoid this, find
     * any PropertyResourceConfigurers defined in the context and run them on this class' bean
     * definition. Then update the values.
     */
    private void processPropertyPlaceHolders() {
        Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);

        if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
            BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext)
                    .getBeanFactory().getBeanDefinition(beanName);

            // PropertyResourceConfigurer does not expose any methods to explicitly perform
            // property placeholder substitution. Instead, create a BeanFactory that just
            // contains this mapper scanner and post process the factory.
            DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
            factory.registerBeanDefinition(beanName, mapperScannerBean);

            for (PropertyResourceConfigurer prc : prcs.values()) {
                prc.postProcessBeanFactory(factory);
            }

            PropertyValues values = mapperScannerBean.getPropertyValues();

            //占位符替换
            this.basePackage = updatePropertyValue("basePackage", values);
            this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values);
            this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values);
        }
    }

从上面源码可以看出,可以实现三个属性的占位符替换:basePackagesqlSessionFactoryBeanNamesqlSessionTemplateBeanName

接下来看 scanner 是如何扫描 basePackage 下的mapper接口的:

scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));

延伸StringUtils.tokenizeToStringArraySpring中字符串工具类,用于将字符串按照给定的格式进行分割。将上面的语句进行翻译:

scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n");

假设 basePackage"controller,mapper;service;util,util;",则调用完tokenizeToStringArray后生成的字符串数组为["controller","mapper","service","util","util"]

下面看下 doScan 的代码:

    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        //调用父类ClassPathBeanDefinitionScanner中的doScan方法
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            processBeanDefinitions(beanDefinitions);
        }
        return beanDefinitions;
    }


    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
        //遍历basePackages路径
        for (String basePackage : basePackages) {
            //获取包中的mapper接口
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition candidate : candidates) {
                //获取spring管理bean的作用域特性,包括名称和作用域代理行为,默认是单例和不使用代理
                ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
                candidate.setScope(scopeMetadata.getScopeName());
                //获取bean名称,默认是类名的首字母小写
                String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
                //其他设置,默认懒加载 lazyInit 为 false
                if (candidate instanceof AbstractBeanDefinition) {
                    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
                }
                //mapper如何有注解,则根据注解值进行赋值
                if (candidate instanceof AnnotatedBeanDefinition) {
                    AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
                }
                //检查beanName对应的mapper是否已注册
                if (checkCandidate(beanName, candidate)) {
                    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                    beanDefinitions.add(definitionHolder);
                    //开始对象的注册
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
    }

注意beanNameGenerator.generateBeanName用于获取beanName。这里有两种默认处理方式:
第一种如果类名是CommonMapper这种首字母大写,第二个字母小写的,则返回commonMapper;
第二种情况,当首字母和第二个字母都是大写,则原样返回,例如类名是URLMapper,则返回的仍然是URLMapper

接下来继续看 registerBeanDefinition 方法,类的注入过程,随着一层一层的debug
我们发现,最终的对象注册是在 DefaultListableBeanFactory 类中的registerBeanDefinition 方法中完成的:

    /** bean对象map,key为beanName */
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();

    /** beanName列表,按照注册的顺序添加 */
    private final List<String> beanDefinitionNames = new ArrayList<String>();

    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {
        //bean名称和对象不能为空
        Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");

        if (beanDefinition instanceof AbstractBeanDefinition) {
            try {
                ((AbstractBeanDefinition) beanDefinition).validate();
            }
            catch (BeanDefinitionValidationException ex) {
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                        "Validation of bean definition failed", ex);
            }
        }
        //将beanDefinitionMap锁定,只能串行处理
        synchronized (this.beanDefinitionMap) {
            Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);
            //如果oldBeanDefinition不为空则说明该beanName已经有对应的对象
            if (oldBeanDefinition != null) {
                //是否允许相同beanName的对象覆盖,不允许则抛出异常
                if (!this.allowBeanDefinitionOverriding) {
                    throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                            "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
                                    "': There is already [" + oldBeanDefinition + "] bound.");
                }
                else {
                    if (this.logger.isInfoEnabled()) {
                        this.logger.info("Overriding bean definition for bean '" + beanName +
                                "': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
                    }
                }
            }
            else {
                //将beanName按照顺序插入到beanDefinitionNames列表中
                this.beanDefinitionNames.add(beanName);
                this.frozenBeanDefinitionNames = null;
            }
            //将beanName及beanDefinition加入/覆盖到beanDefinitionMap容器中
            this.beanDefinitionMap.put(beanName, beanDefinition);
            resetBeanDefinition(beanName);
        }
    }

总结:

  1. 配置文件中添加MapperScannerConfigurerbasePackage 路径。
  2. 调用 BeanDefinitionRegistryPostProcessor 中的postProcessBeanDefinitionRegistry方法配置 scanner属性。
  3. scanner扫描器调用scan方法开始进行mapper文件扫描。
  4. ClassPathBeanDefinitionScanner中的doScan方法对mapper接口进行抽取封装成一个 BeanDefinition 对象。
  5. DefaultListableBeanFactoryregisterBeanDefinition 方法将 beanName对应的 BeanDefinition对象存放到 beanDefinitionMap 容器中,完成bean定义对象的注册。

最后再配上一张时序图,仅供参考,不到之处,尽请指正:
MapperScannerConfigurer扫描时序图

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值