MyBatis源码阅读-XMLConfigBuilder解析mybatis-config.xml

347 阅读10分钟

那么XMLConfigBuilder是什么呢?XMLConfigBuilder是专门用于解析mybatis-config.xml,将解析结果放入Configuration中,特别说明,本文中许多示例可以从mybatis官方说明中查看到

构造器解析

先来看看XMLConfigBuilder提供的构造器

public XMLConfigBuilder(Reader reader)
public XMLConfigBuilder(Reader reader, String environment)
public XMLConfigBuilder(Reader reader, String environment, Properties props)
public XMLConfigBuilder(InputStream inputStream)
public XMLConfigBuilder(InputStream inputStream, String environment)
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props)
private XMLConfigBuilder(XPathParser parser, String environment, Properties props)

真正运行代码的构造器是private XMLConfigBuilder(XPathParser parser, String environment, Properties props),那么XPathParser是什么呢?可以理解为xml的解析器,后续我们单独解析,

先说下XPathParser主要几个方法的作用

  • evalNode 获取xml中节点信息XNode

我们看看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;
}

构造器做的事情简单

  • (1)创建一个Configuration对象并且调用父类BaseBuilder的构造器
  • (2)将传入的参数放入到configuration
  • (3)parsed,标记参数,判断是否已经解析,默认false
  • (4)设置默认环境变量Id
  • (5)设置XML解析器

继续向下看通过调用XMLConfigBuilderparse()

parse解析

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

parse() 做了什么呢?

  • 首先判断是否XMLConfigBuilder已经被解析,不允许重复解析
  • 获取mybatis-config.xmlconfiguration节点信息,调用parseConfiguration开始解析
  • 最终返回解析后 Configuration信息

parseConfiguration解析

parseConfiguration是后面分析的重点,将对每个方法单独解析,那么继续分析parseConfiguration方法为我们做了什么?

private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      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);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

properties xml节点解析

propertiesElement(root.evalNode("properties"));解析properties节点 也就是如下信息

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

源码阅读太枯燥了,生活还是要继续!! 后续解析用注释看了

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      
      //获取properties节点下子节点的property的信息
      Properties defaults = context.getChildrenAsProperties();
      //获取properties节点上的属性 resource或者url
      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.");
      }
      
      //从外部资源中获取property信息,并且覆盖子节点中相同key的值
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      
      //获取构造器中设置的props的值
      Properties vars = configuration.getVariables();
      
      //将从方法中传入的property的值放到defaults,覆盖相同key的值
      if (vars != null) {
        defaults.putAll(vars);
      }
      
      //将配置信息放入到解析器中,为后续解析使用
      parser.setVariables(defaults);
      //将最终的配置信息重新放入到configuration的variables中
      configuration.setVariables(defaults);
    }
}

从解析propertiesElement我们知道了mybatis的配置信息的优先级,由高->低分别为: 方法中传入的配置 > properties节点的外部资源配置 > properties的子节点配置

settings xml节点解析

先看看settings节点什么样子

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

再看看对settings的解析

  private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    
    //获取所有子节点信息放到Properties中
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    
    //下面一段就是判断Configuration中是否有Properties的key对应的set方法
    //MetaClass是mybatis用于简化反射操作的封装类
    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;
  }

VFS 设置

VFS是虚拟文件系统;主要作用是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。

  private void loadCustomVfs(Properties props) throws ClassNotFoundException {
    //从配置中获取vfsImpl信息,多个用","分割,需要继承org.apache.ibatis.io.VFS类
    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);
        }
      }
    }
  }
  public void setVfsImpl(Class<? extends VFS> vfsImpl) {
    if (vfsImpl != null) {
      this.vfsImpl = vfsImpl;
      VFS.addImplClass(this.vfsImpl);
    }
  }

loadCustomLogImpl

获取日志的实现类,Mybatis 通过使用内置的日志工厂提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging
  private void loadCustomLogImpl(Properties props) {
  
    //logImpl支持使用别名
    //configuration 为log设置的别名有查看下面代码块
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }
    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);

typeAliases 节点解析

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:

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

下面看看mybatis具体怎么对typeAliases解析

  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        
        //package 类型通过包名,将包下所有java(不包括内部类,接口)
        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);
          }
        }
      }
    }
  }

