MyBatis 源码解析(二)MyBatis如何解析配置 ?(二)

首先,我们从MyBatis的入口方法入手:

sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {

      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());

}

本系列所有源码为了方便阅读,都会删除一些“结构性”的代码,下同

可以看到,这里是直接新建了XMLConfigBuilder对象,然后调用了XMLConfigBuilder方法进行解析XML文件生成Configuration对象

XMLConfiguration###XMLConfigBuilder()

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    //设置变量
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

XMLConfiguration###parse()

//简单的判断是否已经解析过了
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

XMLConfiguration###parseConfiguration()

//调用各个方法进行解析成Configuration对象 
private void parseConfiguration(XNode root) {
    try {
      //读取用户自定义属性
      propertiesElement(root.evalNode("properties"));
      //读取用户的设置
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加载用户自定义的VFS实现  
      loadCustomVfs(settings);
      //加载日志设置
      loadCustomLogImpl(settings);
      //加载用户定义的别名  
      typeAliasesElement(root.evalNode("typeAliases"));
      //加载用户定义的插件
      pluginElement(root.evalNode("plugins"));
      //加载用户定义的对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //加载用户定义的反射对象工厂
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //加载用户定义的其他设置
      settingsElement(settings);
      //加载用户定义的环境变量
      environmentsElement(root.evalNode("environments"));
      //读取databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //处理类型处理器  
      typeHandlerElement(root.evalNode("typeHandlers"));、
      //处理mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

这里可以看到parseConfiguration方法基本上是所有方法的汇总,基本上通过这个方法即解析了整个XML配置文件,因此我们一个一个看.


XMLConfiguration###propertiesElement()

//顾明思意,此方法便是用来读取properties节点的  
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      //分别读取properties节点的name和value元素,转换为properties对象
      Properties defaults = context.getChildrenAsProperties();
      //获取其他属性的资源路径
      String resource = context.getStringAttribute("resource");
      //获取其他属性的url
      String url = context.getStringAttribute("url");
      //resource和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.");
      }
      //如果resource不为null,则通过`ClassLoader` 加载此资源文件
      //可以知道这里是通过`ClassLoader`加载的资源文件,因此不管这个资源文件放在哪个模块,都能被加到
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } 
      //如果url不为null,则通过JDK的URL类加载此资源
        else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      //将存入的属性取出来
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      //最后再加入之前存入的属性,这样操作主要是为了保证不同地方的优先级
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }

可以看到,这个方法主要包含以下3个步骤:

  • 首先加入节点中的属性
  • 然后加入resourceurl中的属性
  • 然后加入通过XMLConfiguration构造方法传入属性

以上上个步骤顺序严格执行,且后面的操作可以覆盖前面的key

XMLConfiguration###settingsAsProperties()

  private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    //检测所设置的值是否存在对应的`setter`,没有则抛出异常
    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;
  }

这里MetaClass便是MyBatisreflection包中的一个元数据类,用于通过反射获取/设置各个对象的值。

XMLConfiguration###loadCustomLogImpl()

//加载用户设置的日志实现类  
private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }

这个方法没什么特别的,但是可以从这里看看typeAlias的代码。

在平时配置中,我们都是进行如下配置的:

<setting name="logImpl" value="SLF4J"/>
  

我们都是写的简写,而不是com.xxx.xxx.Slf4j,这是因为MyBatis中维护了一个TypeAliasRegister,它维护了简写与实际的类的映射.

TypeAliasRegister###resolveAlias()

  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      //将key都转换为小写
      //这里在国际使用中有个bug,比如如果本地语言是Turkish,那i转成大写就不是I了,而是另外一个字符   
      //(İ)。这样土耳其的机器就用不了mybatis了
      //因此这里要指定一个统一的本地语言
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      //如果别名中找到了所注册的key
      if (typeAliases.containsKey(key)) {
        value = (Class<T>) typeAliases.get(key);
      } else {
      //没找到就尝试直接加载此类    
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }

由这里可以看到,在取值的时候,都是先将Key转换为小写后再取值,因此可以看出来MyBatis是不区分大小写的

同时,可以看到我既可以指定别名,也可以直接写全名。

而日志文件的映射,是在Configuration构造方法里面被注册:

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

XMLConfiguration###typeAliasesElement()

    //加载用户自定义的别名
    //    <typeAliases>
    //       <package name="com.dengchengchao.demo.model"/>
    //   </typeAliases>
  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //如果子节点是`package` 则说明是自动获取包下所有类别名  
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
        //否则,根据type alias 来加载具体的类    
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            //加载此类
            Class<?> clazz = Resources.classForName(type);
            //如果别名为null,则默认为class名首字母小写 
            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);
          }
        }
      }
    }
  }

可以看到这里代码比较简单, 我们可以更加深入看看MyBatis是如何加载类的

首先看注册package目录下的所有类

  public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    //这里ResolverUtil内部便是通过VFS读取了该包下所有的class
    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);
      }
    }
  }

然后看看单独注册指定类:

public void registerAlias(Class<?> type) {
    //获取类的类名
    String alias = type.getSimpleName();
    //查看此类型上是否有Alias注解
    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);
    //别名注册不能重复
    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() + "'.");
    }
    //将对应的Key Value放进map
    typeAliases.put(key, value);
  }

这里可以看到有两点需要注意:

  1. 别名是不区分大小写的,这在前面已经说过
  2. 如果同时又Alias注解和在XML中也配置了aliasMyBatis会以XML中的为准

本章暂时到这里,有上面的源码我可以学到:

  1. MyBatisproperties的优先级的实现
  2. MyBatis中别名是不区分大小写的,以及String#toLowerCase()可能带来的bug
  3. MyBatis中,指定一些类型的设置,也可以不通过类型别名,直接指定全名也行
  4. 学习MyBatis的结构划分,可以发现MyBatis的代码逻辑十分清晰,易读