mybatis MapperScannerConfigurer 源碼剖析

mybatis MapperScannerConfigurer 示例

使用過 mybatis 的人應該都知道,mybatis 有個特性就是對於 Mapper 類,只須要聲明接口就能夠了,而不須要寫具體的實現類,上層在使用 Mapper 接口時只須要直接注入 Mapper 接口就能夠正常工做,下面咱們就來具體剖析下 mybatis 是怎樣經過這個 Mapper 接口來自動生成 Mapper 實現類,而且註冊到 spring 容器中。java

先來看一個 mybatis 的使用示例

applicationContext.xmlspring

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="configLocation" value="classpath:config/mybatis-config.xml" />
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath:mapping/*Mapper.xml" />
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <!-- 掃描全部 RepositoryMapper 註解的類 -->
  <property name="annotationClass" value="com.jaf.framework.core.mapper.RepositoryMapper" />
  <!-- 掃描全部 BaseMapper 接口實現類 -->
  <property name="markerInterface" value="com.jaf.framework.core.mapper.BaseMapper" />
  <property name="basePackage" value="com.hbvtc.exam.mapper.*" />
  <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

TeacherMapper.java & BaseMapper.javasql

@RepositoryMapper
public interface TeacherMapper extends BaseMapper<Teacher> {

}

public interface BaseMapper<E extends BaseEntity<?>> {
    void insertEntity(E var1);

    void updateById(E var1);

    <T> void deleteById(T var1);

    <T> void deleteByIds(T[] var1);

    Page<E> pageQuery(Map<String, Object> var1);
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepositoryMapper {
}

這個 mapper 只有接口,沒有具體的實現類,而且這個 Mapper 接口是被 @RepositoryMapper 註解標識的,因此能夠被 mybatis 掃描到。apache

注意,這裏的 @RepositoryMapper 註解只是個普通的註解,並無被 spring @Component 註解標識,那麼 mybatis 是怎樣掃描到這些 mapper 接口,而且生成對應的實現類對象,而後註冊到 spring 容器中的呢?緩存

TeacherDaoImpl.javamybatis

@Repository("teacherDao")
public class TeacherDaoImpl extends BaseDaoImpl<Teacher> implements TeacherDao {

  // 像使用普通的 spring bean 同樣注入 mapper 類
	@Autowired
	private TeacherMapper teacherMapper;

	@Override
	protected BaseMapper<Teacher> getMapper() {
		return teacherMapper;
	}

}

上層 dao 實現類中,對 mapper 的注入像普通的 bean 同樣。app

詳細分析 mybatis 的 MapperScannerConfigurer

從上面的配置文件中能夠看到,mybatis 配置的類爲 org.mybatis.spring.mapper.MapperScannerConfigurer,也正是這個類實現了 mybatis mapper 接口的掃描,而且註冊具體的實例到 spring 容器中。ide

先看下這個類的類繼承關係圖post

MapperScannerConfigurer 類繼承關係圖

能夠看到這個類是實現了 BeanDefinitionRegistryPostProcessor 接口的,以前已經介紹過 BeanDefinitionRegistryPostProcessor 這個接口能夠在 spring 容器啓動過程當中對 Beandefinition 註冊作一些擴展。this

下面咱們直接來看 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  if (this.processPropertyPlaceHolders) {
    processPropertyPlaceHolders();
  }

  // ClassPathBeanDefinitionScanner 從 ClassPathBeanDefinitionScanner 繼承,這個類主要負責 mybatis mapper 文件的掃描
  // 掃描的基本規則就是根據以前配置文件中配置的
  // basePackage: 在哪一個包下掃描
  // markerInterface: 掃描哪一個接口,markerInterface 接口的全部實現類會被掃描到
  // annotationClass: 或者是掃描被 annotationClass 註解的全部類
  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);
  scanner.registerFilters();
  scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan

public int scan(String... basePackages) {
  int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

  doScan(basePackages);

  // Register annotation config processors, if necessary.
  if (this.includeAnnotationConfig) {
    AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
  }

  return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

#scan 方法是在父類(ClassPathBeanDefinitionScanner)中定義的,其中最主要的是 #doScan 方法。

org.mybatis.spring.mapper.ClassPathMapperScanner#doScan

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
  // 主要仍是經過 super.doScan 方法來掃描,返回全部符合條件的 BeanDefinitionHolder
  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 {
    // mybatis 對返回的 Beandefinition 進行了進一步的處理
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}

mybatis 的 ClassPathMapperScanner 類對父類中的 #doScan 方法作了重寫,但對於 Beandefinition 的掃描和註冊到 spring 容器中,主要仍是經過 super.doScan 方法來實現。

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan 方法

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) {
    // 這個方法的具體邏輯就不展開了,感興趣的能夠本身跟進下源碼
    // 主要過濾條件是經過 org.mybatis.spring.mapper.ClassPathMapperScanner#registerFilters 方法來完成註冊的
    // 過濾條件包含配置文件中配置的 annotationClass 和 markerInterface

    // 另外這裏須要注意的一點是,這裏返回的 BeanDefinition 具體的實現類是 ScannedGenericBeanDefinition,而且 BeanDefinition 屬性 beanClass 對應的是接口類(正常狀況下應該是一個具體的實現類,由於接口是沒法實例化的)
    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);
      if (candidate instanceof AbstractBeanDefinition) {
        postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
      }
      if (candidate instanceof AnnotatedBeanDefinition) {
        // 處理其餘的一些註解(@Lazy, @Primary, @DependsOn...)
        AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
      }
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        definitionHolder =
            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);

        // 向 spring 容器註冊 BeanDefinition
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

這個方法負責掃描 MapperScannerConfigurer 配置的 basePackage 下的全部符合 annotationClass 註解的接口/類,或者實現了 markerInterface 接口的接口/類,而且生成對應的 BeanDefinition,而後註冊到 spring 容器中。
到目前爲止咱們應該解決了 mybatis 是如何掃描到全部的 mapper 接口,而且註冊到 spring 容器中的問題,可是須要注意的是這裏返回的 BeanDefinition 裏面的 beanClass 屬性對應的可能接口類,一個接口是沒辦法被實例化的,也就是目前爲止掃描到的這些 BeanDefinition 在後續 spring 容器啓動過程當中沒辦法生成對應的 bean 實例。
咱們繼續回到上面的 org.mybatis.spring.mapper.ClassPathMapperScanner#doScan 這個方法,這個方法裏面還有調用了一個很重要的方法就是 org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions,具體源代碼以下(刪除了部分日誌打印相關代碼):

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (GenericBeanDefinition) holder.getBeanDefinition();

    // 這兩句代碼時關鍵,這裏將 beanClass 進行了從新設置,設置爲了 MapperFactoryBean,而且將原來接口 class 設置爲構造方法參數
    definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
    definition.setBeanClass(this.mapperFactoryBean.getClass());

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

    // 設置 sqlSessionFactory,若是有配置
    boolean explicitFactoryUsed = false;
    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;
    }

    // 設置 sqlSessionTemplate,若是有配置
    if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
      definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionTemplate != null) {
      definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
      explicitFactoryUsed = true;
    }

    // 若是 sqlSessionFactory & sqlSessionTemplate 都沒有指定,那麼啓用類型自動注入
    if (!explicitFactoryUsed) {
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}

通過這一步的處理,如今 mybatis 掃描到的全部符合條件的 BeanDefinition 都是能夠在後續 spring 容器啓動過程當中,轉換成具體 bean 的了,也就是如今的 BeanDefinition 已是能夠正常使用的了。 下面咱們再深刻一步地看下這個 MapperFactoryBean 是如何生成具體的實現類的,由於咱們的代碼中並無對 Mapper 接口生成具體的實現類,查看 org.apache.ibatis.binding.MapperProxyFactory#newInstance 源碼以下:

public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

跟着調用鏈一直追蹤,最後會調用到 org.apache.ibatis.binding.MapperRegistry#getMapper

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
  // mybatis 經過 jdk 的動態代理方式來爲全部的 mapper 接口生成一個代理實現類
  // 使用 MapperProxy 做爲動態代理的 InvocationHandler
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

到這裏就已經很是明確了,mybatis 經過 jdk 的動態代理方式來爲全部的 mapper 接口生成一個代理實現類。

順便咱們來看下 org.apache.ibatis.binding.MapperProxy#invoke 方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 若是 mapper 是一個具體的實現類,那麼直接調用實現類的方法
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (isDefaultMethod(method)) {
      // default method 是指那些例如:toString, equals 方法等
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  // 當 mapper 接口中的一個方法被調用的時候,會進入到這裏,mybatis 會對這個方法生成一個 MapperMethod 對象,而且緩存這個對象
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

org.apache.ibatis.binding.MapperMethod#execute

// mapper 接口中的 select / update 等方法最終被執行的地方
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
      // 調用了 sqlSession 的 insert / update / delete ...
    Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

至此 mybatis 對於 mapper 接口的掃描、如何註冊到 spring 容器中、如何對 mapper 接口生成動態代理實現類,以及動態代理類方法執行已經所有解析完了。

本文全部源代碼基於 mybatis:3.4.5 & mybatis-spring:1.3.1

相關文章
相關標籤/搜索