Spring mybatis源碼篇章-MapperScannerConfigurer


承接前文Spring mybatis源碼篇章-Mybatis的XML文件加載,本文將在前文的基礎上講解Spring在Mybatis整合方面的另一動作

前話

根據前文的分析可得到以下結論

  1. MappedStatement是mybatis操作sql語句的持久層對象,其id由注解模式的${mapperInterface類全名}.${methodName}或者XML模式的${namespace}.${CRUD標簽的id}確定,且是唯一的

  2. Mybatis對每個CRUD語句都會生成唯一的MappedStatement保存至ConfigurationmappedStatements(Map集合)中

  3. Mybatis提供注解模式和XML模式生成MappedStatement,在兩者同時存在的情況下,前者會覆蓋后者

  4. XML模式生成的MappedStatement,是保存在org.apache.ibatis.session.Configuration對象中的mappedStatements屬性中(Map),並且也將${namespace}對應的DAO類存放至mapperRegistry屬性里,供外部調用。

本文主要分析的是第四點,因為第四點的mapperRegistry類有一個特別的方法

  // SqlSession就是關鍵
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {}

我們都知道Spring在整合Mybatis的時候都會配置一個SqlSessionFactoryBean對象來生成一個SqlSessionFactory,而這個SqlSessionFactory就是作為SqlSession(數據庫會話)的關鍵部分,那么Spring又怎么把這個對象與DAO接口類關聯放在mapperRegistry里呢????

老方式

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
	<property name="mapperInterface" value="com.test.sqlmapper.UserMapper"/>
	<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

上述的配置是針對單個的mapperInterface注入到應用程序中,試想如果有很多的接口則會導致Spring主配置文件臃腫,所以上述的辦法已過時

新方式

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
      <!-- optional unless there are multiple session factories defined -->
      <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
  </bean>

采用MapperScannerConfigurer掃描類來實現對basePackage指定的DAO接口集合統一關聯SqlSession,這就節省了之前老配置很多的代碼空間。

MapperScannerConfigurer

直接一睹MapperScannerConfigurer類的源碼風采,優先查看其內部屬性

    public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {

      private String basePackage;

      private boolean addToConfig = true;

      private SqlSessionFactory sqlSessionFactory;

      private SqlSessionTemplate sqlSessionTemplate;

      private String sqlSessionFactoryBeanName;
    
      private String sqlSessionTemplateBeanName;

      private Class<? extends Annotation> annotationClass;

      private Class<?> markerInterface;
    
      private ApplicationContext applicationContext;

      private String beanName;

      private boolean processPropertyPlaceHolders;

      private BeanNameGenerator nameGenerator;
      ....
     }

其源碼上的注釋其實寫的很清楚了,注釋篇幅過長,就不在這里展示了,稍微提下這個類的相關使用:

  • basePackage 基本屬性,接口類掃描的包路徑,支持,;分隔

  • sqlSessionFactoryBeanName 當有多個SqlSessionFactory環境時,官方通過其來指定加載特定的sqlSessionFactory,value即為bean的id值(建議使用此屬性)

  • sqlSessionFactoty 默認是不用填的,其會去尋找id為sqlSessionFactory的sqlSessionFactory實例,sqlSessionTemplate的操作類似

  • annotationClass 注解類,其會去Spring環境下尋找擁有此注解的接口類,並忽略basePackage的屬性,默認為null

  • markerInterface 父類接口類,其會去尋找繼承此接口類的子接口類但不包括父類接口,並忽略basePackage的屬性,與annotationClass並存,默認為null

MapperScannerConfigurer#postProcessBeanDefinitionRegistry()

直接進入其關鍵方法,觀察下是如何進行掃描的

  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    //支持${basePackage}形式
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }
    //base classpath environment to scan
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    //base annotationClass and markerInterface properties to register interface filters
    scanner.registerFilters();
    // scan 
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

上述代碼也就是設置相應的屬性給ClassPathMapperScanner,具體的如何掃描我們繼續往下看

ClassPathBeanDefinitionScanner

