Mybatis源码解析,撸起袖子就是学

Mybatis是一个很容易上手的持久层框架,相信大家在项目中或许经常用到它,最最近读了一本⽥忠波的小小的mybatis源码分析书之后对mybatis具有了更加深入的了解,接下来我来和大家一起解剖一下mybatis。

Mybatis是什么呢?🙄

MyBatis 的前身是 iBatis,iBatis 是 Apache 软件基金会下的一个开源项目。在2020年的时候改名为Mybatis。
MyBatis 是一种半自动化的 Java 持久层框架(persistence framework),其通过注解或 XML的方式将对象和 SQL 关联起来。之所以说它是半自动的,是因为和 Hibernate 等一些可自动生成 SQL 的 ORM(Object Relational Mapping)框架相比,使用 MyBatis 需要用户自行维护 SQL。维护 SQL 的工作比较繁琐,但也有好处。比如我们可控制 SQL 逻辑,可对其进行优化,以提高效率。

闲话就省略了,既然我们要学习Mybatis的源码,就要先明白mybatis他的内部结构,去了解每个类的作用是什么。🧤

先来解释一下mybatis几个常见的类或者接口吧

  • SqlSessionFactoryBuilder :用于构建SqlSession工厂
  • XMLConfigBuilder:用来解析mybatis的xml配置文件
  • XMLMapperBuilder: 用来解析mapper映射文件
  • Configuration: mybatis主管一样,管理mtbatis配置文件信息
  • SqlSessionFactory接口:工厂当然是用来创建sqlSession
  • Executor接口:mybatis执行器,增删改查时没它可不行
  • StatementHandler: 负责处理Mybatis与JDBC之间Statement的交互
  • ParamterHandler: 负责为 PreparedStatement 的 sql 语句参数动态赋值
  • ResultSetHandler:主要使用反射围绕 resultMap 按层次结构依次解析的
    这里我们大概知道这几个类或者接口了。接下来我们从开始创建sqlsessionFactory时开始深入解析它的配置文件吧😄

配置文件解析

首先在我们使用Mybatis时会创建工厂,让工厂去创建sqlsession进行增删改查。

 SqlSessionFactory factory = new SqlSessionFactoryBuilder()
        .build(Thread.currentThread().getContextClassLoader().getResourceAsStream("configuration.xml"));

从上面一行代码我们可以明显看到,工厂构建器在创建sqlSessionFactory时是去读取了mybatis的配置文件的,让我们点进去build()看一看


SqlSessionFactory>builder()

我们将配置文件比如configuration.xml 读取转化为inputstream之后会创建XmlConfigurationBuilder进行解析,他是如何解析的呢,让我们点进去一探究竟🧐


XMLConfigurationBuilder-parse()

大家应该能够很清楚的看到这里 parseConfiguration(parser.evalNode("/configuration"));解析配置,传入的参数就是我们xml配置文件的configuration根节点。解析我们的配置文件就需要分别解析其中的各个节点。


XMLConfigurationBuilder-parseConfiguration()

我们可以看到此方法解析了许多的节点,properties,settings...等许多接下来我们来一一深入。

1. 解析<properties>节点

<properties>节点的解析工作由 propertiesElement() 这个方法完成的,在分析方法的源码前,我们先来看一下<properties>节点是如何配置的。

<properties resource="org/mybatis/example/config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="F2Fa3!33TYyg"/>
</properties>
  <!--或者读取properties文件-->
 <properties resource="database_config.properties" />

让我们看一下protiesElement()方法是如何描述的

// - 💫-XMLConfiguration 
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
      Properties defaults = context.getChildrenAsProperties();
      //获取properties节点中resource和url的值
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      //❌ 两者都不用空,则抛出异常
      //我们在写的时候要注意不可以两个路径都写上
      if (resource != null && url != null) {
        throw new BuilderException("properties元素不能同时指定URL和基于资源的属性文件引用。请指定一个或另一个.");
      }
      if (resource != null) {
        //从文件系统中加载并解析属性文件
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        //从url中加载并解析属性文件
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      // 将属性值设置到 configuration 中
      configuration.setVariables(defaults);
    }
  }

大家可以看到properties节点先读取子节点的属性,在进行扫描是否有属性文件并加以解析,如果自身子节点属性名与属性文件相同时,不会报错而是属性文件中的属性会覆盖掉子结点中的赋值。
让我们在深入看一下如何解析读取的子节点属性。

// - 💫-XNode
 public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    // 获取并遍历子节点
    for (XNode child : getChildren()) {
      // 获取 property 节点的 name 和 value 属性
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        //判断 > 设置属性到属性对象中
        properties.setProperty(name, value);
      }
    }
    return properties;
  }
// - 💫-XNode
public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<>();
    // 获取子节点列表
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
      for (int i = 0, n = nodeList.getLength(); i < n; i++) {
        Node node = nodeList.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
          // 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中
          children.add(new XNode(xpathParser, node, variables));
        }
      }
    }
    return children;
  }

总结来看解析properties节点主要分为解析子节点与读取properties属性文件,之后将我们读取到的属性信息设置到XPathParser与Configuration中。

2. 解析<setting>节点

setting当中的设置有很多,子节点也很多,如下面的代码。如果想要看详细信息大家可以到mybatis官网文档上面🤭

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

让我们看一下它在解析setting结点的时候先将setting子节点将其转换为properties对象再进行设置

Properties settings = settingsAsProperties(root.evalNode("settings"));

下面我们开始深入代码

// - 💫-XMLConfigBuilder
private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    //获取setting的子节点
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    // 创建Configuration的元信息对象
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      //检测如果元信息中没有相关属性则抛出异常
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
  }

我们在看这段代码的时候,其实大致能明白此方式是干了什么,就是先读取子节点解析为properties对象props,然后为Configuration创建元信息对象,在props通过MetaClass的检测之后返回。大家主要陌生的可能就是MetaClass,接下来我们来看一下这是什么东西🧐

//-💫-MetaClass
public class MetaClass {
  //这里又出现两个类反射器工厂与反射器。
  private final ReflectorFactory reflectorFactory;
  private final Reflector reflector;
  //这里构造方法设为私有,我们只能通过下面的forClass进行创建元信息对象
  private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
    this.reflectorFactory = reflectorFactory;
    this.reflector = reflectorFactory.findForClass(type);
  }

  public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
    return new MetaClass(type, reflectorFactory);
  }

//-💫-MetaClass
//让我们看一下它是如何确定是否含有相关属性的
public boolean hasSetter(String name) {
// 属性分词器,用于解析属性名
      PropertyTokenizer prop = new PropertyTokenizer(name);
      // hasNext 返回 true,则表明 name 是一个复合属性,后面会进行分析
      if (prop.hasNext()) {
        // 调用 reflector 的 hasSetter 方法
        if (reflector.hasSetter(prop.getName())) {
          // 为属性创建创建 MetaClass
          MetaClass metaProp = metaClassForProperty(prop.getName());
          // 再次调用 hasSetter
          return metaProp.hasSetter(prop.getChildren());
         } else {
          return false;
         }
       } else {
      // 调用 reflector 的 hasSetter 方法
      return reflector.hasSetter(prop.getName());
 } }

