Mybatis源码解析-7.Java注解方式配置解析

7.Java注解方式配置的mapper标签的解析

解析<mappers>标签时,我们说过,Mybatis中Mapper接口的配置方式有两种,一种是xml文件配置,就是上一节解释的那种。另一种是基于Java注解方式的配置,这种配置的解析由MapperRegistry.addMapper(Class<T> type)处理。

同样给出一个Java注解方式配置mapper的例子:

@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);

关于更多Java注解方式配置Mapper接口的细节请查看如下文档:

https://mybatis.org/mybatis-3/zh/java-api.html

本节主要讨论基于Java注解配置的解析过程,即MapperRegistry.addMapper(Class<T> type)方法。

public <T> void addMapper(Class<T> type) {
    // 如果Mapper类不是接口,则不解析
    if (type.isInterface()) {
      // 如果已经解析过也不解析
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 将Mapper标记为已被解析
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // 创建MapperAnnotationBuilder开始解析Class对象
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 解析
        parser.parse();
        loadCompleted = true;
      } finally {
        // 解析失败,则从已解析注册表中删除该Class对象
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

上面的代码有3点需要注意:

  1. Mapper必须是接口,不能是类:这是因为Mybatis使用的是Java动态代理,是基于接口的,而不是基于子类的,因此如果不是接口,则无法正常使用。
  2. 先将Mapper标记为已解析,再进行真正的解析操作:阅读了上一节文章应该清楚,XML文件解析过后会查找对应的Class对象进行再次解析,而对对应的Class对象进行解析正是通过这个方法,这里先剧透一下,Class对象解析时也会先解析对应的XML文件,因此,如果这里不进行先标记,就会造成死循环。
  3. 解析完成的接口会被放入knownMappers注册表中,该注册表是Map<Class<?>, MapperProxyFactory<?>>类型的,其中key是解析的Class对象,value是一个MapperProxyFactory<T>,T就是key表示的类型。由value的类型我们也可以看出,真正的Mapper对象是由动态代理实现的,至于具体怎么实现的,之后再看
  4. Mapper接口的解析是通过MapperAnnotationBuilder。需要注意,MapperAnnotationBuilder并不是BaseBuilder的子类。而是单独的一个工具类,用于进行基于注解的Mapper配置的解析

接下来让我们分析MapperAnnotationBuilder这个类,毕竟它是注解配置的解析类,依旧首先查看该类的属性:

// Mybatis提供了@Insert\@Select\@Update\@Delete来替代XML配置中的对应标签
// 这里就是保存这些注解的地方
private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
// Mybatis提供了@InsertProvider\@SelectProvider\@UpdateProvider\@DeleteProvider来提供XML配置中动态SQL的对应功能
private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();

// 存储该SQL配置的Configuration对象
private final Configuration configuration;
// Mapper构建协助器,还是完成它构建复杂对象并将构建结果放入到Configuration对象中的任务
private final MapperBuilderAssistant assistant;
// 解析的Class对象
private final Class<?> type;

该类有两个静态块,用于初始化SQL_ANNOTATION_TYPESSQL_PROVIDER_ANNOTATION_TYPES说实话就是把上面列出的8个注解分别存储在对应的位置。

根据MapperRegistry.addMapper(Class<T> type)方法代码可以看到,构建一个MapperAnnotationBuilder的代码如下:

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);

同样根据MapperRegistry.addMapper(Class<T> type)方法中的如下代码:

parser.parse();

可以看到,解析真正的解析逻辑在MapperAnnotationBuilder.parse()方法中。下面让我们主要研究该方法。

public void parse() {
    String resource = type.toString();
    // 判断Mapper是否进行解析
    if (!configuration.isResourceLoaded(resource)) {
      // 解析XML配置
      loadXmlResource();
      // 将XML配置标记已经解析
      configuration.addLoadedResource(resource);
      // 设置当前命名空间
      assistant.setCurrentNamespace(type.getName());
      // 解析缓存
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      // 遍历接口声明的方法,如果不是桥接方法,则将其视为需要与SQL映射的方法
      // 对其进行解析
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析未关联的方法
    parsePendingMethods();
}

可以看到,如果当前命名空间没有解析,无论怎样都会先进行XML文件的解析,然后再进行Java注解配置的解析。这里解析XML文件配置的方法是loadXmlResource()方法,考察该方法源码:

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 获取XML配置文件名称
      // 默认情况下是全限定类名 将.换成/
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // 如果不能在当前目录中查找到
      // 就去类路径加载文件
      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();
      }
    }
}