掃描的具體操作由ClassPathMapperScanner的父類ClassPathBeanDefinitionScanner來完成,我們簡單的看下其中的邏輯

	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
		for (String basePackage : basePackages) {
		    // 找尋classpath對應的目錄,其中的解析幫助類為PathMatchingResourcePatternResolver
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			// 遍歷
			for (BeanDefinition candidate : candidates) {
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				// 設置基本的屬性,比如lazy-init/autowire-mode等
				if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				// 對針對@mapper注解的bean
				if (candidate instanceof AnnotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
				// 確保不重復注冊到bean工廠
				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;
	}
  • 上述代碼最關鍵的便是找尋對應目錄的所有interface接口,其是通過PathMatchingResourcePatternResolver幫助類來完成的,后續筆者獨立成篇加以分析

  • 對找尋的beanDefinitions集合過程中也會進行filters過濾,即用到了annotationClassmarkerInterface屬性

ClassPathMapperScanner

針對上述的符合條件后獲取的beanDefinitions集合,子類便要進行最后的加工處理

  /**
   * Calls the parent search that will search and register all the candidates.
   * Then the registered objects are post processed to set them as
   * MapperFactoryBeans
   */
  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    //由父類去找到符合條件的interface類,並轉化為bean類
    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 {
      for (BeanDefinitionHolder holder : beanDefinitions) {
        GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();

        if (logger.isDebugEnabled()) {
          logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
              + "' and '" + definition.getBeanClassName() + "' mapperInterface");
        }

        // the mapper interface is the original class of the bean
        // but, the actual class of the bean is MapperFactoryBean
		//最終將definition包裝成MapperFactoryBean,beanClass設置為其內部屬性MapperInterface
        definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
        definition.setBeanClass(MapperFactoryBean.class);

        definition.getPropertyValues().add("addToConfig", this.addToConfig);

        boolean explicitFactoryUsed = false;
		//根據sqlsessionFactoryBeanName尋找運行狀態的SqlsessionFactory的虛引用,但並沒有去真實加載
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
          definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionFactory != null) {
          definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
          explicitFactoryUsed = true;
        }

        if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
          if (explicitFactoryUsed) {
            logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
          }
          definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionTemplate != null) {
          if (explicitFactoryUsed) {
            logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
          }
          definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
          explicitFactoryUsed = true;
        }
		//當沒有指定SqlSession對象,則設置MapperFactoryBean自動去找尋
        if (!explicitFactoryUsed) {
          if (logger.isDebugEnabled()) {
            logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
          }
          definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
      }
    }

    return beanDefinitions;
  }

根據上述代碼得知,其最終被包裝的類為MapperFactoryBean,由其完成最終的關聯

MapperFactoryBean

筆者此處只關注其關鍵方法checkDaoConfig(),源碼如下

  /**
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
	// 判斷mapperInterface接口是否已被注冊過
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
	    // 此處就跟mybatis加載主配置文件時對mapper節點指定package屬性或者mapperClass屬性的入口是一樣的;xml方式的加載也是會走這一步
        configuration.addMapper(this.mapperInterface);
      } catch (Throwable t) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
        throw new IllegalArgumentException(t);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

上述主要是調用Configuration#addMapper()方法來將相應的DAO接口放入mapperRegistry中的knownMappers屬性,我們可以看下這個屬性的類型

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

恩,有動態代理的味道~~~

怎么取相應的DAO接口呢供外界調用呢,就是getObejct()方法

  public T getObject() throws Exception {
	// 間接關聯了SqlSession與mapperInterface
    return getSqlSession().getMapper(this.mapperInterface);
  }

通過上述的代碼便調用了上文所提及問題的mapperRegistry#getMapper()方法了,但具體的如何調用我們后文分析~~

總結

作下簡單的小結

  1. MapperScannerConfigurer類主要實現將basePackage包下的所有接口類都包裝成MapperFactoryBean對象,內含相應的SqlSessionFactory數據庫會話

  2. MapperScannerConfigurer類默認情況下在形成MappedStatement的過程中會優先去找尋與接口同目錄下的XML文件來加載生成。這點與Mybatis的主文件加載方式類同~~~只不過其更方便

  3. MapperScannerConfigurer一般結合sqlSessionFactoryBeanmapperLocations屬性來完成Mybatis主文件的mappers的工作~~

  4. 官方以及筆者推薦大家使用MapperScannerConfigurer與SqlSessionFactoryBean搭配使用~~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM