Mybatis(二)Mybatis源码解析(上)

文章目录

    • 一、配置文件解析过程
      • 1.1 解析< properties >节点
      • 1.2 解析< settings >节点
        • 1.2.1 元信息对象创建过程
        • 1.2.2 DefaultReflectorFactory
        • 1.2.3 Reflector
        • 1.2.4 PropertyTokenizer
      • 1.3 设置< settings >内容到 Configuration 中
      • 1.4 解析< typeAliases >节点
      • 1.5 解析< plugins >节点
      • 1.6 解析< environments >节点
      • 1.7 解析< typeHandlers >节点
    • 二、映射文件解析过程
      • 2.1 映射文件解析解析入口
      • 2.2 解析映射文件
        • 2.2.1 解析< cache >节点
        • 2.2.2 解析< cache-ref >节点
        • 2.2.3 解析< resultMap >节点
        • 2.2.4 解析< sql >节点
        • 2.2.5 解析 SQL 语句节点
      • 2.3 Mapper 接口绑定过程
      • 2.4 处理未完成解析的节点

  本文用到的Mybatis源码版本为3.5.6。

一、配置文件解析过程

  在使用 MyBatis 时,第一步要做的事情一般是根据配置文件构建 SqlSessionFactory对象。相关代码大致如下:

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
	new SqlSessionFactoryBuilder().build(inputStream);

  在上面代码中,首先会使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory对象。这里的 build 方法是我们分析配置文件解析过程的入口方法,所以先看一下SqlSessionFactoryBuilder(位于org.apache.ibatis.session)类中的build方法

  public SqlSessionFactory build(InputStream inputStream) {
     
    return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
     
    try {
     
      // 创建配置文件解析器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 调用 parse 方法解析配置文件,生成 Configuration 对象
      return build(parser.parse());
    } catch (Exception e) {
     
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
     
      ErrorContext.instance().reset();
      try {
     
        inputStream.close();
      } catch (IOException e) {
     
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
     
    // 创建 DefaultSqlSessionFactory
    return new DefaultSqlSessionFactory(config);
  }

  从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过 XMLConfigBuilder 进行解析的。继续看一下XMLConfigBuilder(位于org.apache.ibatis.builder.xml) 的 parse 方法

  public Configuration parse() {
     
    if (parsed) {
     
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 解析配置
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  上述代码中有个xpath 表达式—— /configuration。这个表达式代表的是 MyBatis 配置文件的 节点,这里通过 xpath 选中这个节点,并传递给 parseConfiguration 方法

  private void parseConfiguration(XNode root) {
     
    try {
     
      // issue #117 read properties first
      // 解析 properties 配置
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 配置,并将其转换为 Properties 对象
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 加载 vfs
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // 解析 typeAliases 配置
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析 plugins 配置
      pluginElement(root.evalNode("plugins"));
      // 解析 objectFactory 配置
      objectFactoryElement(root.evalNode("objectFactory"));
      // 解析 objectWrapperFactory 配置
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 解析 reflectorFactory 配置
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // settings 中的信息设置到 Configuration 对象中
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析 environments 配置
      environmentsElement(root.evalNode("environments"));
      // 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 解析 typeHandlers 配置
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mappers 配置
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
     
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

  至此,一个完整的配置解析过程就呈现出来了,每个节点的的解析逻辑均封装在了相应的方法中。

1.1 解析< properties >节点

  节点的解析工作由XMLConfigBuilder类中的 propertiesElement 这个方法完成的,在分析方法的源码前,我们先来看一下节点是如何配置的:

<properties resource="jdbc.properties">
	<property name="jdbc.username" value="coolblog"/>
	<property name="hello" value="world"/>
properties>

  上面配置包含了一个 resource 属性,和两个子节点。接下面,我们参照上面的配置,来分析 propertiesElement方法的逻辑:

  private void propertiesElement(XNode context) throws Exception {
     
    if (context != null) {
     
      // 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
      Properties defaults = context.getChildrenAsProperties();
      // 获取 propertis 节点中的 resource 和 url 属性值
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
     
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      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);
    }
  }

  上面的代码调用了XNode(位于org.apache.ibatis.parsing)中的getChildrenAsProperties和getChildren方法:

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

  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 对象中。二是从文件系统或通过网络读取属性配置,这取决于节点的 resource 和 url 是否为空。第二步对应的代码比较简单,就不分析了。最后一步则是将包含属性信息的 Properties 对象设置到XPathParser 和 Configuration 中。

  需要注意的是,propertiesElement 方法是先解析节点的子节点内容,然后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到 defaults 属性对象中。这会导致同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性和属性值会覆盖掉子节点中同名的属性和及值。

1.2 解析< settings >节点

  相关配置是 MyBatis 中非常重要的配置,这些配置用于调整 MyBatis 运行时的行为。settings 配置繁多,在对这些配置不熟悉的情况下,保持默认配置即可。先来看一个比较简单的配置:

<settings>
	<setting name="cacheEnabled" value="true"/>
	<setting name="lazyLoadingEnabled" value="true"/>
	<setting name="autoMappingBehavior" value="PARTIAL"/>
settings>

  最开始是XMLConfigBuilder中的settingsAsProperties方法:

  private Properties settingsAsProperties(XNode context) {
     
    if (context == null) {
     
      return new Properties();
    }
    // 获取 settings 子节点中的内容
    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()) {
     
      // 检测 Configuration 中是否存在相关属性,不存在则抛出异常
      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;
  }

  在上面的代码中出现了一个陌生的类 MetaClass,这个类是用来做什么的呢?答案是用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等。简单总结一下上面代码的逻辑:

  1. 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
  2. 为 Configuration 创建元信息对象
  3. 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法,不存在则抛异常
  4. 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束

  我们需要重点关注一下第 2 步和第 3 步的流程。

1.2.1 元信息对象创建过程

  元信息类 MetaClass(位于org.apache.ibatis.reflection) 的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass 方法进行创建:

public class MetaClass {
     

  private final ReflectorFactory reflectorFactory;
  private final Reflector reflector;

  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);
  }
  // 省略其他方法
}

  上面代码出现了两个新的类ReflectorFactory 和 Reflector,MetaClass 通过引入这些新类帮助它完成功能。关于这些类,可以先看下hasSetter方法:

  public boolean hasSetter(String name) {
     
  	// 属性分词器,用于解析属性名
    PropertyTokenizer prop = new PropertyTokenizer(name);
    // hasNext 返回 true,则表明 name 是一个复合属性
    if (prop.hasNext()) {
     
      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 方法最终调用了 Reflector 的hasSetter 方法。下面来简单介绍一下上面代码中出现的几个类:

  1. ReflectorFactory:顾名思义,Reflector 的工厂类,兼有缓存 Reflector 对象的功能
  2. Reflector:反射器,用于解析和存储目标类中的元信息
  3. PropertyTokenizer:属性名分词器,用于处理较为复杂的属性名

1.2.2 DefaultReflectorFactory

  DefaultReflectorFactory(位于org.apache.ibatis.reflection) 用于创建 Reflector,同时兼有缓存的功能:

public class DefaultReflectorFactory implements ReflectorFactory {
     
  private boolean classCacheEnabled = true;
  /** 目标类和反射器映射缓存 */
  private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();

  // 省略部分代码

  @Override
  public Reflector findForClass(Class<?> type) {
     
    if (classCacheEnabled) {
     
      // synchronized (type) removed see issue #461
      //如果type对应的Reflector不存在,就创建一个新的 Reflector 实例
      return reflectorMap.computeIfAbsent(type, Reflector::new);
    } else {
     
      // 创建一个新的 Reflector 实例
      return new Reflector(type);
    }
  }
}

1.2.3 Reflector

  Reflector(位于org.apache.ibatis.reflection) 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用。下面分析该类的部分逻辑:

  1. Reflector 构造方法及成员变量分析
  2. getter 方法解析过程
  3. setter 方法解析过程
  • 1、Reflector 构造方法及成员变量分析
      Reflector 构造方法中包含了很多初始化逻辑,目标类的元信息解析过程也是在构造方法中完成的,这些元信息最终会被保存到 Reflector 的成员变量中:
public class Reflector {
     

  private final Class<?> type;
  private final String[] readablePropertyNames;
  private final String[] writablePropertyNames;
  private final Map<String, Invoker> setMethods = new HashMap<>();
  private final Map<String, Invoker> getMethods = new HashMap<>();
  private final Map<String, Class<?>> setTypes = new HashMap<>();
  private final Map<String, Class<?>> getTypes = new HashMap<>();
  private Constructor<?> defaultConstructor;

  private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();

  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);
    }
  }
  // 省略其他方法
}

  Reflector 部分成员变量的用途:

变量名 用途
readablePropertyNames 可读属性名称数组,用于保存 getter 方法对应的属性名称
writeablePropertyNames 可写属性名称数组,用于保存 setter 方法对应的属性名称
setMethods 用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中
getMethods 用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中
setTypes 用于保存 setter 对应的属性名与参数类型的映射
getTypes 用于保存 getter 对应的属性名与返回值类型的映射
caseInsensitivePropertyMap 用于保存大写属性名与属性名之间的映射,比如
  • 2、Reflector getter 方法解析过程
      getter 方法解析的逻辑被封装在了 addGetMethods 方法中,这个方法除了会解析形如getXXX 的方法,同时也会解析 isXXX 方法:
  private void addGetMethods(Class<?> clazz) {
     
    Map<String, List<Method>> conflictingGetters = new HashMap<>();
    // 获取当前类,接口,以及父类中的方法
    Method[] methods = getClassMethods(clazz);
    // getter 方法不应该有参数,若存在参数,则忽略当前方法
    Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
      // 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name
      //addMethodConflict方法用于解决冲突。 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:getTitle 和 isTitle 两个方法经过 methodToProperty 处理,均得到 name = title,这会导致冲突。对于冲突的方法,这里先统一起存起来,后续再解决冲突
      .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
    // 解决 getter 冲突
    resolveGetterConflicts(conflictingGetters);
  }

  addGetMethods 方法的逻辑:

  1. 获取当前类,接口,以及父类中的方法
  2. 遍历上一步获取的方法数组,并过滤出以 get 和 is 开头的方法
  3. 将方法名转换成相应的属性名
  4. 将属性名和方法对象添加到冲突集合中
  5. 解决冲突

  第4步和第5步的具体逻辑:

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

  /** 解决冲突 */
  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;
          /*
		   * 如果方法返回值类型为 boolean,且方法名以 "is" 开头,
		   * 则认为候选方法 candidate 更为合适
		   */
          } else if (candidate.getName().startsWith("is")) {
     
            winner = candidate;
          }
        /*
		 * winnerType 是 candidateType 的子类,类型上更为具体,
		 * 则认为当前的 winner 仍是合适的,无需做什么事情
		 */
        } else if (candidateType.isAssignableFrom(winnerType)) {
     
          // OK getter type is descendant
        /*
	     * candidateType 是 winnerType 的子类,此时认为 candidate 方法
         * 更为合适,故将 winner 更新为 candidate
         */
        } else if (winnerType.isAssignableFrom(candidateType)) {
     
          winner = candidate;
        } else {
     
          isAmbiguous = true;
          break;
        }
      }
      // 将筛选出的方法添加到 getMethods 中,并将方法返回值添加到 getTypes 中
      addGetMethod(propName, winner, isAmbiguous);
    }
  }

  private void addGetMethod(String name, Method method, boolean isAmbiguous) {
     
    MethodInvoker invoker = isAmbiguous
        ? new AmbiguousMethodInvoker(method, MessageFormat.format(
            "Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
            name, method.getDeclaringClass().getName()))
        : new MethodInvoker(method);
    getMethods.put(name, invoker);
    // 解析返回值类型
    Type returnType = TypeParameterResolver.resolveReturnType(method, type);
    // 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中
    getTypes.put(name, typeToClass(returnType));
  }

  以上就是解除冲突的过程,只要记住解决冲突的规则即可理解上面代码的逻辑。相关规则如下:

  1. 冲突方法返回值类型具有继承关系,子类返回值对应方法被认为是更合适的选择
  2. 冲突方法的返回值类型相同,如果返回值类型为 boolean,那么以 is 开头的方法则是更合适的选择
  3. 冲突方法的返回值类型相同,但类型非 boolean,此时出现歧义,抛出异常
  4. 冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常分析完 getter 方法的解析过程,下面继续分析 setter 方法的解析过程。
  • 3、Reflector setter 方法解析过程
      与 getter 方法解析过程相比,setter 方法的解析过程与此有一定的区别。主要体现在冲突出现的原因,以及冲突的解决方法上:
  private void addSetMethods(Class<?> clazz) {
     
    Map<String, List<Method>> conflictingSetters = new HashMap<>();
    // 获取当前类,接口,以及父类中的方法
    Method[] methods = getClassMethods(clazz);
    // 过滤出 setter 方法
    // setter 方法发生冲突原因是:可能存在重载情况,比如:void setSex(int sex);void setSex(SexEnum sex);
    Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
      .forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
    // 解决 setter 冲突
    resolveSetterConflicts(conflictingSetters);
  }

  setter 方法之间出现冲突的原因。即方法存在重载,方法重载导致 methodToProperty 方法解析出的属性名完全一致。而 getter 方法之间出现冲突的原因是 getXXX 和 isXXX 对应的属性名一致。解决冲突的具体逻辑:

  private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
     
    for (Entry<String, List<Method>> entry : conflictingSetters.entrySet()) {
     
      String propName = entry.getKey();
      List<Method> setters = entry.getValue();
      /*
	   * 获取 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;
        }
      }
      // 将筛选出的方法放入 setMethods 中,并将方法参数值添加到 setTypes 中
      if (match != null) {
     
        addSetMethod(propName, match);
      }
    }
  }

  /** 从两个 setter 方法中选择一个更为合适方法 */
  private Method pickBetterSetter(Method setter1, Method setter2, String property) {
     
    if (setter1 == null) {
     
      return setter2;
    }
    Class<?> paramType1 = setter1.getParameterTypes()[0];
    Class<?> paramType2 = setter2.getParameterTypes()[0];
    // 如果参数 2 可赋值给参数 1,即参数 2 是参数 1 的子类,
    // 则认为参数 2 对应的 setter 方法更为合适
    if (paramType1.isAssignableFrom(paramType2)) {
     
      return setter2;
    } else if (paramType2.isAssignableFrom(paramType1)) {
     
      return setter1;
    }
    MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
        MessageFormat.format(
            "Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.",
            property, setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
    setMethods.put(property, invoker);
    Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
    setTypes.put(property, typeToClass(paramTypes[0]));
    return null;
  }

  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. 冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常

1.2.4 PropertyTokenizer

  PropertyTokenizer(位于org.apache.ibatis.reflection.property) 对数组和复合属性均处理:

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) {
     
      /*
	   * 以点位为界,进行分割。比如:
       * fullname = www.coolblog.xyz
       *
       * 以第一个点为分界符:
       * name = www
       * children = coolblog.xyz
       */
      name = fullname.substring(0, delim);
      children = fullname.substring(delim + 1);
    } else {
     
      // fullname 中不存在字符 '.'
      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);
      // 获取分解符前面的内容,比如:fullname = articles[1],name = articles
      name = name.substring(0, delim);
    }
  }

  @Override
  public boolean hasNext() {
     
    return children != null;
  }

  @Override
  public PropertyTokenizer next() {
     
    // 对 children 进行再次切分,用于解析多重复合属性
    return new PropertyTokenizer(children);
  }
}

1.3 设置< settings >内容到 Configuration 中

  节点内容解析出来后,需要有一个存放的地方,以使其他代码可以找到这些配置。这个存放地方就是 Configuration 对象,本节来看一下将节点内容设置到 Configuration 对象中的过程。以下的代码仍在XMLConfigBuilder类中:

  private void settingsElement(Properties props) {
     
    // 设置 autoMappingBehavior 属性,默认值为 PARTIAL
    configuration.setAutoMappingBehavior(AutoMappingBehavior.
        valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    // 设置 cacheEnabled 属性,默认值为 true
    configuration.setCacheEnabled(
    	booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
    configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
    configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
    configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
    configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
    configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
    configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
    configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
    configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
    configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
    configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
    configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
    configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
    // 设置默认枚举处理器
    configuration.setDefaultEnumTypeHandler(
    	resolveClass(props.getProperty("defaultEnumTypeHandler")));
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
    configuration.setLogPrefix(props.getProperty("logPrefix"));
    configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
    configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
  }

  在上面的代码中,调用了BaseBuilder(位于org.apache.ibatis.builder)类中的resolveClass方法:

  protected <T> Class<? extends T> 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 <T> Class<? extends T> resolveAlias(String alias) {
     
    // 通过别名注册器解析别名对于的类型 Class
    return typeAliasRegistry.resolveAlias(alias);
  }

  TypeAliasRegistry 的用途就是将别名和类型进行映射,这样就可以用别名表示某个类了,方便使用。既然聊到了别名,那下面我们不妨看看别名的配置的解析过程。

1.4 解析< typeAliases >节点

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

<typeAliases>
	<package name="xyz.coolblog.chapter2.model1"/>
	<package name="xyz.coolblog.chapter2.model2"/>
typeAliases>

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

<typeAliases>
	<typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article" />
	<typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author" />
typeAliases>

  第一种自动扫描的方式配置起来比较简单,第二种方式比较繁琐,特别是配置项比较多时。
  上面是使用,接下来就看下具体在代码中是怎么解析的,即XMLConfigBuilder中的typeAliasesElement方法:

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

  上面的代码通过一个 if-else 条件分支来处理两种不同的配置。

  • 1、从< 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");
    }
    // issue #748
    // 将别名转成小写
    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 会使用类名的小写形式作为别名。比如,全限定类名 xyz.coolblog.model.Author 的别名为 author。若类中有@Alias 注解,则从注解中取值作为别名。

  • 2、从指定的包中解析并注册别名
      从指定的包中解析并注册别名过程主要由别名的解析和注册两步组成,这两个方法均在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) {
     
      // Ignore inner classes and interfaces (including package-info.java)
      // Skip also inner classes. See issue #6
      // 忽略匿名类,接口,内部类
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
     
        // 为类型注册别名
        registerAlias(type);
      }
    }
  }
  • 3、注册 MyBatis 内部类及常见类型的别名
      最后,我们来看一下一些 MyBatis 内部类及一些常见类型的别名注册过程,以下代码在Configuration(位于org.apache.ibatis.session)类中:
  public Configuration() {
     
    // 注册事务工厂的别名
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    // 省略部分代码
    // 注册数据源的别名
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    // 注册缓存策略的别名
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    // 注册日志类的别名
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    // 注册动态代理工厂的别名
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
  }

  以下代码在TypeAliasRegistry类中:

  public TypeAliasRegistry() {
     
    // 注册 String 的别名
    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);
    // 注册基本类型包装类数组的别名
    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);
    // 注册基本类型的别名
    registerAlias("_byte", byte.class);
    registerAlias("_long", long.class);
    registerAlias("_short", short.class);
    registerAlias("_int", int.class);
    registerAlias("_integer", int.class);
    registerAlias("_double", double.class);
    registerAlias("_float", float.class);
    registerAlias("_boolean", boolean.class);
    // 注册基本类型包装类的别名
    registerAlias("_byte[]", byte[].class);
    registerAlias("_long[]", long[].class);
    registerAlias("_short[]", short[].class);
    registerAlias("_int[]", int[].class);
    registerAlias("_integer[]", int[].class);
    registerAlias("_double[]", double[].class);
    registerAlias("_float[]", float[].class);
    registerAlias("_boolean[]", boolean[].class);
    // 注册 Date, BigDecimal, Object 等类型的别名
    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("bigdecimal", BigDecimal.class);
    registerAlias("biginteger", BigInteger.class);
    registerAlias("object", Object.class);
    // 注册 Date, BigDecimal, Object 等数组类型的别名
    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("bigdecimal[]", BigDecimal[].class);
    registerAlias("biginteger[]", BigInteger[].class);
    registerAlias("object[]", Object[].class);
    // 注册集合类型的别名
    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);
    // 注册 ResultSet 的别名
    registerAlias("ResultSet", ResultSet.class);
  }

  以上就是别名解析的全部流程。