所以说默认情况下,Mappper接口对应的XML配置文件在类路径的下面,并且相对路径是Mapper接口全限定类名转化过来的。最近有一个关系贼好的老哥,把Mapper接口Mapper.xml都放在了java的一个包中,配置文件中的<mappers>标签是如下方式写的:

<mappers>
  <mapper class="fun.andre.mapper.UserMapper"/>
</mappers>

然后说怎么都找不到配置文件,这种当然找不到了,因为你将项目打包后配置文件就进入了你的jar包,根本不在类路径上啊,而在jar包里面。

因此建议乖乖写上XML配置文件位置,尽管你不写Mapper接口位置,其实问题也不大。下面开始分析具体的解析逻辑。

解析缓存

在使用Mapper的注解配置时,如果你想为Mapper配置二级缓,可以使用@CacheNamespace注解。Mapper会根据@CacheNamespace注解中的参数创建缓存。当然创建缓存依旧使用的是MapperBuilderAssistant。解析@CacheNamespace注解的方法是MapperAnnotationBuilder.parseCache()方法:

private void parseCache() {
    // 获取CacheNamespace注解
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      // 解析CacheNamespace注解参数
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      // 根据解析的参数创建缓存,并添加到Configuration中
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
}

其实这个过程与XMLMapperBuilder.cacheElement(XNode context)极其相似。这里我们把XMLMapperBuilder.cacheElement(XNode context)源码再贴在这里对比一下,其实几乎没有任何差别,只是注解配置缺少了别名相关的解析罢了:

private void cacheElement(XNode context) {
    if (context != null) {
      // 获取缓存类型
      // 默认情况下是永久缓存,没有过期时间
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 解析过期策略,默认是LRU,最近最少使用
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 设置刷新间隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      // 设置缓存大小
      Integer size = context.getIntAttribute("size");
      // 设置缓存是否只读
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      // 将所有属性按照Properties读取
      Properties props = context.getChildrenAsProperties();
      // 将读取到的属性应用到缓存中,构建缓存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

解析缓存引用

如果提到缓存,除了<cache>标签外,必定还有<cache-ref>标签,注解配置同样整合了这个标签的功能,与其对应的注解是@CacheNamespaceRef注解,该注解提供了value属性用于设置引用缓存的Mapper,解析该标签的方法是MapperAnnotationBuilder.parseCacheRef()方法,源码如下:

private void parseCacheRef() {
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
    if (cacheDomainRef != null) {
      // 获取引用缓存的命名空间对应的Class对象
      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));
      }
    }
}

这里我们可以把XMLMapperBuilder.cacheRefElement(XNode context)方法拿过来对比一下,可以发现有很大的区别:

private void cacheRefElement(XNode context) {
    if (context != null) {
      // 将当前命名空间与引用缓存的命名空间之间的关系放到`cacheRefMap`注册表中
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 让当前命名空间使用引用命名空间的缓存
        // assistant.useCacheRef(namespace);
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
}

明显我们可以发现基于注解的配置少了一步。所以综上我们可以知道了缓存的配置顺序是这样的,默认使用最后一种。

XML<cache-ref>->XML<cache>->CacheNamespace->CacheNamespaceRef

解析Statement

最后就是解析SQL语句了,其实这部分代码和XMLMapperBuilder的解析<insert>/<delete>/<update>/<select>标签部分十分相似,当然长度也相似,这里简单过一下:

void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    // 获取SQL
    // 可以对应到XML的处理include标签,虽然注解配置没有这个标签对应的功能
    // 但是都是获取完整的SQL
    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 = configuration.getDefaultResultSetType();
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;
      // 处理自动生成key问题
      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();
        if (options.resultSetType() != ResultSetType.DEFAULT) {
          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);
      }
      // 创建对应的MappedStatement
      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);
    }
  }

有兴趣的读者可以与XMLStatementBuilder.parse()方法对比一下,主要流程都相似。这里便不再赘述。

经过如此一番解析,Mybatis所有配置文件都已经存储到了Configuration对象中。接下来让我们再重新分析一遍Configuration对象,复习一下每个属性到底是做什么用的。就可以开始分析Mybatis的基本使用逻辑了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值