//
这里我们可以清楚的观察到MetaClass的hasSetter()方法其实就是调用了反射器的hasSetter()方法。元信息对象在创建的时候需要两个参数一个是Configuration对象类型,一个则是反射器工厂通过反射器工厂构造反射器进行赋值。ReflectorFactory是一个接口,我已我们需要移步到他的实现类DefaultReflectorFactory。

//-💫-DefaultRefaultorFactory
public class DefaultReflectorFactory implements ReflectorFactory {
  private boolean classCacheEnabled = true;
  private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();

  public DefaultReflectorFactory() {}
  @Override
  public boolean isClassCacheEnabled() {
    return classCacheEnabled;
  }
  @Override
  public void setClassCacheEnabled(boolean classCacheEnabled) {
    this.classCacheEnabled = classCacheEnabled;
  }
  @Override
  public Reflector findForClass(Class<?> type) {
     // classCacheEnabled 默认为 true
    if (classCacheEnabled) {
        // 从缓存中获取 Reflector 对象
        Reflector cached = reflectorMap.get(type);
        // 缓存为空,则创建一个新的 Reflector 实例,并放入缓存中
        if (cached == null) {
            cached = new Reflector(type);
            // 将 <type, cached> 映射缓存到 map 中,方便下次取用
            reflectorMap.put(type, cached);
      }
}

反射器工厂为我们构建一个反射器Reflector,这里还具有缓存的功能,如果缓存开启则会创建一个新的Reflector实例放入缓存中。对于Reflector这个类主要通过反射获取目标类的信息的,这里我们深入去了解一下。

Reflector
1. Reflector 构造方法及成员变量分析

Reflector 构造方法中包含了很多初始化逻辑,目标类的元信息解析过程也是在构造方法中完成的,这些元信息最终会被保存到 Reflector 的成员变量中。下面我们先来看看 Reflector的构造方法和相关的成员变量定义,如下:

public class Reflector {

  private final Class<?> type;
  private final String[] readablePropertyNames;  //可读属性名称数组,用于保存 getter 方法对应的属性名称
  private final String[] writablePropertyNames;  //可写属性名称数组,用于保存 setter 方法对应的属性名称
  private final Map<String, Invoker> setMethods = new HashMap<>();  //用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中,Invoke 实现类比较简单.
  private final Map<String, Invoker> getMethods = new HashMap<>();  //用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中
  private final Map<String, Class<?>> setTypes = new HashMap<>();  //用于保存 setter 对应的属性名与参数类型的映射
  private final Map<String, Class<?>> getTypes = new HashMap<>();  //用于保存 getter 对应的属性名与返回值类型的映射  
  private Constructor<?> defaultConstructor;

  private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();  //用于保存大写属性名与属性名之间的映射,比如 <NAME, name>

  public Reflector(Class<?> clazz) {
    type = clazz;
    // 解析目标类的默认构造方法,并赋值给 defaultConstructor 变量
    addDefaultConstructor(clazz);
    // 解析 getter 方法,并将解析结果放入 getMethods 中
    addGetMethods(clazz);
    // 解析 setter 方法,并将解析结果放入 setMethods 中
    addSetMethods(clazz);
    // 解析属性字段,并将解析结果添加到 setMethods 或 getMethods 中
    addFields(clazz);
    // 从 getMethods 映射中获取可读属性名数组
    readablePropertyNames = getMethods.keySet().toArray(new String[0]);
    // 从 setMethods 映射中获取可写属性名数组
    writablePropertyNames = setMethods.keySet().toArray(new String[0]);
    // 将所有属性名的大写形式作为键,属性名作为值,
    // 存入到 caseInsensitivePropertyMap 中
    for (String propName : readablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    for (String propName : writablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
  }
2. getter方法解析

getter方法的逻辑被封装到addGetMethods方法中,方法中除了解析getXXX方法还有isXXX方法。源码分析如下:

      private void addGetMethods(Class<?> clazz) {
      Map<String, List<Method>> conflictingGetters = new HashMap<>();
      // 获取当前类,接口,以及父类中的方法。
      Method[] methods = getClassMethods(clazz);
      for (Method method : methods) {
            // getter 方法不应该有参数,若存在参数,则忽略当前方法
            if (method.getParameterTypes().length > 0) {
            continue;
       }
      String name = method.getName();
      // 过滤出以 get 或 is 开头的方法
      if ((name.startsWith("get") && name.length() > 3)|| (name.startsWith("is") && name.length() > 2)) {
      // 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name
      name = PropertyNamer.methodToProperty(name);
     /*
     * 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:
     * 
     * getTitle 和 isTitle 两个方法经过 methodToProperty 处理,
     * 均得到 name = title,这会导致冲突。
     *
     * 对于冲突的方法,这里先统一起存起来,后续再解决冲突
     */
     resolveGetterConflicts(conflictingGetters);
  }

      /** 添加属性名和方法对象到冲突集合中 */
      private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
      List<Method> list = conflictingMethods.get(name);
      if (list == null) {
          list = new ArrayList<Method>();
      conflictingMethods.put(name, list);
       }
      list.add(method);
    }

addGetMethods 方法的代码不是很多,但是逻辑有点多。这里总结一下:

  1. 获取当前类,接口,以及父类中的方法
  2. 遍历上一步获取的方法数组,并过滤出以 get 和 is 开头的方法
  3. 将方法名转换成相应的属性名
  4. 将属性名和方法对象添加到冲突集合中
  5. 解决冲突
    上面得代码很简单,主要就是第五步解决冲突大家可能会有些疑惑,冲突就类似当两个方法isTree和getTree他获取到得属性名都是tree,此时他会放入冲突集合当中去解决冲突。下面我们来看一下解决冲突代码:
//-💫-Reflector
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
    for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
      Method winner = null;
      String propName = entry.getKey();
      boolean isAmbiguous = false;
      for (Method candidate : entry.getValue()) {
        if (winner == null) {
          winner = candidate;
          continue;
        }
        Class<?> winnerType = winner.getReturnType();
        Class<?> candidateType = candidate.getReturnType();
/* 
* 两个方法的返回值类型一致,若两个方法返回值类型均为 boolean,
* 则选取 isXXX 方法为 winner。否则无法决定哪个方法更为合适, * 只能抛出异常
*/
        if (candidateType.equals(winnerType)) {
          if (!boolean.class.equals(candidateType)) {
            isAmbiguous = true;
            break;
          } else if (candidate.getName().startsWith("is")) {
            winner = candidate;
          }
        } else if (candidateType.isAssignableFrom(winnerType)) {
          // OK getter type is descendant
        } else if (winnerType.isAssignableFrom(candidateType)) {
          winner = candidate;
        } else {
          isAmbiguous = true;
          break;
        }
      }
      addGetMethod(propName, winner, isAmbiguous);
    }
  }

private void addGetMethod(String name, Method method) {
    if (isValidPropertyName(name)) {
      getMethods.put(name, new MethodInvoker(method));
      // 解析返回值类型
     Type returnType = TypeParameterResolver.resolveReturnType(method, type);
      // 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中
     getTypes.put(name, typeToClass(returnType));
 } }

这些就是解决冲突的代码,大致可以理解为几个规则

  1. 冲突方法返回值类型具有继承关系,子类返回值对应方法被认为是更合适的选择
  2. 冲突方法的返回值类型相同,如果返回值类型为 boolean,那么以 is 开头的方法则是更合适的选择
  3. 冲突方法的返回值类型相同,但类型非 boolean,此时出现歧义,抛出异常
  4. 冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常
3. 解决setter

setter与getter大致相同,主要问题是setter遇到冲突可能是方法重载的问题即传入得参数可能不一致,所以这里我们需要了解一下setter解决冲突的办法:

private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
    for (String propName : conflictingSetters.keySet()) {
      List<Method> setters = conflictingSetters.get(propName);
      /*
      * 获取 getter 方法的返回值类型,由于 getter 方法不存在重载的情况,
      * 所以可以用它的返回值类型反推哪个 setter 的更为合适
      */
      Class<?> getterType = getTypes.get(propName);
      boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
      boolean isSetterAmbiguous = false;
      Method match = null;
      for (Method setter : setters) {
          //参数类型和返回类型一致,则认为是最好的选择,并结束循环
        if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
          // should be the best match
          match = setter;
          break;
        }
        if (!isSetterAmbiguous) {
          match = pickBetterSetter(match, setter, propName);
          isSetterAmbiguous = match == null;
        }
      }
      if (match != null) {
        addSetMethod(propName, match);
      }
    }
  }


 private void addSetMethod(String name, Method method) {
    MethodInvoker invoker = new MethodInvoker(method);
    setMethods.put(name, invoker);
      // 解析参数类型列表
    Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
      // 将参数类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes
    setTypes.put(name, typeToClass(paramTypes[0]));
  }

关于 setter 方法冲突的解析规则,这里总结一下:

  1. 冲突方法的参数类型与 getter 的返回类型一致,则认为是最好的选择
  2. 冲突方法的参数类型具有继承关系,子类参数对应的方法被认为是更合适的选择
  3. 冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常

我们来会看一下代码在解析setting结点的时候,我们创建了configuration得元信息对象MetaClass,调用其中的hasSetter来查看是否还有相关属性,其实调用的是其属性当中Reflector的hasSetter()来进行判断的:

/**
//-💫-Reflector
   * 按名称检查类是否具有可写属性。.
   *
   * @param propertyName - 要检查的属性的名称
   * @return True 如果对象的名称具有可写属性
   */
  public boolean hasSetter(String propertyName) {
    return setMethods.keySet().contains(propertyName);
  }

大家可能没有发现,在MetaClass调用hasSetter时候出现了一个新的类PropertyTokenizer(属性标记器),什么时候使用呢,当然是处理一些复杂的属性时我们需要去使用它。比如类中引用其他类作为属性或者使用集合作为属性的时候我们就需要去处理这些。让我们看一下它的源码是怎么写的:

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
  private String name;
  private final String indexedName;
  private String index;
  private final String children;

  public PropertyTokenizer(String fullname) {
    //检测参数中是否包含字符‘.’
    int delim = fullname.indexOf('.');
    if (delim > -1) {
      /**通过'.'进行分割比如:
      *      dullname = itlzq.cn
      *      name = itlzq
      *      children = cn
      */   
      name = fullname.substring(0, delim);
      children = fullname.substring(delim + 1);
    } else {
      // 没有‘.’的话则直接赋给name
      name = fullname;
      children = null;
    }
    indexedName = name;
    // 检测参数中是否包含' [ '
    delim = name.indexOf('[');
    if (delim > -1) {
      /*
      * 获取中括号里的内容,比如:
      * 1. 对于数组或 List 集合:[] 中的内容为数组下标,
      * 比如 fullname = articles[1],index = 1
      * 2. 对于 Map:[] 中的内容为键,
      * 比如 fullname = xxxMap[keyName],index = keyName
      */
      index = name.substring(delim + 1, name.length() - 1);
      name = name.substring(0, delim);
    }
  }
  @Override
  public PropertyTokenizer next() {
    return new PropertyTokenizer(children);
  }

  @Override
  public void remove() {
    throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
  }
}

中间写了点注释,相信大家看了之后应该能明白👍。下来我们来看一下它是如何设置setting的

设置setting到Configuration中

先来看一下方法,


settingElement

这里大部分代码都一样的,我们这里看一下调用的 resolveClass这个方法,源码如下:

// -☆- BaseBuilder
protected Class<?> resolveClass(String alias) {
    if (alias == null) {
        return null;
     }
    try {
        // 通过别名解析
        return resolveAlias(alias);
         } catch (Exception e) {
              throw new BuilderException("Error resolving class. Cause: " + e, e);
         } }
        protected final TypeAliasRegistry typeAliasRegistry;
        protected Class<?> resolveAlias(String alias) {
            // 通过别名注册器解析别名对于的类型 Class
            return typeAliasRegistry.resolveAlias(alias);
        }

这里出现了一个新的类 TypeAliasRegistry,大家对于它可能会觉得陌生,但是对于typeAlias 应该不会陌生。TypeAliasRegistry 的用途就是将别名和类型进行映射,这样就可以用别名表示某个类了,方便使用。既然聊到了别名,那下面我们不妨看看别名的配置的解析过程。

3.解析<typeAliases>节点

在 MyBatis 中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输入别名即可,无需再把全限定的类名写出来。
第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合 Alias 注解使用,即通过注解为某个类配置别名,而不是让 MyBatis 按照默认规则生成别名。这种方式的配置如下:

