Mybatis—MappedStatement

  Mybatis通过MappedStatement来描述XML中和@Select、@Update等注解配置的SQL信息,Mybatis在解析XML配置构造Configuration的时候一并解析构造好了MappedStatement,以XMLConfigBuilder#parse为切入点,一起来分析下Mybatis是如何完成构造MappedStatement。
  XMLConfigBuilder对外提供了一个parse()方法用于解析构造Configuration对象,XMLConfigBuilder#parse方法首先判断当前XML是否正在被解析,如果没有被解析就将parsed属性设置为true,就确保一次调用;然后获取configuration标签下的所有信息;最后调用XMLConfigBuilder#mapperElement方法解析对应标签信息。
  XMLConfigBuilder#parseConfiguration方法是XMLConfigBuilder中用于解析各属性信息的一个私有方法,下述代码仅截取了构造MappedStatement的部分代码,不难看出XMLConfigBuilder#parseConfiguration方法在获取到mappers属性标签信息后,直接调用了XMLConfigBuilder#mapperElement私有方法。

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 标记为解析中,保证一次调用
    parsed = true;
    // 首先获取configuration标签下的所有信息
    // 然后调用parseConfiguration方法解析对应标签信息
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

private void parseConfiguration(XNode root) {
    try {
      // 省略部分代码
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

解析

  XMLConfigBuilder#mapperElement方法相当于一个分流器,将不同类型标识mapper的标签信息指引到所属的处理逻辑上,如下(亦可访问org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement源代码了解更多信息):

  1. 如果当前XNode节点描述的是package标签,那么执行package标签解析逻辑;
  2. 如果当前XNode节点描述的是resource标签,在获取到resource对应的信息后,执行resource标签解析逻辑;
  3. 如果当前XNode节点描述的是url标签,在获取到url对应的信息后,执行url标签解析逻辑;
  4. 如果当前XNode节点描述的是class标签,获取class对应的接口信息,然后执行class标签解析逻辑;

  虽然XMLConfigBuilder#mapperElement针对package、resource、url和class标签都有其专属的解析逻辑,但是归根结底就两个逻辑,一个是解析XML配置中的标签,另一个则是解析@Select、@Update等注解配置的信息,分别对应XMLMapperBuilder#parse和MapperAnnotationBuilder#parse。

XMLMapperBuilder#parse

  当XML配置的是resource和url节点时会使用XMLMapperBuilder#parse来解析构造MappedStatement,但是在调用XMLMapperBuilder#parse之前,需要先使用XMLMapperBuilder的构造方法构造一个XMLMapperBuilder对象。
  XMLMapperBuilder#parse处理逻辑大致分为,

  1. 判断当前资源是否已经被加载,如果已被加载直接解析返回参数、缓存和请求参数;
  2. 如果当前资源没有被加载,就获取mapper标签下所有信息,调用XMLMapperBuilder#configurationElement私有方法;
  3. 如果当前资源没有被加载,调用xml.XMLMapperBuilder#bindMapperForNamespace私有方法从命名空间中解析构造MappedStatement。
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 首先获取mapper标签信息
      // 然后解析mapper标签下所有信息  
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 从命名空间构造MappedStatement
      bindMapperForNamespace();
    }
    
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

  XMLMapperBuilder#configurationElement分别解析了cache-ref、cache、/mapper/parameterMap、/mapper/resultMap、/mapper/sql和select|insert|update|delete标签信息,可详见org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement源代码。

MapperAnnotationBuilder#parse

  当XML配置的是package和class节点时会使用XMLMapperBuilder#parse来解析构造MappedStatement,同样需要先使用MapperAnnotationBuilder构造方法构造MapperAnnotationBuilder对象,才能使用parse方法。
  从源代码来看MapperAnnotationBuilder#parse,首先会调用MapperAnnotationBuilder#loadXmlResource私有方法来通过XMLMapperBuilder#parse方法解析构造当前Mapper接口对应XML文件中的信息,在完成XML解析之后才是正在解析Mapper接口中定义的注解,详细可见org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse。

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          parseResultMap(method);
        }
        try {
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
}

  无论是使用XMLMapperBuilder#parse还是MapperAnnotationBuilder#parse来解析,最终都会通过MapperBuilderAssistant#addMappedStatement方法来构造MappedStatement对象放入configuration中的mappedStatements属性。

动态代理

  了解了Mybatis是如何完成解析构造MappedStatement之后,我们在一起来看看Mybatis是如何根据Mapper接口来动态生成真正调用类。回到XMLMapperBuilder#parse和MapperAnnotationBuilder#parse方法中调用的MapperRegistry#addMapper方法,其源代码中通过MapperProxyFactory类来注册一个Mapper代理类工厂,然后在调用MapperRegistry#getMapper方法时通过MapperProxyFactory#newInstance方法来生成代理类。

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        // 省略部分代码
        knownMappers.put(type, new MapperProxyFactory<>(type));
    }
}

  MapperProxyFactory#newInstance使用JDK动态代理Proxy类来完成生成mapper接口的代理类,这也正是Mybatis的mapper需要以接口形式存在的原因。

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,mybatis,java,后端)