继续查看怎么存储别名的,主要通过TypeAliasRegistry类的registerAliases方法存储别名,具体放到HashMap容器,以下为registerAliases实现

  /**
   * 将包下所有java(不包括内部类,接口)注册到别名存储器中
   * 该方法会调用registerAlias(Class<?> type) 
   */
  public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new 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);
      }
    }
  }
 
  /**
   * 将不带别名的类注册到别名存储器中,默认别名为类名
   * 如果类上有注解 Alias,则获取注解中设置的别名
   * 该方法会调用public void registerAlias(String alias, Class<?> value)
   */
  public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
  }

  /**
   * 别名为空,抛出异常
   * 将别名都转换成小写,判断别名是否存在,如果存在,抛出异常
   * 最后通过put方法将别名和对应的类型,存储到别名存储器中
   */
  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);
  }

  /**
   * 将类字符串转换成Class
   * 调用public void registerAlias(String alias, Class<?> value)
   */
  public void registerAlias(String alias, String value) {
    try {
      registerAlias(alias, Resources.classForName(value));
    } catch (ClassNotFoundException e) {
      throw new TypeException("Error registering type alias " + alias + " for " + value + ". Cause: " + e, e);
    }
  }

plugins 节点解析

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query)

先看看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()) {
        //获取节点上interceptor节点信息
        String interceptor = child.getStringAttribute("interceptor");
        //获取节点下子节点的配置信息
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        //将配置信息放入到interceptor中
        interceptorInstance.setProperties(properties);
        //将插件放入到配置中,从addInterceptor方法可知,interceptor中配置的类需要实现Interceptor接口,
        //除此之外,还需指定想要拦截的方法签名,也就是添加@Intercepts注解
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

下面查看示例:

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

objectFactory 节点解析

先看看mybatis-config.xml的配置

<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

实现和plugins一样,这里不多做解析,贴一下代码,说明下type需要实现ObjectFactory接口

  private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties properties = context.getChildrenAsProperties();
      ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      factory.setProperties(properties);
      configuration.setObjectFactory(factory);
    }
  }

objectWrapperFactory,reflectorFactory

解析同objectFactory不在多累赘

settingsElement

将settings节点中解析的properties的信息set到Configuration中

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

environments 节点解析

MyBatis 可以配置成适应多种环境,尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。看看environments节点配置

<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>
  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      //如果没有指定环境Id,就去默认的环境Id,也就是节点上default属性,environment就是构造器中的environment
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        //判断是否是指定环境变量
        if (isSpecifiedEnvironment(id)) {
          //解析transactionManager节点,设置事务管理器,同objectFactory解析,很简单,略过
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //解析dataSource节点,设置数据源,同objectFactory解析,很简单,略过
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();

          //创建Environment对象,放入到configuration
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }

  /**
   * 判断是否是指定的环境变量Id
   */ 
  private boolean isSpecifiedEnvironment(String id) {
    if (environment == null) {
      throw new BuilderException("No environment specified.");
    } else if (id == null) {
      throw new BuilderException("Environment requires an id attribute.");
    } else if (environment.equals(id)) {
      return true;
    }
    return false;
  }    
    }
  }

databaseIdProvider 节点解析

MyBatis 可以根据不同的数据库厂商执行不同的语句,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>
  private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // awful patch to keep backward compatibility
      //用于相后兼容
      if ("VENDOR".equals(type)) {
        type = "DB_VENDOR";
      }

      //配置信息解析放入到type中
      Properties properties = context.getChildrenAsProperties();
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();

    //根据数据源不同,设置不同的databaseId
    if (environment != null && databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }

typeHandlerElement 节点解析

先来看看配置mybatis-config.xml

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

也可通过mybatis使用自动发现功能,通过注解方式来指定 JDBC 的类型

<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

接下来看看源码解析,实现类似于typeAliases节点解析

  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方法将typeHandler加入到typeHandlerRegistry中,也就是BaseBuilder构造其中从 this.configuration.getTypeHandlerRegistry() 获取的TypeHandlerRegistry,从而加入到configuration中

  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解析比较复杂,后续单独分析