    <typeAliases>
        <package name="domain"/>
    </typeAliases>

第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:

    <typeAliases>
        <typeAlias type="domain.Student" alias="stu"/>
    </typeAliases>

对比这两种方式,第一种自动扫描的方式配置起来比较简单,缺点也不明显。唯一能想到缺点可能就是 MyBatis 会将某个包下所有符合要求的类的别名都解析出来,并形成映射关系。如果你不想让某些类被扫描,这个好像做不到,没发现 MyBatis 提供了相关的排除机制。不过我觉得这并不是什么大问题,最多是多解析并缓存了一些别名到类型的映射,在时间和空间上产生了一些的消耗而已。当然,如果无法忍受这些消耗,可以使用第二种配置方式,通过手工的方式精确配置某些类型的别名。不过这种方式比较繁琐,特别是配置项比较多时。至于两种方式怎么选择,这个看具体的情况了。配置项非常少时,两种皆可。比较多的话,还是让 MyBatis 自行扫描吧。接下来我们来看一下两种不同的配置是如何解析的:

  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //⭐ 从指定的包中解析别名和类型的映射
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        // ⭐ 从 typeAlias 节点中解析别名和类型的映射
        } else {
          // 获取 alias 和 type 属性值,alias 不是必填项,可为空
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            // 加载 type 对应的类型
            Class<?> clazz = Resources.classForName(type);
            // 注册别名到类型的映射
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }

