美文网首页
MyBatis印象阅读之Mapper解析(上)

MyBatis印象阅读之Mapper解析(上)

作者: 向光奔跑_ | 来源:发表于2019-07-31 10:15 被阅读0次

    之前我们还完了所有欠下的债,现在是无债一声轻。下面我们回忆下在解析XML文件资源的时候,我们还剩下最后一个解析mapper文件没有分析。今天我们就来开始进入。

    1. Mapper映射文件解析

    还记得在解析XML资源文件的最后,有一个解析Mapper文件的方法,我们来进入这个方法的源码来看下:

      private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            // 解析package的过程
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              // 解析其他情况下的过程
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              if (resource != null && url == null && mapperClass == null) {
                ErrorContext.instance().resource(resource);
                InputStream inputStream = Resources.getResourceAsStream(resource);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                mapperParser.parse();
              } else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                InputStream inputStream = Resources.getUrlAsStream(url);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                mapperParser.parse();
              } 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.");
              }
            }
          }
        }
      }
    
    

    在第一个流程中,是解析xml文件中指定的package包下的所有类,这个我们先放一放,我们先来看下面那个流程。

    这里面的逻辑看上去还是很简单的,会从xml文件中取出resource、url、mapperClass文件,当中只能有一个不为null,然后对应进行解析。其中,resource和url的解析方式差不多都是使用了:

    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    
    

    而另外一种是直接在configuration中添加Mapper,我们先来看这个简单的逻辑。

    1.1 configuration#addMapper方法解析

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

    就这么点,这里引入了一个新的实例mapperRegistry,猜想估计和之前分析的一些Registry差不多,我们来看:

    1.2 MapperRegistry源码分析

    我们先来看属性:

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

    就两个属性,但是会让我们疑惑的是这个knownMappers存的是什么?先不要着急,我们继续来看构造方法:

      public MapperRegistry(Configuration config) {
        this.config = config;
      }
    
    

    OK,这个也不用多讲,就是把让MapperRegistry持有Configuration。我们重点来关注MapperRegistry的addMapper方法。

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
          //<1> 判断是否已经存在
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          boolean loadCompleted = false;
          try {
            //<2>MapperProxyFactory何用?
            knownMappers.put(type, new MapperProxyFactory<>(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.
            //<3> 注解解析
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    
    

    我们一步步来看,首先是<1>处源码:

      public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
      }
    

    非常简单,我们继续往下看<2>处关于MapperProxyFactory是什么?

    public class MapperProxyFactory<T> {
    
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
    
      public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
      }
    
      public Class<T> getMapperInterface() {
        return mapperInterface;
      }
    
      public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
      }
    
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    
    }
    

    简单认知下,结合我们使用MyBatis的流程,这就就是Mapper接口的代理工厂类,可以通过这个类来实例化一个mapper,具体的话我们目前也不做深入,桥到船头自然直。我们继续来看<3>处源码,关于MapperAnnotationBuilder。

    1.3 MapperAnnotationBuilder

    一看这名字,知道跟我们Mapper注解方式相关,我们来看下相应源码,先来看属性:

      private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
      private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();
    
      private final Configuration configuration;
      private final MapperBuilderAssistant assistant;
      private final Class<?> type;
    
      static {
        SQL_ANNOTATION_TYPES.add(Select.class);
        SQL_ANNOTATION_TYPES.add(Insert.class);
        SQL_ANNOTATION_TYPES.add(Update.class);
        SQL_ANNOTATION_TYPES.add(Delete.class);
    
        SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
        SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
        SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
        SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
      }
    

    在这里我们见到的熟悉的@Select等注解。

    我们继续来看构造方法:

      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;
      }
    

    我们看到这里把类转化成了一个文件,然后实例化了MapperBuilderAssistant。

    1.4 MapperBuilderAssistant源码解析

    这里我们先只是简单的过一下,之后会详细详解。就只看属性和构造方法:

      //MapperBuilderAssistant
    
      private String currentNamespace;
      private final String resource;
      private Cache currentCache;
      private boolean unresolvedCacheRef; // issue #676
    
      public MapperBuilderAssistant(Configuration configuration, String resource) {
        super(configuration);
        ErrorContext.instance().resource(resource);
        this.resource = resource;
      }
    

    光看这些应该不会觉得这个类有什么难的。实际上这个类只是用来辅助解析Mapper的,这是一个辅助类。
    我们这就只看一个方法:

      public void setCurrentNamespace(String currentNamespace) {
        if (currentNamespace == null) {
          throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
        }
    
        if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
          throw new BuilderException("Wrong namespace. Expected '"
              + this.currentNamespace + "' but found '" + currentNamespace + "'.");
        }
    
        this.currentNamespace = currentNamespace;
      }
    

    这个方法一目了然,就是用来设置命名空间的,如果重复则抛出异常。我们回到之前的MapperAnnotationBuilder方法解析。其中我们调用了parse方法,我们来查看下源码。

    1.5 MapperAnnotationBuilder#parse方法源码解析

     public void parse() {
        String resource = type.toString();
        //<1> 查看是否加载过
        if (!configuration.isResourceLoaded(resource)) {
          //<2> 加载xml资源文件
          loadXmlResource();
          与<1>处对应
          configuration.addLoadedResource(resource);
          // 1.4中我们分析过了
          assistant.setCurrentNamespace(type.getName());
          // <3> 解析缓存
          parseCache();
          //<4> 解析缓存映射
          parseCacheRef();
          Method[] methods = type.getMethods();
          for (Method method : methods) {
            try {
              // issue #237
              if (!method.isBridge()) {
                //<5> 解析MappedStatement
                parseStatement(method);
              }
            } catch (IncompleteElementException e) {
              configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
          }
        }
        parsePendingMethods();
      }
    

    我们继续一步步深入,先看<1>:

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

    再来看<2>处,这里是加载xml资源的,跟我们这操作其实没多大关系,但是为了避免强迫症,我们也下进去看下:

     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())) {
          String xmlResource = type.getName().replace('.', '/') + ".xml";
          // #1347
          InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
          if (inputStream == null) {
            // Search XML mapper that is not in the module but in the classpath.
            try {
              inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
              // ignore, resource is not required
            }
          }
          if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
          }
        }
      }
    

    这里会加载不到资源,我们先粗略看下。

    之后到了第<3>步,解析缓存:

      private void parseCache() {
        CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
        if (cacheDomain != null) {
          Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
          Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
          Properties props = convertToProperties(cacheDomain.properties());
          //cacheDomain.implementation是指缓存实现,cacheDomain.eviction是指缓存策略
          assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
        }
      }
    
     public Cache useNewCache(Class<? extends Cache> typeClass,
          Class<? extends Cache> evictionClass,
          Long flushInterval,
          Integer size,
          boolean readWrite,
          boolean blocking,
          Properties props) {
        Cache cache = new CacheBuilder(currentNamespace)
            .implementation(valueOrDefault(typeClass, PerpetualCache.class))
            .addDecorator(valueOrDefault(evictionClass, LruCache.class))
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .blocking(blocking)
            .properties(props)
            .build();
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
      }
    

    这里看上去还是比较简单的,除了关于CacheNamespace的几个属性我们不太知晓之外。不过注释上我把主要的也大概讲了一下,有助于理解。

    在第<4>步中其实和上一步有点关联,我们看到上步中我们把cache放入到了configuration中,而在这步:

      private void parseCacheRef() {
        CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
        if (cacheDomainRef != null) {
          Class<?> refType = cacheDomainRef.value();
          String refName = cacheDomainRef.name();
          if (refType == void.class && refName.isEmpty()) {
            throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
          }
          if (refType != void.class && !refName.isEmpty()) {
            throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
          }
          String namespace = (refType != void.class) ? refType.getName() : refName;
          try {
            assistant.useCacheRef(namespace);
          } catch (IncompleteElementException e) {
            configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
          }
        }
      }
    
      public Cache useCacheRef(String namespace) {
        if (namespace == null) {
          throw new BuilderException("cache-ref element requires a namespace attribute.");
        }
        try {
          unresolvedCacheRef = true;
          Cache cache = configuration.getCache(namespace);
          if (cache == null) {
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
          }
          currentCache = cache;
          unresolvedCacheRef = false;
          return cache;
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
        }
      }
    

    这个代码逻辑也不复杂,也不过多赘述。

    最后一步<5>。也是最重要最复杂的一步,我们在这先不做过多处理,因为这个展开会有很多新的知识点,之后会慢慢讲述,我们这边就过一下:

    void parseStatement(Method method) {
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method);
        SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
        if (sqlSource != null) {
          Options options = method.getAnnotation(Options.class);
          final String mappedStatementId = type.getName() + "." + method.getName();
          Integer fetchSize = null;
          Integer timeout = null;
          StatementType statementType = StatementType.PREPARED;
          ResultSetType resultSetType = null;
          SqlCommandType sqlCommandType = getSqlCommandType(method);
          boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
          boolean flushCache = !isSelect;
          boolean useCache = isSelect;
    
          KeyGenerator keyGenerator;
          String keyProperty = null;
          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) {
            resultMapId = String.join(",", resultMapAnnotation.value());
          } 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);
        }
      }
    

    2. 今日总结

    今天我们分析了MyBatis针对mapper接口注解解析成MappedStatement的部分过程,希望大家能有收货~~

    相关文章

      网友评论

          本文标题:MyBatis印象阅读之Mapper解析(上)

          本文链接:https://www.haomeiwen.com/subject/nlrorctx.html