Skip to main content

mybatis 配置文件解析

源码mybatisconfigurationAbout 9 min

从SqlSessionFactory创建说起

使用SqlSessionFactoryBuilder解析传入的mybatis.xml配置, 可以创建出SqlSessionFactory.

如下为创建SqlSessionFactory的方法.

SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
InputStream inputStream = Resources.getResourceAsStream("config/mybatis-config.xml");
return sqlSessionFactoryBuilder.build(inputStream);
  1. xml文件的解析是通过XMLConfigBuilderparse方法进行的. 该方法解析之后会返回一个Configuration对象.
  2. 最后调用build方法, 将Configuration对象作为参数, 实例化DefaultSqlSessionFactory, 得到SqlSessionFactory.

XMLConfigBuilder

初始化

XMLConfigBuilder对象需要持有Xml解析器XPathParser, 激活的当前环境信息, 属性参数. 其构造器见下:

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

这里首先初始化了一个Configuration. 并将parsed设置状态(未解析).

调用parse方法解析xml

  1. parse 方法调用后首先根据parsed判断是否已经进行过解析. 没有则将其设置为true.
  2. 之后调用parseConfiguration方法, 该方法会解析xml中的各节点配置, 并给configuration赋值.
public Configuration parse() {
if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

evalNode 方法通过Xpath表达式获取xml中的节点信息(结果封装为: XNode, 它是xml节点的类型,属性,名称xpath表达式等的表示).
parser.evalNode("/configuration") 就获取到了xml配置中的configuration节点. 并将其作为获取后续节点进行解析的根节点.


配置文件解析

properties节点

该节点的配置方式如下, resource指定了参数配置文件的位置

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

对该节点的解析使用propertiesElement方法, 方法的输入参数为root.evalNode("properties"), 即properties节点的封装.

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        Properties defaults = context.getChildrenAsProperties();
        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) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
  }

该方法主要用于获取mybatis的所有配置信息, 主要来自于4个方面:

  1. 配置文件中的propertiesproperty中的配置.
  2. properties中的resource属性所指位置的配置文件
  3. properties中的url属性所指位置的配置文件
  4. configuration对象中的属性配置, 一般程序在创建XMLConfigBuilder会进行设置.

以上的属性都会重新设置到configuration对象中去, 并交给解析器进行处理.

getChildrenAsProperties: 获取当前节点下的子节点, 将节点的name和value属性值, 存放到Properties中.

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>

对应的解析方法为settingsAsProperties, 该方法同样使用getChildrenAsProperties获取子节点中的键值, 并返回Properties对象.

之后对设置的参数进行处理, 相关方法见下:

loadCustomVfs(settings);
loadCustomLogImpl(settings);
....
settingsElement(settings);
  • loadCustomVfs: 指定VFS(虚拟文件系统)的实现.
    该方法将字符串配置反射为Class, 配置到ConfigurationClass<? extends VFS> vfsImpl属性上去.

  • loadCustomLogImpl: 指定 MyBatis 所用日志的具体实现,未指定时将自动查找.
    可使用的日志框架及实现如下:

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

    以上配置发生在Configuration构造器中.
    获取到对应的实现类后, 配置到ConfigurationClass<? extends Log> logImpl属性上去.

  • settingsElement: 设置xml中的配置对应Configuration中的属性.

typeAliases节点

typeAliasesElement方法为处理typeAliases节点的方法. 该方法对两种子节点进行处理:

  • package
  • typeAlias

获取到节点的属性配置后, 通过typeAliasRegistry.registerAlias将其添加到private final Map<String, Class<?>> typeAliases = new HashMap<>();中缓存.

plugins插件节点

使用该节点首先需要实现Interceptor接口, 实现intercept方法, 或者重写setProperties, 用于给当前拦截器设置属性.

该节点配置方式如:

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

pluginElement方法, 见下:

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

通过反射获取拦截器对象, 将其加入到拦截器调用链上去. 该调用链在InterceptorChain类中定义为:
private final List<Interceptor> interceptors = new ArrayList<>();

objectFactory节点