1.从指定的包中解析并注册别名

// -☆- TypeAliasRegistry
public void registerAliases(String packageName) {
    //调用内部重载方法
    registerAliases(packageName, Object.class);
  }

 public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
    // superType = Object.class,所以 ResolverUtil 将查找所有的类。
    // 查找完成后,查找结果将会被缓存到内部集合中。
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    //获取查找结果
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for (Class<?> type : typeSet) {
      // 忽略匿名类,接口,内部类
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        // 为类型注册别名
        registerAlias(type);
      }
    }
  }

总结来看,扫描包的步骤就两步一是查找指定包下的所有类;二是遍历查找到的类型集合,为每个类型注册别名。其中最后注册别名没有详细为大家讲,我们接下来说从<typeAlias>节点中解析并注册别名来详细说一下此方法。

2. 从<typeAlias>节点中解析并注册别名

在别名的配置中,type 属性是必须要配置的,而 alias 属性则不是必须的。。如果使用者未配置 alias 属性,则需要 MyBatis 自行为目标类型生成别名。对于别名为空的情况,注册别名的任务交由 registerAlias(Class<?>) 方法处理。若不为空,则由 registerAlias(String,Class<?>) 进行别名注册。代码如下:

//-☆- TypeAliasRegistry
public void registerAlias(Class<?> type) {
    // 获取全路径类名的简称
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      // 从注解中取出别名
      alias = aliasAnnotation.value();
    }
     // 调用重载方法注册别名和类型映射
    registerAlias(alias, type);
  }

  public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // 将别名转成小写
    String key = alias.toLowerCase(Locale.ENGLISH);
    // 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型
    // 是否一致,不一致则抛出异常,不允许一个别名对应两种类型
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    //缓存别名类型
    typeAliases.put(key, value);
  }

若用户未明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。若类中有@Alias 注解,则从注解中取值作为别名。
别名解析并不是很难让我们来看一下大致步骤:

  1. 通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,
  2. 比如 domain/Student.class
  3. 筛选以.class 结尾的文件名
  4. 将路径名转成全限定的类名,通过类加载器加载类名
  5. 对类型进行匹配,若符合匹配规则,则将其放入内部集合中
3. mybatis内部当中注册的别名
//-☆- Configuration
public Configuration() {
    // 注册事务工厂的别名
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    // 注册数据源的别名
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    // 注册缓存策略的别名
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);  //先进先出
    typeAliasRegistry.registerAlias("LRU", LruCache.class);   //最少使用
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);  //软引用缓存
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);  //弱引用缓存

    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

    // 注册日志类的别名
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    // 注册动态代理工厂的别名
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }

// -☆- TypeAliasRegistry
 public TypeAliasRegistry() {
    registerAlias("string", String.class);
    //这里配置都是我们经常使用了一些类型
    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);
    .··· 省略  ···

别名解析其实很简单,大家应该都明白了,让我们继续往下看。

4.解析<plugins>节点

插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)
    如何使用呢,举个例子:
// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

解析过程如下:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        // 获取配置信息
        Properties properties = child.getChildrenAsProperties();
        // 解析拦截器的类型,并创建拦截器
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        // 设置属性
        interceptorInstance.setProperties(properties);
        // 添加拦截器到 Configuration 中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

代码就这么多,插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。🌚

5. 解析<environments>节点

MyBatis 中,事务管理器(transactionManager)和数据源(dataSource)是配置在<environments>节点下的环境变量(environment)节点下的。
⭐即使environments节点下可以写多个环境,但是我们每个 SqlSessionFactory 实例只能选择一种环境。
如何配置呢,我们来看一下

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意一些关键点:

  • 默认使用的环境 ID(比如:default="development")。
  • 每个 environment 元素定义的环境 ID(比如:id="development")。
  • 事务管理器的配置(比如:type="JDBC")。
  • 数据源的配置(比如:type="POOLED")。
    默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。
    让我们看一下是如何解析的
 private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null)
        // 获取 default 属性
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        // 获取 id 属性
        String id = child.getStringAttribute("id");
        // 检测当前 environment 节点的 id 与其父节点 environments 的
        // 属性 default 内容是否一致,一致则返回 true,否则返回 false
        if (isSpecifiedEnvironment(id)) {
          // 解析 transactionManager 节点
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 解析 dataSource 节点,逻辑和插件的解析逻辑很相似
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          // 创建 DataSource 对象
          DataSource dataSource = dsFactory.getDataSource();
          // 构建 Environment 对象,并设置到 configuration 中
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

6.解析<typeHandlers>节点

在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有 CHAR 和 VARCHAR 等类型,但 Java 中没有这些类型,不过 Java 有 String类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成String。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler 去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。下面,我们来看一下类型处理器的配置方法:

<!-- 自动配置 -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>
<!-- 手动配置 -->
<typeHandlers>
  <typeHandler jdbcType="TINYINT" javaType="enum.StudentTypeEnum" handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

使用自动扫描的方式注册类型处理器时,应使用@MappedTypes 和@MappedJdbcTypes注解配置 javaType 和 jdbcType。关于注解,这里就不演示了,比较简单,大家自行尝试。下面开始分析代码。

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 从指定的包中注册 TypeHandler
        if ("package".equals(child.getName())) {
          //自动配置扫描
          String typeHandlerPackage = child.getStringAttribute("name");
          // 注册方法 ①
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          //手动设置
          // 从 typeHandler 节点中解析别名到类型的映射
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          // 解析上面获取到的属性值
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          // 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              // 注册方法 ②
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              // 注册方法 ③
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
              // 注册方法 ④
              typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }

上面注释出现了四种注册方法,这其实就是一个重载方法,接下来我们来分别看一下。
1. register(String packageName)
用于自动扫描类型处理器,并调用其他方法注册扫描结果。

  public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    //传入包名,从指定包中查找 TypeHandler
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
       // 忽略内部类,接口,抽象类等
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
        // 调用注册方法 ④
        register(type);
      }
    }
  }

2. register(Class<?> typeHandlerClass)
刚刚我们在第一部调用了register,未配置 javaType 和 jdbcType 属性的值的时候我们就会调用此方法,接下来我们来看一下他的代码

  // Only handler type
  public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
     // 获取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        // 遍历 @MappedTypes 注解中配置的值
      for (Class<?> javaTypeClass : mappedTypes.value()) {
        // 调用注册方法 ②
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      // 调用中间方法 register(TypeHandler)
      register(getInstance(null, typeHandlerClass));
    }
  }


 public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    // 获取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> handledType : mappedTypes.value()) {
        // 调用中间方法 register(Type, TypeHandler)
        register(handledType, typeHandler);
        mappedTypeFound = true;
      }
    }
    // 自动发现映射类型
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
      try {
        TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
        // 获取参数模板中的参数类型,并调用中间方法 register(Type, TypeHandler)
        register(typeReference.getRawType(), typeHandler);
        mappedTypeFound = true;
      } catch (Throwable t) {
        // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
      }
    }
    if (!mappedTypeFound) {
    // 调用中间方法 register(Class, TypeHandler)
      register((Class<T>) null, typeHandler);
    }
  }

