zoukankan      html  css  js  c++  java
  • 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中进行的设置,如下

    为什么这样设置,后面会总结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两个过程。

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

  • 相关阅读:
    Uva 11806 拉拉队 二进制+容斥原理 经典!
    CSU CHESS
    hdu 4049 Tourism Planning 状态压缩dp
    HDOJ 4661: Message Passing(找递推公式+逆元)
    HDU
    hdu4647(思路啊!)
    spoj 370. Ones and zeros(搜索+同余剪枝+链表存数(可能越界LL))
    URAL
    URAL
    hdu4614 (二分线段树)
  • 原文地址:https://www.cnblogs.com/teach/p/13208707.html
Copyright © 2011-2022 走看看