《Mybatis源码》第5章 解析Mapper接口

1.调用链

上一章我们介绍了,如何解析Mapper.xml文件,那么这次我们看一下如何解析Mapper接口,首先回顾一下mapper解析的方法mapperElement()

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 遍历
      for (XNode child : parent.getChildren()) {
        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");
          // 第一种情况  通过resource获取mapper.xml
          if (resource != null && url == null && mapperClass == null) {
            // 错误日志,见单独文章
            ErrorContext.instance().resource(resource);
            // 读取文件
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 转换
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // 第二种情况 通过url获取mapper.xml
            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) {
            // 第三种情况 通过类路径获取mapper.class
            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.");
          }
        }
      }
    }
  }

解析Mapper入口

从上面的代码我们看到,解析Mapper接口的入口有3处:

  1. 通过mappers标签配置的package子标签,解析该包下的所有接口
  2. 通过mappers标签配置的mapper子标签,并且配置的是mapperClass属性,代表直接设置了需要解析的接口
  3. 通过mappers标签配置的mapper子标签,并且配置的是url / resource属性,当解析完映射文件以后,会通过设置的namespace来解析接口

通过以上3种方式,相当于获取了需要解析的接口,然后汇总,通过configuration提供的addMapper()来解析

  // 全局配置中保存解析结果
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);  

  // 批量解析包下的接口,并且设置父类型
  public void addMappers(String packageName, Class<?> superType) {
    mapperRegistry.addMappers(packageName, superType);
  }
  // 解析包下的所有接口  默认父类是Object
  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

  // 上面的方法获取到包下所有接口  都会遍历调用该方法
  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

通过addMapper()解析,此方法属于MapperRegistry

MapperRegistry

public class MapperRegistry {

  private final Configuration config;
  // 解析过的接口通过Map来存储
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
   
  // 添加
  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 {
        // 存储到Map里面
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // 映射器注解构建器
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 解析映射接口类,因为我们的mapper接口可以添加注解 
        // 该方法解析mapper标签下的所有方法和注解,并对解析出来的信息加以封装
        parser.parse();
        loadCompleted = true;
      } finally {
        // 如果完成加载 则移除这个映射类
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
    
} 

注册方法很简单,就是判断是否为接口,然后判断是否重复加载,但是这里会通过一个 MapperAnnotationBuilder 来解析,因为我们的接口里面允许使用很多注解,这里都需要解析,所以借助该类

2.MapperAnnotationBuilder 注解解析

通过类名,顾名思义,主要的功能就是解析Mapper接口的注解,

我认为该类用的比较少,原因:

  1. 我们在解析接口的时候,会通过路径,解析出相同路径下的XML文件,但我们平常接口和XML文件分开放,所以使用较少
  2. 我们平常使用Mybatis的时候,更多的会使用XML文件,注解使用较少,因为更多的是把关于Mybatis的内容放到映射文件中

constructor

public class MapperAnnotationBuilder {
   
  // SQL注解类型
  private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>();
  // SQL提供注解类型
  private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<Class<? extends Annotation>>();
  
  // 全局配置
  private final Configuration configuration;
  // mapper构建助理类
  private final MapperBuilderAssistant assistant;
  // 类型
  private final Class<?> type;
  
  // 构造方法
  public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    // 路径 拼接结果例如: com/jianan/springtest/dao/CarMapper.java (best guess)
    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);
  }   
}

parse() 统一调度方法

// 转换方法
  public void parse() {
    // 接口地址
    String resource = type.toString();
    // 判断是否已加载
    if (!configuration.isResourceLoaded(resource)) {
      // 加载并解析指定的xml配置文件
      loadXmlResource();
      // 添加到已加载
      configuration.addLoadedResource(resource);
      // 设置当前操作空间
      assistant.setCurrentNamespace(type.getName());
      // 二级缓存设置
      parseCache();
      parseCacheRef();
      // 接口方法
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {// 不是桥接方法
            // 解析方法,生成MappedStatememt
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析出现异常的方法
    parsePendingMethods();
  }

loadXmlResource()

这个方法的意思就是判断该接口对应的xml 是否重复加载,然后通过XMLMapperBuilder加载,该加载过程在上篇文章已经讲解

这里的意思就是

  1. 通过Mapper接口,我们可以获取到XML文件,但是这两个文件必须路径相同
  2. 通过XML文件,我们可以通过namespace获取到接口,然后解析
  3. 但是前面2条则矛盾,通过接口能加载XML,通过XML能加载接口,则会造成重复加载,所以需要判断,正如下面方法英文注释介绍的,通过XMLMapperBuilder#bindMapperForNamespace 来判断
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
    // 英文翻译: 
    // Spring框架可能不知道真正的路径名,所以我们设置一个标记去预防重复加载
    // 这个标记就是 XMLMapperBuilder#bindMapperForNamespace 方法
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 这里看到,通过接口地址获取到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());
        xmlParser.parse();
      }
    }
}

parseCache() 解析缓存注解

下面代码的大概意思就是,我们配置cache缓存,可以通过在XML文件中设置标签,也可以通过在接口类上添加CacheNamespace注解,上篇文件我们介绍了通过标签获取,这里介绍的是通过注解创建

 // 通过注解获取缓存的属性 然后创建缓存
 private void parseCache() {
    // 判断接口类注解CacheNamespace
    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转换成Java的Properties
      Properties props = convertToProperties(cacheDomain.properties());
      // 创建
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }

  private Properties convertToProperties(Property[] properties) {
    if (properties.length == 0) {
      return null;
    }
    Properties props = new Properties();
    for (Property property : properties) {
      props.setProperty(property.name(),
          PropertyParser.parse(property.value(), configuration.getVariables()));
    }
    return props;
  }

parseCacheRef() 解析缓存引用注解

获取@CacheNamespaceRef 注解来获取缓存引用

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;
      assistant.useCacheRef(namespace);
    }
  }

parseStatement()

void parseStatement(Method method) {
    // 没有入参返回null,一个入参返回该入参类型,多个入参返回ParamMap
    // ParamMap为MapperMethod的静态内部类
    Class<?> parameterTypeClass = getParameterType(method);
    // 获取语言驱动
    LanguageDriver languageDriver = getLanguageDriver(method);
    // 从注解上获取SQL对象
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      // 获取@Options注解
      // 能够设置查询结果缓存时间,能够在插入数据时获得对象生成自增的主键值
      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 = 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 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();
      }
      // 解析ResultMap注解
      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);
      }
      // 同样是创建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);
    }
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为人师表好少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值