3. register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)

public void register(Class<?> javaTypeClass, 
    JdbcType jdbcType, Class<?> typeHandlerClass) {
    // 调用终点方法
    register(javaTypeClass, jdbcType,getInstance(javaTypeClass, typeHandlerClass));
}

/** 类型处理器注册过程的终点 */
private void register(Type javaType, 
    JdbcType jdbcType, TypeHandler<?> handler) {
      if (javaType != null) {
      // JdbcType 到 TypeHandler 的映射
      Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
      map = new HashMap<JdbcType, TypeHandler<?>>();
      // 存储 javaType 到 Map<JdbcType, TypeHandler> 的映射
      TYPE_HANDLER_MAP.put(javaType, map);
       }
      map.put(jdbcType, handler);
 }
    // 存储所有的 TypeHandler
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

**4.register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
当代码执行到此方法时,表示 javaTypeClass != null && jdbcType == null 条件成立,即使用者仅设置了 javaType 属性的值。

 public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }

XML映射文件解析

mybatis好的地方之一就是因为他的语句映射,官网也明确说了映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。它的映射文件其实很简单元素并不是很多,主要有

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

解析入口

我们在解析xml配置文件的时候解析各个节点,这些节点中也包括了我们的mappers>mapper节点,让我们来看一下他的代码:

// -☆- XMLConfigBuilder
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //判断是否是<package>节点
        if ("package".equals(child.getName())) {
          // 获取 <package> 节点中的 name 属性
          String mapperPackage = child.getStringAttribute("name");
          // 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置
          configuration.addMappers(mapperPackage);
        } else {
          // 获取 resource/url/class 等属性
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          //如果resource 不为空,且其他两者为空,则从指定路径中加载配置
          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();
           // url 不为空,且其他两者为空,则通过 url 加载配置
          } 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();
            // mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置
          } 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.");
          }
        }
      }
    }
  }

大家看完这一段代码大概也明白了,mybatis解析mapper基本上就分为4种方式,
① 第一种先扫描有没有package节点,扫描包下面的所有类,为每个类解析映射信息;
② 从文件系统加载映射文件
③ 从url路径中加载映射文件
④ 最后一种,通过mapper接口加载映射文件,映射信息可以写在注解或者映射文件中。
mybatis通过配置注解映射信息有一定的局限性,官网文档中也写道:
Java 注解的表达力和灵活性十分有限。尽管很多时间都花在调查、设计
和试验上,最强大的MyBatis映射并不能用注解来构建——并不是在开玩笑,的确是这样。

所以通过注解并不能发挥出mybatis的能力,如果大家想要使用注解又想使用mybatis的话我推荐大家使用mybatisplus,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。相信大家会喜欢🤭。
接下来就带大家看一看这个节点是如何解析的。


XMLMapperConfigBuilder

接下里我们分别进入这几个节点里面观察是如何解析的🧐:

1.解析<cache>节点-该命名空间的缓存配置

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。默认情况下,只启用了本地的会话缓存SqlSession,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行。

<cache/>

这么短几个字母有什么用呢,官方文档已经表明了😱:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
    我们也可以修改缓存的一些属性,比如创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的缓存:
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