1.5 解析< plugins >节点

  插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现 Interceptor接口。然后在插件类上添加@Intercepts 和@Signature 注解,用于指定想要拦截的目标方法。
  比较常见的插件有分页插件、分表插件。在分析插件的配置的解析过程之前,我们先来了解一下插件的配置:

<plugins>
	<plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
		<property name="key" value="value"/>
	plugin>
plugins>

  关于该配置的解析在XMLConfigBuilder中:

  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 中。

1.6 解析< environments >节点

  事务管理器和数据源是配置在节点中的。它们的配置大致如下:

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

  对于上述配置的解析过程也在在XMLConfigBuilder中:

  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.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 构建 Environment 对象,并设置到 configuration 中
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

1.7 解析< typeHandlers >节点

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


<typeHandlers>
	<package name="xyz.coolblog.handlers"/>
typeHandlers>

<typeHandlers>
	<typeHandler jdbcType="TINYINT"
		javaType="xyz.coolblog.constant.ArticleTypeEnum"
		handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
typeHandlers>

  使用自动扫描的方式注册类型处理器时,应使用@MappedTypes 和@MappedJdbcTypes注解配置 javaType 和 jdbcType。其对应的解析代码也在XMLConfigBuilder中:

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

  在上面的代码中,用照①②③④标注了四个重载方法,这些代码均在TypeHandlerRegistry(位于org.apache.ibatis.type)类中。

  • 1、register(Class,JdbcType,Class)方法
      当代码执行到此方法时,表示 javaTypeClass!= null && jdbcType != null 条件成立,即开发者明确配置了 javaType 和 jdbcType 属性的值。
  // java type + jdbc type + handler type
  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 = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
     
        map = new HashMap<>();
      }
      map.put(jdbcType, handler);
      // 存储 javaType 到 Map 的映射
      typeHandlerMap.put(javaType, map);
    }
    // 存储所有的 TypeHandler
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

  所谓的注册过程也就是把类型和处理器进行映射而已。

  • 2、register(Class,Class)方法
      当代码执行到此方法时,表示 javaTypeClass != null && jdbcType == null 条件成立,即使用者仅设置了 javaType 属性的值。
  public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
     
    // 调用中间方法 register(Type, TypeHandler)
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }

  private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
     
    // 获取 @MappedJdbcTypes 注解
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
     
      // 遍历 @MappedJdbcTypes 注解中配置的值
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
     
        // 调用终点方法,参考上一小节的分析
        register(javaType, handledJdbcType, typeHandler);
      }
      if (mappedJdbcTypes.includeNullJdbcType()) {
     
        // 调用终点方法,jdbcType = null
        register(javaType, null, typeHandler);
      }
    } else {
     
      // 调用终点方法,jdbcType = null
      register(javaType, null, typeHandler);
    }
  }

  上面的逻辑也比较简单,主要做的事情是尝试从注解中获取 JdbcType 的值。

  • 3、register(Class)方法
      当代码执行到此方法时,表示 javaTypeClass==null&&jdbcType!=null 条件成立,即使用者未配置 javaType 和 jdbcType 属性的值。
  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);
    }
  }

