Mybatis解析配置文件
mybatis-config.xml 解析全流程
MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。
这里创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件。
BaseBuilder 抽象类扮演了构造者模式中 Builder 接口的角色,下面我们先来看 BaseBuilder 中各个字段的定义。
-
configuration(Configuration 类型):MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。
-
typeAliasRegistry(TypeAliasRegistry 类型):别名注册中心。我们在 mybatis-config.xml 配置文件中,使用 标签为很多类定义了别名。
-
typeHandlerRegistry(TypeHandlerRegistry 类型):TypeHandler 注册中心。除了定义别名之外,我们在 mybatis-config.xml 配置文件中,还可以使用
标签添加自定义 TypeHandler 实现,实现数据库类型与 Java 类型的自定义转换,这些自定义的 TypeHandler 都会记录在这个 TypeHandlerRegistry 对象中。
除了关联 Configuration 对象之外,BaseBuilder 还提供了另外两个基本能力:
-
解析别名,核心逻辑是在 resolveAlias() 方法中实现的,主要依赖于 TypeAliasRegistry 对象;
-
解析 TypeHandler,核心逻辑是在 resolveTypeHandler() 方法中实现的,主要依赖于 TypeHandlerRegistry 对象。
首先我们来了解一下 XMLConfigBuilder 的核心字段。
-
parsed(boolean 类型):状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完 mybatis-config.xml 配置文件。
-
parser(XPathParser 类型):XPathParser 对象是一个 XML 解析器,这里的 parser 对象就是用来解析 mybatis-config.xml 配置文件的。
-
environment(String 类型): 标签定义的环境名称。
-
localReflectorFactory(ReflectorFactory 类型):ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。
在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析,其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程,核心步骤如下:
-
解析
标签; -
解析
标签;
处理日志相关组件;
-
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签; -
解析
标签。
从 parseConfiguration()方法中,我们可以清晰地看到 XMLConfigBuilder 对 mybatis-config.xml 配置文件中各类标签的解析方法,下面我们就逐一介绍这些方法的核心实现。
- 处理
标签
我们可以通过标签定义 KV 信息供 MyBatis 使用,propertiesElement() 方法的核心逻辑就是解析 mybatis-config.xml 配置文件中的 标签。
从
- 处理
标签
MyBatis 中有很多全局性的配置,例如,是否使用二级缓存、是否开启懒加载功能等,这些都是通过 mybatis-config.xml 配置文件中的标签进行配置的。
XMLConfigBuilder.settingsAsProperties() 方法的核心逻辑就是解析
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 处理<settings>标签的所有子标签,也就是<setting>标签,将其name属性和value属性
// 整理到Properties对象中保存
Properties props = context.getChildrenAsProperties();
// 创建Configuration对应的MetaClass对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
// 检测Configuration对象中是否包含每个配置项的setter方法
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;
}
- 处理
和 标签
XMLConfigBuilder 中提供了 typeAliasesElement() 方法和 typeHandlerElement() 方法,分别用来负责处理标签和 标签,解析得到的别名信息和 TypeHandler 信息就会分别记录到 TypeAliasRegistry 和 TypeHandlerRegistry(前面介绍 BaseHandler 的时候,我们已经简单介绍过这两者了)。
下面我们以 typeHandlerElement() 方法为例来分析一下这个过程:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) { // 处理全部<typeHandler>子标签
if ("package".equals(child.getName())) {
// 如果指定了package属性,则扫描指定包中所有的类,
// 并解析@MappedTypes注解,完成TypeHandler的注册
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 如果没有指定package属性,则尝试获取javaType、jdbcType、handler三个属性
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 根据属性确定TypeHandler类型以及它能够处理的数据库类型和Java类型
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
// 调用TypeHandlerRegistry.register()方法注册TypeHandler
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
- 处理
标签
我们知道 MyBatis 是一个非常易于扩展的持久层框架,而插件就是 MyBatis 提供的一种重要扩展机制。
我们可以自定义一个实现了 Interceptor 接口的插件来扩展 MyBatis 的行为,或是拦截 MyBatis 的一些默认行为。插件的工作机制我们会在后面的课时中详细分析,这里我们重点来看 MyBatis 初始化过程中插件配置的加载,也就是 XMLConfigBuilder 中的 pluginElement()方法,该方法的核心就是解析
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历全部的<plugin>子标签
for (XNode child : parent.getChildren()) {
// 获取每个<plugin>标签中的interceptor属性
String interceptor = child.getStringAttribute("interceptor");
// 获取<plugin>标签下的其他配置信息
Properties properties = child.getChildrenAsProperties();
// 初始化interceptor属性指定的自定义插件
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 初始化插件的配置
interceptorInstance.setProperties(properties);
// 将Interceptor对象添加到Configuration的插件链中保存,等待后续使用
configuration.addInterceptor(interceptorInstance);
}
}
}
- 处理
标签
MyBatis 支持自定义 ObjectFactory 实现类和 ObjectWrapperFactory。XMLConfigBuilder 中的 objectFactoryElement() 方法就实现了加载自定义 ObjectFactory 实现类的功能,其核心逻辑就是解析标签中配置的自定义 ObjectFactory 实现类,并完成相关的实例化操作,相关的代码实现如下:
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 获取<objectFactory>标签的type属性
String type = context.getStringAttribute("type");
// 根据type属性值,初始化自定义的ObjectFactory实现
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 初始化ObjectFactory对象的配置
Properties properties = context.getChildrenAsProperties();
factory.setProperties(properties);
// 将ObjectFactory对象记录到Configuration这个全局配置对象中
configuration.setObjectFactory(factory);
}
除了
- 处理
标签
在 MyBatis 中,我们可以通过标签为不同的环境添加不同的配置,例如,线上环境、预上线环境、测试环境等,每个 标签只会对应一种特定的环境配置。
environmentsElement() 方法中实现了 XMLConfigBuilder 处理
下面是 environmentsElement() 方法的核心逻辑:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) { // 未指定使用的环境id,默认获取default值
environment = context.getStringAttribute("default");
}
// 获取<environment>标签下的所有配置
for (XNode child : context.getChildren()) {
// 获取环境id
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 获取<transactionManager>、<dataSource>等标签,并进行解析,其中会根据配置信息初始化相应的TransactionFactory对象和DataSource对象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 创建Environment对象,并关联创建好的TransactionFactory和DataSource
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将Environment对象记录到Configuration中,等待后续使用
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
- 处理
标签
在 mybatis-config.xml 配置文件中,我们可以通过
databaseIdProviderElement() 方法是 XMLConfigBuilder 处理
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
// 获取type属性值
String type = context.getStringAttribute("type");
if ("VENDOR".equals(type)) { // 兼容操作
type = "DB_VENDOR";
}
// 初始化DatabaseIdProvider
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// 通过DataSource获取DatabaseId,并保存到Configuration中,等待后续使用
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
可以看到,解析
从继承关系图中可以看出,DefaultDatabaseIdProvider 是个空实现,而且已被标记为过时了,所以这里我们就重点来看 VendorDatabaseIdProvider 实现。
在 getDatabaseId() 方法中,VendorDatabaseIdProvider 首先会从 DataSource 中拿到数据库的名称,然后根据
public String getDatabaseId(DataSource dataSource) {
// 省略边界检查和异常处理
return getDatabaseName(dataSource);
}
private String getDatabaseName(DataSource dataSource) throws SQLException {
// 从数据库连接中,获取数据库名称
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
// 根据<databaseIdProvider>标签配置,查找自定义数据库名称
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue(); // 返回配置的value
}
}
return null;
}
return productName;
}
- 处理
标签
除了 mybatis-config.xml 这个全局配置文件之外,MyBatis 初始化的时候还会加载标签下定义的 Mapper 映射文件。 标签中会指定 Mapper.xml 映射文件的位置,通过解析 标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。
mapperElement() 方法就是 XMLConfigBuilder 处理
mapperElement() 方法的具体实现如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) { // 遍历每个子标签
if ("package".equals(child.getName())) {
// 如果指定了<package>子标签,则会扫描指定包内全部Java类型
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 解析<mapper>子标签,这里会获取resource、url、class三个属性,这三个属性互斥
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 如果<mapper>子标签指定了resource或是url属性,都会创建XMLMapperBuilder对象,
// 然后使用这个XMLMapperBuilder实例解析指定的Mapper.xml配置文件
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();
} 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();
} else if (resource == null && url == null && mapperClass != null) {
// 如果<mapper>子标签指定了class属性,则向MapperRegistry注册class属性指定的Mapper接口
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.");
}
}
}
}
}