我们也可以进行配置第三方缓存设置type,property而整合到mybatis中去,关于缓存的详细知识我们后面再进行阐述。先让我们看一下他是怎么解析的:


 private void cacheElement(XNode context) {
    if (context != null) {
      // 获取各种属性
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      //默认使用最少使用策略
      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 props = context.getChildrenAsProperties();
       // 构建缓存对象
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
//二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

接下来我们发现有一个新方法,就是缓存对象的构建逻辑封装在 BuilderAssistant 类的 useNewCache (),下面我们来看一下该方法的逻辑:

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 对象中
    configuration.addCache(cache);
    // 设置 currentCache 遍历,即当前使用的缓存
    currentCache = cache;
    return cache;
  }

上面用了构建模式构造缓存实例,那它到底是如何构造的呢让我们来一探究竟👺:

public Cache build() {
    // 设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    // 通过反射创建缓存
    setCacheProperties(cache);
   // 仅对内置缓存 PerpetualCache 应用装饰器
    if (PerpetualCache.class.equals(cache.getClass())) {
      // 遍历装饰器集合,应用装饰器
      for (Class<? extends Cache> decorator : decorators) {
        // 通过反射创建装饰器实例
        cache = newCacheDecoratorInstance(decorator, cache);
        // 设置属性值到缓存实例中
        setCacheProperties(cache);
      }
      // 应用标准的装饰器,比如 LoggingCache、SynchronizedCache
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      // 应用具有日志功能的缓存装饰器
      cache = new LoggingCache(cache);
    }
    return cache;
  }

缓存的构建大致为先设置默认的缓存类型以及装饰器,再接着应用装饰器到PerpetualCache对象上,然后应用标准装饰器,最后就是判断是否为日志类型的缓存应用LoggingCache装饰器。这里面调用了许多装饰过的方法,让我们来分开看一看。

setDefaultImplementations()
 private void setDefaultImplementations() {
      //这里类中属性implementation没有设置为空,decorators(空的ArrayList)都没有进行赋值所以默认执行下面的代码
    if (implementation == null) {
      // 设置默认的缓存实现类
      implementation = PerpetualCache.class;
      if (decorators.isEmpty()) {
         //// 添加 LruCache 装饰器
        decorators.add(LruCache.class);
      }
    }
  }
setCacheProperties(cache)
 private void setCacheProperties(Cache cache) {
    if (properties != null) {
      // 为缓存实例生成一个“元信息”实例,forObject 方法调用层次比较深,
      // 但最终调用了 MetaClass 的 forClass 方法。
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      for (Map.Entry<Object, Object> entry : properties.entrySet()) {
        String name = (String) entry.getKey();
        String value = (String) entry.getValue();
        if (metaCache.hasSetter(name)) {
          // 获取 setter 方法的参数类型
          Class<?> type = metaCache.getSetterType(name);
          // 根据参数类型对属性值进行转换,并将转换后的值
          // 通过 setter 方法设置到 Cache 实例中
          if (String.class == type) {metaCache.setValue(name, value);
          } else if (int.class == type|| Integer.class == type) {
            /*
             * 此处及以下分支包含两个步骤:
             * 1.类型转换 → Integer.valueOf(value)
             * 2.将转换后的值设置到缓存实例中 → 
             * metaCache.setValue(name, value)
             */
            metaCache.setValue(name, Integer.valueOf(value));
          } else if (long.class == type || Long.class == type) {
            metaCache.setValue(name, Long.valueOf(value));
          } else if (short.class == type|| Short.class == type) {
            metaCache.setValue(name, Short.valueOf(value));
          } else if (byte.class == type|| Byte.class == type) {
            metaCache.setValue(name, Byte.valueOf(value));
          } else if (float.class == type|| Float.class == type) {
            metaCache.setValue(name, Float.valueOf(value));
          } else if (boolean.class == type || Boolean.class == type) {
            metaCache.setValue(name, Boolean.valueOf(value));
          } else if (double.class == type|| Double.class == type) {
            metaCache.setValue(name, Double.valueOf(value));
          } else {
            throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
          }
        }
      }
    }
    // 如果缓存类实现了 InitializingObject 接口,
    // 则调用 initialize 方法执行初始化逻辑
    if (InitializingObject.class.isAssignableFrom(cache.getClass())) {
      try {
        ((InitializingObject) cache).initialize();
      } catch (Exception e) {
        throw new CacheException("Failed cache initialization for '"
          + cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
      }
    }
  }
setStandardDecorators(Cache cache)
private Cache setStandardDecorators(Cache cache) {
    try {
      // 创建“元信息”对象
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        // 设置 size 属性
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
         // clearInterval 不为空,应用 ScheduledCache 装饰器
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        // readWrite 为 true,应用 SerializedCache 装饰器
        cache = new SerializedCache(cache);
      }
      /*
      * 应用 LoggingCache,SynchronizedCache 装饰器,
      * 使原缓存具备打印日志和线程同步的能力
      */
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      // blocking 为 true,应用 BlockingCache 装饰器
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

以上代码用于为缓存应用一些基本的装饰器,除了 LoggingCache 和 SynchronizedCache这两个是必要的装饰器,其他的装饰器应用与否,取决于用户的配置。

2.解析<cache-ref>节点

在Mybatis中,二级缓存是可以公用的。这里需要通过此节点为命名空间配置参照缓存,可以引用别的映射文件中的缓存:
举个栗子🕊:

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

接下来,我们来看一下cache-ref节点的解析过程:

//-💫-XMLMapperBuilder
  private void cacheRefElement(XNode context) {
    if (context != null) {
      //添加缓存引用配置
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
         //解析缓存节点
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }

主要就是添加此命名空间引用的缓存创建缓存引用,只用解析缓引用节点:

//-💫-CacheRefResolver
 public Cache resolveCacheRef() {
    return assistant.useCacheRef(cacheRefNamespace);
  }

//-💫-MapperBuilderAssistant
public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      //根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
      Cache cache = configuration.getCache(namespace);
      //判断引用的缓存实例不存在为空则抛出异常
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      //设置cache为当前使用缓存 
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

缓存引用并不难理解,通俗来讲主要就是找别人的缓存然后判断是否存在加以使用。

3.解析<resultMap>节点

resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

这一段是官方给我们的描述,大家使用mybatis肯定对resultMap都不陌生吧,他就是在我们设置返回结果的时候设置,大部分情况都是我们的返回对象中嵌套着别的对象,存在着一对多,一对一等复杂的情况时我们需要自己去配置

一个简单的resultMap

  • constructor - 用于在实例化类时,注入结果到构造方法中
    - idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    - arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
    association – 一个复杂类型的关联;许多结果将包装成这种类型
    - 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的
  • 引用collection – 一个复杂类型的集合
    - 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的
  • 引用discriminator – 使用结果值来决定使用哪个 resultMap
    - case – 基于某些值的结果映射嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
//-💫-XMLMapperBuilder
  private void resultMapElements(List<XNode> list) throws Exception {
    //遍历获取的所有resultMap
    for (XNode resultMapNode : list) {
      try {
        //解析每个resultMap节点
        resultMapElement(resultMapNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }
    
  private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    //内部重载方法
    return resultMapElement(resultMapNode, Collections.emptyList(), null);
  }

//正经解析在这里🙄
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获取 type 属性
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
     //解析 type 属性对应的类型,并返回对应类型实例
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
      typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>();
    //我们从上面传参的时候可以看到传入一个空的集合
    resultMappings.addAll(additionalResultMappings);
    //获取该节点的子节点
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      //获取构造方法
      if ("constructor".equals(resultChild.getName())) {
      // 解析 constructor 节点,并生成相应的 ResultMapping
      //参数分别是constructor节点,对应类型class,resultMappings这是一个空的集合传入引用,之后需要去添加映射结果到集合中
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        // 解析 discriminator 节点
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    //获取id(id值),ectend(继承),autoMapping(自动映射,默认未设置)
    //如果属性内没有id值比如association节点执行getValueBasedIdentifier()
    String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    //创建一个结果映射解析者用于解析resultMap并返回
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      // 根据前面获取到的信息构建 ResultMap 对象
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

这一段代码挺长的总结一下:

  • 1.先获取节点各种属性属性type,解析type获取实例class
  • 2.获取并遍历resultMap的子节点,判断子节点名称相对应解析
  • 3.创建结果映射集合,在解析之后构建ResultMap
  • 4.异常的时候会将resultMapResolver添加到incompleteResultMaps集合中。
    这下我们带着疑惑再去探究一下这些子节点是如何进行解析的🤨:

解析<id>和<result>节点

 private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    //判断节点是否是constructor以下的,获取name属性或者property  
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
      property = context.getStringAttribute("property");
    }
    // 获取节点其他各种属性
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    // 解析 resultMap 属性,该属性出现在 <association> 和 <collection> 节点中。
    // 若这两个节点不包含 resultMap 属性,则调用processNestedResultMappings 方法
    // 解析嵌套的 resultMap。
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    // 解析 javaType、typeHandler 的类型以及枚举类型 JdbcType
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 构建 ResultMapping 对象
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }

上面代码主要获取id以及result节点的属性,然后如果我们关联属性一对一association或者一对多collection的时候我们需要去进行进一步判断。之后做获取完各个属性之后构建并返回ResultMapping对象。
processNestedResultMappings(XNode , List<ResultMapping>, Class)

//-💫-XMLMapperBuilder
 private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
     // 判断节点名称
    if ("association".equals(context.getName())
        || "collection".equals(context.getName())
        || "case".equals(context.getName())) {
      if (context.getStringAttribute("select") == null) {
        // resultMapElement 是解析 ResultMap 入口方法
        validateCollection(context, enclosingType);
        ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
        return resultMap.getId();
      }
    }
    return null;
  }

  //验证集合
  protected void validateCollection(XNode context, Class<?> enclosingType) {
    if ("collection".equals(context.getName()) && context.getStringAttribute("resultMap") == null
        && context.getStringAttribute("javaType") == null) {
      MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
      String property = context.getStringAttribute("property");
      //判断如果不能进行设置提示指定javaType或者resultMapper类型
      if (!metaResultType.hasSetter(property)) {
        throw new BuilderException(
          "Ambiguous collection type for property '" + property + "'. You must specify 'javaType' or 'resultMap'.");
      }
    }
  }

看完节点解析之后我们看看一下ResultMapping是如何构建的

//-💫-MapperBuilderAssistant
 public ResultMapping buildResultMapping(
      Class<?> resultType,String property,String column,Class<?> javaType,
      JdbcType jdbcType,String nestedSelect,String nestedResultMap,String notNullColumn,
      String columnPrefix,Class<? extends TypeHandler<?>> typeHandler,
      List<ResultFlag> flags,String resultSet,String foreignColumn,boolean lazy) {
    // 若 javaType 为空,这里根据 property 的属性进行解析。关于下面方法中的参数,
   // - resultType:即 <resultMap type="xxx"/> 中的 type 属性
   // - property:即 <result property="xxx"/> 中的 property 属性
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    // 解析 TypeHandler
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    // 解析 column = {property1=column1, property2=column2} 的情况,
    // 这里会将 column 拆分成多个 ResultMapping
    List<ResultMapping> composites;
    if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
      composites = Collections.emptyList();
    } else {
      composites = parseCompositeColumnName(column);
    }
    // 通过建造模式构建 ResultMapping
    return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
        .jdbcType(jdbcType)
        .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
        .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
        .resultSet(resultSet)
        .typeHandler(typeHandlerInstance)
        .flags(flags == null ? new ArrayList<>() : flags)
        .composites(composites)
        .notNullColumns(parseMultipleColumnNames(notNullColumn))
        .columnPrefix(columnPrefix)
        .foreignColumn(foreignColumn)
        .lazy(lazy)
        .build();
  }

 public ResultMapping build() {
      // lock down collections
      // 将 flags 和 composites 两个集合变为不可修改集合
      resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
      resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
      // 从 TypeHandlerRegistry 中获取相应 TypeHandler
      resolveTypeHandler();
      validate();
      return resultMapping;
    }

ResultMapping的构建过程就是先解析javaType类型,创建typeHandler实例处理符合复合column,构建ResultMappping实例。

解析<constructor>节点

  private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
    // 获取子节点列表
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
      List<ResultFlag> flags = new ArrayList<>();
      // 向 flags 中添加 CONSTRUCTOR 标志 1
      flags.add(ResultFlag.CONSTRUCTOR);
      if ("idArg".equals(argChild.getName())) {
        // 向 flags 中添加 ID 标志
        flags.add(ResultFlag.ID);
      }
      // 构建 ResultMapping,上面已经分析过
      resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
  }

解析完这些之后我们会获取到ResultMapping的集合,拿到这些单挑映射结果之后我们如何去构建ResultMap,让我们来接着探究😡:

 private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
     //···省略部分代码···
    // 创建 ResultMap 解析器
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      //根据获取的参数信息创建ResultMap
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

我们主要看的就是resolve()方法👻:

//-💫-ResultMapResolver
//我们看到方法委托给了内部属性MapperBuilderAssistant对象assistant
 public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
  }
//-💫-MapperBuilderAssistant
 public ResultMap addResultMap(
      String id,
      Class<?> type,
      String extend,
      Discriminator discriminator,
      List<ResultMapping> resultMappings,
      Boolean autoMapping) {
     // 为 ResultMap 的 id 和 extend 属性值拼接命名空间
    id = applyCurrentNamespace(id, false);
    extend = applyCurrentNamespace(extend, true);

    //如果extend不为空
    if (extend != null) {
      if (!configuration.hasResultMap(extend)) {
        throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
      }
      ResultMap resultMap = configuration.getResultMap(extend);
      List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
      extendedResultMappings.removeAll(resultMappings);
      //如果此resultMap声明构造函数,请删除父构造函数.
      boolean declaresConstructor = false;
      //遍历映射集合
      for (ResultMapping resultMapping : resultMappings) {
        //判断是否包含 CONSTRUCTOR 标志的元素
        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
          declaresConstructor = true;
          break;
        }
      }
      // 如果当前 <resultMap> 节点中包含 <constructor> 子节点,将移除包含CONSTRUCTOR标志的节点
      if (declaresConstructor) {
        extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
      }
      // 将扩展 resultMappings 集合合并到当前 resultMappings 集合中,当然这是在extend不为空的前提下
      resultMappings.addAll(extendedResultMappings);
    }
    //合并完之后依然要通过构建模式去构建ResultMap
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    configuration.addResultMap(resultMap);
    return resultMap;
  }

