mybatis配置加载阶段源码之XMLConfigBuilder

594 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看详情

作用

XMLConfigBuilder 的作用是解析mybatis-config.xml配置文件,它是在SqlSessionFactoryBuilder被初始化的,然后调用XMLConfigBuilder 对象的parse 方法开始解析配置文件。 在这里插入图片描述

构造方法

XMLConfigBuilder 继承了BaseBuilder,它有七个构造方法, 在这里插入图片描述 但最终调用的是XMLConfigBuilder(XPathParser parser, String environment, Properties props)

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

配置解析

XMLConfigBuilder 真正开始进行配置解析的 ,parse()方法是解析的开始,在方法parseConfiguration(XNode root) 进行各个节点的解析 parse()

 public Configuration parse() {
//    已经解析过不能再次解析
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

parseConfiguration

/**
   *  解析配置文件
   * @param root
   */
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      // 解析properties 节点
      propertiesElement(root.evalNode("properties"));

      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // 解析       typeAliases 标签
      typeAliasesElement(root.evalNode("typeAliases"));

      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析环境environments 标签
      environmentsElement(root.evalNode("environments"));

      databaseIdProviderElement(root.evalNode("databaseIdProvider"));

      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析SQL 文件
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

标签解析

配置文件的标签如下: configuration 配置    properties(属性) settings(设置) typeAliases(类型别名) typeHandlers(类型处理器) objectFactory(对象工厂) plugins(插件) environments(环境配置) environment(环境变量) transactionManager(事务管理器) dataSource(数据源) databaseIdProvider(数据库厂商标识) mappers(映射器) 以上每个标签都有各自的作用

properties

properties 可以配置一些属性值,然后在配置文件中动态替换。在配置文件中是下面这样的

<properties  resource="db.properties">
    <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
  </properties>

解析步骤: 1、如果有properties标签,先把properties标签下的子标签property进行解析,name作为key,value 作为value,以键值对的形式返回到java.util.Properties 对象中,defaults接收
2、解析properties 标签属性,resource 和 url ,二者只能配置一个,否则会报错,
3、把 配置文件中的属性解析出来,然后放到defaults 中,由于是hashtable存储,所以会覆盖掉property属性中相同key的值
4、把解析到的key-value 设值到XMLConfigBuilder的parser属性的variables和configuration对象的variables属性中以便后续进行属性替换

/**
   *  解析properties 标签中的变量
   *  并最终把解析到的变量放到configuration 中的variables字段中
   * @param context
   * @throws Exception
   */
  private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 解析默认的property 标签
      Properties defaults = context.getChildrenAsProperties();

      String resource = context.getStringAttribute("resource");
      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.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } 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);
    }
  }

setting

setting 是MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为,有关setting的标签可以查看 官网有详细的介绍,这里主要是看下源码 setting的加载有四个步骤:

1、判断setting 子标签中的name属性对应的值是否是Configuration的属性,不是则抛异常

/**
   *  判断setting 子标签中的name属性对应的值是否是Configuration的属性,不是则抛异常
   * @param context
   * @return
   */
  private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      // 判断是否有set 方法
      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;
  }

2、设值自定义的VFS 实现类并设值到configuration对象中

private void loadCustomVfs(Properties props) throws ClassNotFoundException {
    String value = props.getProperty("vfsImpl");
    if (value != null) {
      String[] clazzes = value.split(",");
      for (String clazz : clazzes) {
        if (!clazz.isEmpty()) {
          @SuppressWarnings("unchecked")
          Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
          configuration.setVfsImpl(vfsImpl);
        }
      }
    }
  }

3、加载自定义的日志实现类并设值到configuration对象中

private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }

4、将setting中其他配置加载并设值到configuration对象中

/**
   * setting 属性值设值到configuration对象中
   * @param props
   */
  private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    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")));
  }

typeAliases

使用 typeAliases 标签给解析到的类起一个别名,或者也可以自己给类自定义别名。 使用方法如下:

<typeAliases>
    <typeAlias alias="User" type="entity.UserInfo"/>
<!--    <package name="entity.UserInfo"/>-->
  </typeAliases>

解析的方法是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);
        } else {
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            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);
          }
        }
      }
    }
  }

该方法会解析typeAliases 下的所有标签元素如果解析到package标签,它会package标签下指定的包下的所有类都给注册到configuration#typeAliasRegistry别名注册表中,注册时会把类名全部转成小写字母然后作为key,类权限定名作为value,注册到注册表中。 如果解析到typeAlias标签,会获取alias和type 属性,如果alias属性没有会判断type 类上是否有Alias 注解,如果有就用Alias注解中的表名作为key,没有类名转为小写字母注册。 注册源码如下:

public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
  }

两个标签最终都会调用TypeAliasRegistry#registerAlias(String alias, Class<?> value)方法

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

注意: package 和typeAlias 这两个标签最好不要同时使用,如果同一个类被注册两次是会抛异常的,所以最好不要重复扫描。

plugins

plugins 标签中可以指定自定义实现Interceptor的拦截器。官网 关于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.addInterceptor(interceptorInstance);
      }
    }
  }

它会把解析到的自定义拦截器注册到configuration#interceptorChain 拦截器链中,InterceptorChain 类中维护中一个Interceptor list数组,使用的时候会循环调用Interceptor 的plugin方法,最终是通过动态代理使用调用的。

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

objectFactory

objectWrapperFactory

reflectorFactory

environments

databaseIdProvider

objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider 这几个官网介绍也详细,源码感兴趣的可以看一下。

typeHandlers

typeHandlers 类型处理器 是在数据库查到结果进行映射时使用的。 在XMLConfigBuilder父类BaseBuilder中维护着一个typeHandlerRegistry 注册表,解析到的类型处理器都会注册到注册表中,跟typeAliasRegistry类似,只不过作用不一样,它是以Type对象作为key,以Map<JdbcType, TypeHandler<?>>对象作为value

/**
   * 类型映射
   * @param parent
   */
  private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          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);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }

最终调用的方法 TypeHandlerRegistry#register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
      }
      map.put(jdbcType, handler);
      typeHandlerMap.put(javaType, map);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

mappers

在以上配置都解析完成以后mappers 映射才开始解析,它是解析*Mpper.xml文件的。 mappers 标签是告诉mybatis去哪里找SQL映射文件, 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等,如:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

XMLConfigBuilder#mapperElement(XNode parent)源码如下,在mapperElement方法中又引出了mybatis初始化的另一个核心类XMLMapperBuilder。

/**
   *  解析mapper 中*Mapper.xml文件
   * @param parent
   * @throws Exception
   */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 解析package 标签
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {

          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try(InputStream inputStream = Resources.getUrlAsStream(url)){
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } 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.");
          }
        }
      }
    }
  }

解析步骤:
1、先解析是否有package标签,有的话就把对象包下的Mpper类注册到configuration对象下的mapperRegistry注册表中。最终调用的方法是MapperRegistry#addMapper(Class type)方法, addMapper 方法中会对Mapper类进行注解扫描。先看addMapper 源码

/**
   * 把mapper 接口添加到 knownMappers 中注册中心
   * @param type
   * @param <T>
   */
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 建立mapper 和 MapperProxyFactory 的连接
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.

        // 解析接口上的注解信息并添加到configuration对象中
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

MapperAnnotationBuilder#parse 进行注解扫描并解析映射器方法的@Selcet的注解,生成MappedStatement对象,MappedStatement是存储Selcet、update、insert、delete节点的信息的,里面包含着这些节点的重要信息,MappedStatement在以后的文章中会介绍。

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();

      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        // 如果方法有 Select、 SelectProvider注解 并且有ResultMap 注解,解析 方法
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          parseResultMap(method);
        }
        try {
        //解析节点信息
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

2、解析mapper标签,获取resource、url、class属性并解析,解析resource和url时会生成 XMLMapperBuilder 对象,通过XMLMapperBuilder 解析获取*Mapper.xml信息,解析class属性时跟解析pakage标签一样进行映射器注册。

// 使用相对于类路径的资源引用
          String resource = child.getStringAttribute("resource");
          // 使用完全限定资源定位符(URL)
          String url = child.getStringAttribute("url");
          //使用映射器接口实现类的完全限定类名
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try(InputStream inputStream = Resources.getUrlAsStream(url)){
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } 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.");
          }

注意: package标签是 扫描Mapper类的,它会把mapper类注册到mapper注册表中,同时会扫描类中的方法是否有SQL语句的注解,例如:@Selcet、@Insert等,如果使用的是mybatis全注解的话可以使用package标签,当然混用也行,但是切记不要将同一个*Mapper.java 不要注册两次,否则会报错,也就是说package 和 mapper 标签不要进行重复注册。

XMLConfigBuilder 内容大致就这些,如果感兴趣可以自己探索一下源码。

能力有限,水平一般,如有错误,请多指出。