该节点为对象工厂配置, mybatis会使用对象工厂创建实例.
因此需要将实现了ObjectFactory的类配置到该节点的type属性上. mybatis提供了该接口的默认实现DefaultObjectFactory. 可以继承该类并做一些自定义.

该节点的解析使用objectFactoryElement方法, 通过反射获取实例, 并设置到Configuration对象上.

objectWrapperFactory节点

objectWrapperFactoryElement用于解析该节点.
节点配置类需要实现ObjectWrapperFactory接口.

reflectorFactory节点

reflectorFactoryElement用于解析该节点.
节点配置类需要实现ReflectorFactory接口.

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>

environmentsElement用于解析该节点的配置. 以下为解析代码:

  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
  1. 如果创建XMLConfigBuilder时没有指定环境配置(id), 则使用xml中的默认配置. 即获取default属性值.
  2. 遍历子节点, 找到当前环境的配置相同的id. 如果匹配则进行3步骤的解析过程, 否则继续寻找匹配的环境配置节点.
  3. 环境配置主要包括:
    • 事务工厂: TransactionFactory
      transactionManagerElement方法根据transactionManager配置, 创建TransactionFactory对象
    • 数据源: DataSource
      dataSourceElement方法根据dataSource创建一个数据源工厂对象, 并由该工厂获取数据源.

databaseIdProvider节点

mybatis根据该节点识别数据厂商, 执行sql语句.
MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。

参考配置如下:

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

databaseIdProviderElement方法会解析该节点为Configuration对象设置databaseId.

VendorDatabaseIdProviderDB_VENDOR对应的mybatis数据库厂商识别类. 它通过数据源的DatabaseMetaData对象的getDatabaseProductName方法获取数据库名称, 与属性中配置的键进行匹配, 如果包含则别名设置为该键的值. 如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为“oracle”。

typeHandlers节点

mybatis的类型转换器用于处理JDBC类型和java类型之间的转换逻辑.

参考: mybatis类型转换器open in new window

typeHandlerElement在处理该节点时, 分为两种情况:

  1. 使用package方式配置.
    这种方式会加载指定包下的所有实现TypeHandler接口的子类(推荐重写BaseTypeHandler).
    并获取该类上@MappedJdbcTypes注解所指定的JdbcType.
    最后通过typeHandlerRegistry.register完成注册.
  2. 使用typeHandler方式配置
    这种方式会直接获取javaType, jdbcType, handler属性, 通过反射获取其对应的java类型.
    最后通过typeHandlerRegistry.register完成注册.

所有的TypeHandler都放到Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();中, java类型和jdbc类型的映射关系, 由Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();管理.
TypeHandlerRegistry初始化时, 会添加默认的类型转换器.

mappers节点

该节点支持使用package作为子节点配置, 也支持使用子节点使用resource, url, class属性配置资源位置.

1. 使用package配置

配置如下:

<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

程序会获取当前包路径下的所有Class, 并依次调用addMapper(Class<T> type)方法.

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 {
      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.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

addMapper只对接口类型进行处理. 所有处理过的mapper接口都由Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();管理.

hasMapper用于判断当前接口是否存在于knownMappers, 存在则不用处理. 不存在则向knownMappers新增当前类及对应的MapperProxyFactory(用于动态代理生成实体, 执行接口方法).

之后, 创建MapperAnnotationBuilder对象用于解析当前mapper接口, parse()方法为具体的解析逻辑.

2. 使用resource配置

配置如下:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

XMLMapperBuilder是解析sql xml文件的解析器. 调用parse方法用于解析xml.

InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

3. 使用url配置

解析方式同resource

4. 使用class配置

解析方式相当于package中遍历解析Class的一个环节.

总结

几种解析器:

  1. XMLConfigBuilder: mapper配置文件xml的解析. 它的parse方法返回Configuration对象.
  2. XMLMapperBuilder: xml映射文件的解析.
  3. MapperAnnotationBuilder: 接口映射的解析. 通过ConfigurationaddMapper or addMapper调用到.
What do you think?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v3.0.0-alpha.10