合理嵌套代码有点多大家不要厌倦😤:

轻松一刻👍
某日刘虹宏涛遇到外宾,就上前搭话曰:"iam hongtao liu .”外宾曰:"我还他妈的是黑桃八呢."

接下来进入刚刚部分代码最后一步构建过程:

//-💫-ResultMap
public ResultMap build() {
      if (resultMap.id == null) {
        throw new IllegalArgumentException("ResultMaps must have an id");
      }
      //用于存储 <id>、<result>、<idArg>、<arg> 节点 column 属性-mappedColumns 
      resultMap.mappedColumns = new HashSet<>();
      //用于存储 <id> 和 <result> 节点的 property 属性,或 <idArgs> 和 <arg> 节点的 name 属性-mappedProperties 
      resultMap.mappedProperties = new HashSet<>();
      //用于存储 <id> 和 <idArg> 节点对应的 ResultMapping 对象-idResultMappings 
      resultMap.idResultMappings = new ArrayList<>();
      //用于存储 <id> 和 <result> 节点对应的 ResultMapping 对象-constructorResultMappings 
      resultMap.constructorResultMappings = new ArrayList<>();
      //用于存储 <idArgs> 和 <arg> 节点对应的 ResultMapping 对象-propertyResultMappings 
      resultMap.propertyResultMappings = new ArrayList<>();
      final List<String> constructorArgNames = new ArrayList<>();
      for (ResultMapping resultMapping : resultMap.resultMappings) {
        // 检测 <association> 或 <collection> 节点
       // 是否包含 select 和 resultMap 属性
        resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
        resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
        final String column = resultMapping.getColumn();
        if (column != null) {
          // 将 colum 转换成大写,并添加到 mappedColumns 集合中resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
        } else if (resultMapping.isCompositeResult()) {
          for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
            final String compositeColumn = compositeResultMapping.getColumn();
            if (compositeColumn != null) {
              resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
            }
          }
        }
       // 添加属性 property 到 mappedProperties 集合中
        final String property = resultMapping.getProperty();
        if (property != null) {
          resultMap.mappedProperties.add(property);
        }
        // 检测当前 resultMapping 是否包含 CONSTRUCTOR 标志
        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
          // 添加 resultMapping 到 constructorResultMappings 中
          resultMap.constructorResultMappings.add(resultMapping);
          // 添加属性(constructor 节点的 name 属性)到constructorArgNames 中
          if (resultMapping.getProperty() != null) {
            constructorArgNames.add(resultMapping.getProperty());
          }
        } else {
          // 添加 resultMapping 到 propertyResultMappings 中
          resultMap.propertyResultMappings.add(resultMapping);
        }
        if (resultMapping.getFlags().contains(ResultFlag.ID)) {
          // 添加 resultMapping 到 idResultMappings 中
          resultMap.idResultMappings.add(resultMapping);
        }
      }
      if (resultMap.idResultMappings.isEmpty()) {
        resultMap.idResultMappings.addAll(resultMap.resultMappings);
      }
      if (!constructorArgNames.isEmpty()) {
        // 获取构造方法参数列表
        final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
        if (actualArgNames == null) {
          throw new BuilderException("Error in result map '" + resultMap.id
              + "'. Failed to find a constructor in '"
              + resultMap.getType().getName() + "' by arg names " + constructorArgNames
              + ". There might be more info in debug log.");
        }
        // 对 constructorResultMappings 按照构造方法参数列表的顺序进行排序
        resultMap.constructorResultMappings.sort((o1, o2) -> {
          int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
          int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
          return paramIdx1 - paramIdx2;
        });
      }
      // lock down collections
      // 将以下这些集合变为不可修改集合
      resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
      resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
      resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
      resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
      resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
      return resultMap;
    }

