mybatis源码配置文件解析之五:解析mappers标签(解析class属性)

在上篇文章中分析了mybatis解析mapper标签中的resource、url属性的过程,《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》。通过分析可以知道在解析这两个属性的时候首先解析的是对应的XML映射文件,然后解析XML映射文件中的namespace属性配置的接口,在上篇中说到该解析过程和mapper标签中的class属性的解析过程是一样的,因为class属性配置的即是一个接口的全限类名。

一、概述

在mybatis的核心配置文件中配置mappers标签有以下方式,

<mappers>
        <mapper class="cn.com.mybatis.dao.UserMapper"/> 
</mappers>

上面这种方式便是mapper标签的class属性配置方式,其解析部分过程如下,

else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }

可以看到主要是调用了configuration.addMapper方法,和上篇文章中解析namespace调用的方法是一致的。看其具体实现

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

下方分析mapperRegistry.addMapper方法。

二、详述

mapperRegistry.addMapper方法的定义如下,

 public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {//判断是否为接口
      if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
          //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It‘s important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won‘t try.
        //2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
      //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
        /**sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
         * 
         */
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
          knownMappers.remove(type);
        }
      }
    }
  }

该方法主要分为下面几个步骤。

1、检查是否解析过接口

首先会判断knowMappers中是否已经存在该接口,如果存在则会抛出异常

if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }

如果不存在则放入knownMappers中,

 //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
        knownMappers.put(type, new MapperProxyFactory<T>(type));

继续解析对应的映射文件及接口方法注解

2、解析接口对应的映射文件及接口方法注解

上面把mapper接口放入了knownMappers中,接着需要解析映射文件及注解,

//2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
        //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
        /**sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
         * 
         */
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        
        parser.parse();
        loadCompleted = true;

上面的代码,生成了一个MapperAnnotationBuilder实例,

public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    String resource = type.getName().replace(‘.‘, ‘/‘) + ".java (best guess)";
    this.assistant = new MapperBuilderAssistant(configuration, resource);
    this.configuration = configuration;
    this.type = type;

    sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
  }

给sqlAnnotationTypes和sqlProviderAnnotationTypes进行了赋值。

下面看具体的解析过程,

parser.parse();

MapperAnnotationBuilder的parse方法如下,

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {//判断是否加载过该Mapper接口
        //解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
        /**
         * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
         * 并放入mappedStatements中
         * 
         */
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      //解析接口上的@CacheNamespace注解
      parseCache();
      parseCacheRef();
      //2、获得接口中的所有方法,并解析方法上的注解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
              //解析方法上的注解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

首先判断是否加载过该资源,

if (!configuration.isResourceLoaded(resource)) {

}

只有未加载过,才会执行该方法的逻辑,否则该方法执行完毕。

public boolean isResourceLoaded(String resource) {
    return loadedResources.contains(resource);
  }

从loadResources中进行判断,判断是否解析过该Mapper接口,答案是没有解析过,则会继续解析。

1.1、解析对应的XML文件

首先会解析XML文件,调用下面的方法,

//解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
        /**
         * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
         * 并放入mappedStatements中
         * 
         */
      loadXmlResource();

看loadXmlResource方法

/**
 * 解析mapper配置文件
 */
  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        //解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
      String xmlResource = type.getName().replace(‘.‘, ‘/‘) + ".xml";
      InputStream inputStream = null;
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        //解析xml映射文件
        xmlParser.parse();
      }
    }
  }

首先进行了判断,进入if判断,看判断上的注解

// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace

第一句注解没理解什么意思,第二句的意思是方式两次加载资源,第三句是说明了该标识是在XMLMapperBuilder类中的bindMapperForNamespace中进行的设置,如下

mybatis源码配置文件解析之五:解析mappers标签(解析class属性)

为什么这样设置,后面会总结mapper的加载流程详细说明该问题。

判断之后寻找相应的XML映射文件,映射文件的文件路径如下,

//解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
      String xmlResource = type.getName().replace(‘.‘, ‘/‘) + ".xml";

从上面可以看出Mapper接口文件和XML映射文件在同一个包下,且文件名称相同(扩展名不同)。接着便是解析XML映射文件的逻辑。

if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        //解析xml映射文件
        xmlParser.parse();
      }

该逻辑和《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》的过程是一样的,调用XMLMapperBuilder的parse方法进行解析,解析的结果为MapperStatement对象。

1.2、解析接口方法上的注解

上面是解析接口对应的XML映射文件,解析完成之后,还要解析接口方法上的注解,因为mybatis的sql配置有两种方式,一种是通过XML映射文件,另一种便是注解(当SQL比较复杂建议使用映射文件的方式),下面看解析注解的过程,

//2、获得接口中的所有方法,并解析方法上的注解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
              //解析方法上的注解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }

通过反射的方式获得接口中的所有方法,遍历方法执行parseStatement方法

void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    //获得方法上的注解,并生成SqlSource
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      //生成mappedStatementId,为接口的权限类名+方法名。从这里可以得出同一个接口或namespace中不允许有同名的方法名或id
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = "id";
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

注解的解析和解析XML映射文件的方式是一样的,解析的属性是一致的。需要注意下面的注解

@SelectProvider(type=BaseUserProvider.class,method="getUser")

该注解的意思是定义select语句的提供者,需要配置type和method,即提供类的Class对象和相应的方法(返回一个字符串)

3、解析失败回退

如果在继续过程中失败或抛出异常,则进行回退,回退的意思是从knownMappers中删除该类型。

finally {
        if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
          knownMappers.remove(type);
        }
      }

因为Mapper解析的过程有两个结果一个是放入到configuration.knownMappers中的MapperProxyFactory对象,一个是放入到configuration.mappedStatements中MappedStatement对象,由于生产MappedStatement对象失败,所以要回退生成MapperProxyFactory对象过程。

三、总结

本文分析了mybatis解析<mapper class=""/>的过程,依旧是包含MapperProxyFactory和MappedStatement两个过程。

 

有不当之处,欢迎指正,感谢!

mybatis源码配置文件解析之五:解析mappers标签(解析class属性)

上一篇:Android开发者指南-运动传感器Motion Sensor


下一篇:mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)