public <T> void register(
	Class<T> javaType, TypeHandler<? extends T> typeHandler) {
     
	// 调用中间方法 register(Type, TypeHandler)
	register((Type) javaType, typeHandler);
}

  不管是通过注解的方式,还是通过反射的方式,它们最终目的是为了解析出 javaType 的值。解析完成后,这些方法会调用中间方法
register(Type,TypeHandler),这个方法负责解析 jdbcType,该方法上一节已经分析过。一个负责解析 javaType,另一个负责解析 jdbcType,逻辑比较清晰了。

  • 4、register(String)方法
      本节代码的主要是用于自动扫描类型处理器,并调用其他方法注册扫描结果。
  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);
      }
    }
  }

二、映射文件解析过程

  与配置文件不同,映射文件用于配置 SQL语句,字段映射关系等。映射文件中包含
等二级节点,这些节点将在接下来内容中进行分析。本章除了分析常规的 XML 解析过程外,还会介绍 Mapper 接口的绑定过程,以及其他一些知识。

2.1 映射文件解析解析入口

  映射文件的解析过程是配置文件解析过程的一部分,MyBatis 会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement 方法中:

  private void mapperElement(XNode parent) throws Exception {
     
    if (parent != null) {
     
      for (XNode child : parent.getChildren()) {
     
        if ("package".equals(child.getName())) {
     
          // 获取  节点中的 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.");
          }
        }
      }
    }
  }

  代码的主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以 XML 为载体的配置称为映射文件。在 MyBatis 中,共有四种加载映射文件或映射信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 URL 的方式加载映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
  在 MyBatis中,通过注解配置映射信息的方式是有一定局限性的,这一点 MyBatis 官方文档中描述的比较清楚:

  因为最初设计时,MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,而且映射语句也是定义在 XML 中的。而到了 MyBatis3,就有新选择了。MyBatis3 构建在全面且强大的基于 Java 语言的配置 API 之上。这个配置 API 是基于 XML 的 MyBatis 配置的基础,也是新的基于注解配置的基础。注解提供了一种简单的方式来实现简单映射语句,而不会引入大量的开销。
  注意:不幸的是,Java 注解的表达力和灵活性十分有限。尽管很多时间都花在调查、设计和试验上,最强大的MyBatis映射并不能用注解来构建。

  从上面内容可以看出:限于 Java 注解的表达力和灵活性,通过注解的方式并不能完全发挥 MyBatis 的能力。因此,对于一些较为复杂的配置信息,我们还是应该通过XML 的方式进行配置。
  下面开始分析映射文件的解析过程,在分析之前,先来看一下映射文件解析入口。在上面的mapperElement方法中调用了mapperParser.parse()方法,这就是我们想要的入口,即XMLMapperBuilder(位于org.apache.ibatis.builder.xml)中的parse方法:

  public void parse() {
     
    // 检测映射文件是否已经被解析过
    if (!configuration.isResourceLoaded(resource)) {
     
      // 解析 mapper 节点
      configurationElement(parser.evalNode("/mapper"));
      // 添加资源路径到“已解析资源集合”中
      configuration.addLoadedResource(resource);
      // 通过命名空间绑定 Mapper 接口
      bindMapperForNamespace();
    }
    // 处理未完成解析的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

  映射文件解析入口逻辑包含三个核心操作:

  1. 解析 mapper 节点
  2. 通过命名空间绑定 Mapper 接口
  3. 处理未完成解析的节点

2.2 解析映射文件

  映射文件 包 含 多 种 二 级 节 点 , 比 如 以 及 等。除此之外,还包含了一些三级节点,比如 等。先来看一个映射文件配置示例:

<mapper namespace="xyz.coolblog.dao.AuthorDao">
	<cache/>
	<resultMap id="authorResult" type="Author">
		<id property="id" column="id"/>
		<result property="name" column="name"/>
		
	resultMap>
	<sql id="table">
	   author
	sql>
	<select id="findOne" resultMap="authorResult">
		 SELECT
			 id, name, age, sex, email
		 FROM
			<include refid="table"/>
		 WHERE
 			id = #{id}
	select>
	
mapper>

  以上配置中每种节点的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder(位于org.apache.ibatis.builder.xml)类 的configurationElement 方法统一调用:

  private void configurationElement(XNode context) {
     
    try {
     
      // 获取 mapper 命名空间
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
     
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 设置命名空间到 builderAssistant 中
      builderAssistant.setCurrentNamespace(namespace);
      // 解析  节点
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析  节点
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析  节点
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析  节点
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析 以及等。这几个节点中存储的是相同的内容,都是 SQL 语句,所以这几个节点的解析过程也是相同的。

  在进行代码分析之前,这里需要特别说明一下:为了避免和节点混淆,同时也为了描述方便,这里把 节点名称为 select String nodeName = context.getNode().getNodeName(); // 根据节点名称解析 SqlCommandType SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 解析 节点 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // 解析 节点 processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: and were parsed and removed) KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { // 获取 KeyGenerator 实例 keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { // 创建 KeyGenerator 实例 keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } // 解析 SQL 语句 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // 解析 Statement 类型,默认为 PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); // 获取各种属性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String resultType = context.getStringAttribute("resultType"); // 通过别名解析 resultType 对应的类型 Class<?> resultTypeClass = resolveClass(resultType); String resultMap = context.getStringAttribute("resultMap"); // 解析 ResultSetType String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); // 构建 MappedStatement 对象,并将该对象存储到 // Configuration 的 mappedStatements 集合中 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }

  上面的代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以上代码做的事情:

  1. 解析节点
  2. 解析节点
  3. 解析 SQL,获取 SqlSource
  4. 构建 MappedStatement 实例
  • 1、解析< include >节点
      节点的解析逻辑封装在 XMLIncludeTransformer(位于org.apache.ibatis.builder.xml)的applyIncludes方法 中:
  public void applyIncludes(Node source) {
     
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    // 将 configurationVariables 中的数据添加到 variablesContext 中
    Optional.ofNullable(configurationVariables)
    	.ifPresent(variablesContext::putAll);
    // 调用重载方法处理  节点
    applyIncludes(source, variablesContext, false);
  }

  创建了一个新的 Properties 对象,并将全局 Properties 添加到其中。这样做的原因是 applyIncludes 的重载方法会向 Properties 中添加新的元素,如果直接将全局 Properties 传给重载方法,会造成全局 Properties 被污染。

  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
     
    if ("include".equals(source.getNodeName())) {
     
      // 获取  节点。若 refid 中包含属性占位符 ${},
      // 则需先将属性占位符替换为对应的属性值
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      // 解析的子节点,并将解析结果与 variablesContext 融合,
      // 然后返回融合后的 Properties。若  节点的 value 属性中存在
      // 占位符 ${},则将占位符替换为对应的属性值
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      /*
	   * 这里是一个递归调用,用于将  节点内容中出现的属性占位符 ${}
	   * 替换为对应的属性值。这里要注意一下递归调用的参数:
	   * - toInclude: 节点对象
	   * - toIncludeContext: 子节点  的解析结果与
	   * 全局变量融合后的结果
	   */
      applyIncludes(toInclude, toIncludeContext, true);
      // 如果  节点不在一个文档中,
	  // 则从其他文档中将  节点引入到  所在文档中
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
     
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 将  节点替换为  节点
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
     
        // 将  中的内容插入到  节点之前
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 前面已经将  节点的内容插入到 dom 中了,
	  // 现在不需要  节点了,这里将该节点从 dom 中移除
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
     
      if (included && !variablesContext.isEmpty()) {
     
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
     
          Node attr = attributes.item(i);
          // 将 source 节点属性中的占位符 ${} 替换成具体的属性值
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
     
        // 递归调用
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
     
      // 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }
  • 2、解析< selectKey >节点
      对于一些不支持自增主键的数据库来说,我们在插入数据时,需要明确指定主键数据。以 Oracle 数据库为例,Oracle 数据库不支持自增主键,但它提供了自增序列工具。我们每次向数据库中插入数据时,可以先通过自增序列获取主键数据,然后再进行插入。这里涉及到两次数据库查询操作,但我们并不能在一个