有没有感觉这部分构建老长了🤣,其实大致意思很清晰。先判断你写的这个resultMap有没有id(你如果不写的编辑器就会报错吧😥),接着它创建了五个hashSet让我们来看一下它们:

集合名称用途 用途
mappedColumns 用于存储 <id>、<result>、<idArg>、<arg> 节点 column 属性
mappedProperties 用于存储 <id> 和 <result> 节点的 property 属性,或 <idArgs> 和 <arg> 节点的 name 属性
idResultMappings 用于存储 <id> 和 <idArg> 节点对应的 ResultMapping 对象
propertyResultMappings 用于存储 <id> 和 <result> 节点对应的 ResultMapping 对象
constructorResultMappings 用于存储 <idArgs> 和 <arg> 节点对应的 ResultMapping 对象

大家看一下这几个set集合,下面的方法几乎就是遍历resultMapping,然后判断符合条件就添加到相应的集合当中去,这几个集合都是resultMap的属性,在我们将他们各自填满装好之后就将我们的Result对象返回去了。
为了大家更好的理解一下我们来写个栗子试一试:

实体类Student
public class Student {
    private Integer sid;
    private String sname;
    private String ssex;
    private Integer sage;
    public Student() {
    }
    public Student(Integer sid, String sname, String ssex, Integer sage) {
        this.sid = sid;
        this.sname = sname;
        this.ssex = ssex;
        this.sage = sage;
    }
···省略set,get方法
Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.StudentDao">
    <resultMap id="stuResult" type="domain.Student" >
        <constructor>
            <idArg column="sid"   javaType="Integer"/>
            <arg  column="sname" javaType="String"/>
            <arg  column="ssex" javaType="String"/>
            <arg  column="sage" javaType="Integer"/>
        </constructor>
        <id property="sid" column="sid"/>
    </resultMap>
</mapper>
测试Main

public class Main {
    public static void main(String[] args) throws IOException {

        Configuration configuration = new Configuration();
        String resource = "mapper/StudentMapper.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        XMLMapperBuilder builder = new XMLMapperBuilder(inputStream,
                configuration, resource, configuration.getSqlFragments());
        builder.parse();
        ResultMap resultMap = configuration.getResultMap("stuResult");
        System.out.println("\n----------+✨ mappedColumns ✨+-----------");
        System.out.println(resultMap.getMappedColumns());
        System.out.println("\n---------+✨ mappedProperties ✨+---------");
        System.out.println(resultMap.getMappedProperties());
        System.out.println("\n---------+✨ idResultMappings ✨+----------");
        resultMap.getIdResultMappings().forEach(
                rm -> System.out.println(simplify(rm)));
        System.out.println("\n------+✨ propertyResultMappings ✨+-------");
        resultMap.getPropertyResultMappings().forEach(
                rm -> System.out.println(simplify(rm)));
        System.out.println("\n----+✨ constructorResultMappings ✨+-----");
        resultMap.getConstructorResultMappings().forEach(
                rm -> System.out.println(simplify(rm)));
        System.out.println("\n---------+✨ resultMappings ✨+-----------");

        resultMap.getResultMappings().forEach(
                rm -> System.out.println(simplify(rm)));
        inputStream.close();
    }

    private static String simplify(ResultMapping resultMapping) {
        return String.format(
                "ResultMapping{column='%s', property='%s', flags=%s, type=%s,...}",
                resultMapping.getColumn(), resultMapping.getProperty(),
                resultMapping.getFlags(),resultMapping.getJavaType().getSimpleName());
    }
}

测试结果:


Main Run结果图

我来给大家分析了一下如下图😫:


分析结果图

解析ResultMap就看到这里吧😑。

解析<sql>节点

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。

  private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      // 调用 sqlElement 解析 <sql> 节点        
      sqlElement(list, configuration.getDatabaseId());
    }
    // 再次调用 sqlElement,不同的是,这次调用,该方法的第二个参数为 null
    sqlElement(list, null);
  }

  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 获取 id 和 databaseId 属性
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      // id = currentNamespace + "." + id
      id = builderAssistant.applyCurrentNamespace(id, false);
      // 检测当前 databaseId 和 requiredDatabaseId 是否一致
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        // 将 <id, XNode> 键值对缓存到 sqlFragments 中
        sqlFragments.put(id, context);
      }
    }
  }

这个方法逻辑比较简单,首先是获取<sql>节点的 id 和 databaseId 属性,然后为 id 属性值拼接命名空间。最后,通过检测当前 databaseId 和 requiredDatabaseId 是否一致,来决定保存还是忽略当前的<sql>节点。

 private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      // 当前 databaseId 和目标 databaseId 不一致时,返回 false
      return requiredDatabaseId.equals(databaseId);
    }
// 如果目标 databaseId 为空,但当前 databaseId 不为空。两者不一致,返回 false
    if (databaseId != null) {
      return false;
    }
    // 如果当前 <sql> 节点的 id 与之前的 <sql> 节点重复,且先前节点
    // databaseId 不为空。则忽略当前节点,并返回 false
    if (!this.sqlFragments.containsKey(id)) {
      return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
  }

这里总结一下 databaseId 的匹配规则:

  1. databaseId 与 requiredDatabaseId 不一致,即失配,返回 false
  2. 当前节点与之前的节点出现 id 重复的情况,若之前的<sql>节点 databaseId 属性
  3. 不为空,返回 false
  4. 若以上两条规则均匹配失败,此时返回 true
    在上面配置中,两个<sql>节点的 id 属性值相同,databaseId 属性不一致。假设configuration.databaseId = mysql,第一次调用 sqlElement 方法,第一个<sql>节点对应的 XNode会被放入到 sqlFragments 中。第二次调用 sqlElement 方法时,requiredDatabaseId 参数为空。由于 sqlFragments 中已包含了一个 id 节点,且该节点的 databaseId 不为空,此时匹配逻辑返回 false,第二个节点不会被保存到 sqlFragments。
    上面的分析内容涉及到了 databaseId,关于 databaseId 的用途,简单介绍一下。databaseId用于标明数据库厂商的身份,不同厂商有自己的 SQL方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349