XMLConfigBuilder
XMLConfigBuilder是BaseBuilder的众多子类之一,它扮演的是具体建造者的角色。XMLConfigBuilder 要负责解析mybatis-config.xml 配置文件,其核心字段如下:
// 标识过是否解析过mybatis-config.xml配置文件
private boolean parsed;
// 用于解析mybatis-config.xml配置文件中的XPathParser对象。
private final XPathParser parser;
// 标识<environment>配置的名称,默认读取<environment>标签的default属性
private String environment;
// 负责创建和缓存Reflector对象
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
XMLConfigBuilder.parse()方法是解析mybatis-config.xml配置文件的入口,它通过调用XMLConfigBuilder 的parseConfiguration()方法实现整个解析过程的具体实现如下:
public Configuration parse() {
///根据 parsed变量的值,判断是否已经完成了对mybatis-config.xml配置文件的解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 在mybatis-config.xml 配置文件中查找<configuration>节点,并开始解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 解析<properties>节点
propertiesElement(root.evalNode("properties"));
// 解析<settings>节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings); // 设置vfsImpl字段
loadCustomLogImpl(settings);// 设置logImpl字段
// 解析<typeAliases>节点
typeAliasesElement(root.evalNode("typeAliases"));
// 解析<plugins>节点
pluginElement(root.evalNode("plugins"));
// 解析<objectFactory>节点
objectFactoryElement(root.evalNode("objectFactory"));
// 解析<objectWrapperFactory>节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析<reflectorFactory>节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 将settings值设置到configuration
settingsElement(settings);
// 解析<environments>节点
environmentsElement(root.evalNode("environments"));
// 解析<databaseIdProvider>节点
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析<typeHandlers>节点
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析<mappers>节点
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
XMLConfigBuilder将mybatis-config.xml配置文件中每个节点的解析过程封装成了一个相应的方法,下面将逐一分析这些解析过程。
(1)解析<properties>节点
XMLConfi Builder propertiesElement() 方法会解析 mybatis-config.xml 配置 文件中的<properties>节点并形 java. util.Properties对象,之后将该Properties对象设置到 XPathParser和Configuration的variable字段中。在后面的解析过程中,会使用该 Properties 对象中的信息替换占位符。propertiesElement()方法的具体实现如下:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析<properties>子节点(<property>标签)的name和value属性,并记录到Properties中
Properties defaults = context.getChildrenAsProperties();
// 解析<properties>的resource和url属性,这两个属性确定properties配置文件的位置
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.");
}
// 加载resource或url属性指定的properties文件
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 与Configuration中的variables字段合并
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 更新XPathParser和Configuration的variables字段
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
(2)解析<settings>节点
XMLConfigBuilder.settingsAsProperties()方法负责解析<settings>点,在<settings>节点下的配置是 MyBatis 全局性的配置,它们会改变 MyBatis 的运行时行为。需要注意的是,在MyBatis初始化时,这些全局配置信息都会被记录到Configuration对象的对应属性中。
settingsAsProperties()方法的解析方式与 propertiesElement()方法类似但是多了使用MetaClass检测 key 定的属性在Configuration类中是否有对应的setter方法的步骤。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是否定义了key指定属性相应的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;
}
(3)解析<typeAliases>、<typeHandlers>节点
XMLConfigBuilder. typeAliasesElement()方法负责解析<typeAliases>节点及其子节点,并通TypeAliasRegistry完成别名的注册,具体实现如下:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 处理全部子节点
for (XNode child : parent.getChildren()) {
// 处理<package>节点
if ("package".equals(child.getName())) {
// 获取指定的包名
String typeAliasPackage = child.getStringAttribute("name");
// 通过TypeAliasRegistry扫描指定包中的所有类,并解析@Alias注解,完成别名的注册
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 获取指定的别名
String alias = child.getStringAttribute("alias");
// 获取别名对应的类型
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
// 扫描@alias注解,完成注册
typeAliasRegistry.registerAlias(clazz);
} else {
// 注册别名
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
XMLConfigBuilder. typeHandlerElement()方法负责解析<typeHandlers>节点及其子节点,并通过TypeHandlerRegistry对象完成TypeHandler的注册。该方法和解析<typeAliases>方法相似,不再过多说明。
(4)解析<Plugins>节点
插件是Mybatis的扩展机制之一,用户可以通过添加自定义插件在SQL语句执行过程中的某一点进行拦截。Mybatis中的自定义插件只需要实现Interceptor接口,并通过注解想要拦截的方法签名即可。
XMLConfigBuilder.pluginElement()方法解析<Plugins>节点中定义的插件,并完成实例化和配置操作,具体实现如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取<plugin>节点的interceptor的值
String interceptor = child.getStringAttribute("interceptor");
// 获取<plugin>节点下<properties>配置信息,并形成Properties对象
Properties properties = child.getChildrenAsProperties();
// 通过TypeAliasRegistry解析别名后,实例化Interceptor对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 设置Interceptor属性
interceptorInstance.setProperties(properties);
// 添加Interceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
所有配置的 Interceptor 对象都是通过Configuration.interceptorChain字段(InterceptorChain
类型)管理的,InterceptorChain底层使用ArrayList<Interceptor实现。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
// 其他方法在Plugin专题时详解
}
(5)解析<objectFactory>节点
XMLConfigBuilder.objectFactoryElement()方法负责解析井实例化<objectFactory>节点指定ObjectFactory实现类,之后将自定义的ObjectFactory对象记录到Configuration.objectFactory宇段中,具体实现如下:
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 获取<objectFactory>节点的type属性
String type = context.getStringAttribute("type");
// 获取<objectFactory>节点下的配置信息,并生成properties对象
Properties properties = context.getChildrenAsProperties();
// 别名解析后,实例化自定义objectFactory的实现
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 设置objectFactory的属性,完成初始化相关操作
factory.setProperties(properties);
// 将自定义的ObjectFactory对象记录到Configuration.objectFactory宇段中
configuration.setObjectFactory(factory);
}
}
XMLConfigBuilder对<objectWrapperFactory>节点、<reflectorFactory>节点的解析与上述过程类似,最终会将解析得到的自义对象并记录到Configuration的相应宇段中,不再过多说明。
(6)解析<environments>节点
在实际生产中 ,同一项目可能分为开发、 测试和生产多个不同的环境,每个环境的配置可能也不尽相同MyBatis可以配置多个<environment>节点,每个<environment>节点对应一种环境的配置。但需要注意的是,尽管可以配置多个环境,每个SqlSessionFactory实例只能选择其一。
XMLConfigBuilder.environmentsElement()方法负责解析<environments>的相关配置,它会根据XMLConfigBuilder.environment宇段值确定要使用的<environment>配置,之后创建对应的TransactionFactory和DataSource对象,井封装进Environment对象中。environmentsElement()方法的具体实现如下:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// 未指定XMLConfigBuilder.environment字段,则使用default属性指定的<environment>
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 遍历子节点,即<environment>节点
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// 与XMLConfigBuilder.environment字段匹配
if (isSpecifiedEnvironment(id)) {
// 创建TransactionFactory,具体实现是先通过TypeAliasRegistry解析别名之后,实例化创建TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 创建DataSourceFactory和DataSource
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 创建Environment,Environment中封装了上面创建的TransactionFactory对象以及DataSource对象,这里应用了建造者模式
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将Environment对象记录到Configuration.environment字段中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
(7)解析<databaseIdProvider>节点
在mybatis-config.xml配置文件中,通过<databaseldProvider>定义所有支持的数据库产品的databaseld,然后在映射配置文件中定义 SQL 语句节点时,通过 databaseld 指定该SQL 语句应用的数据库产品 。
在MyBatis初始化时,会根据前面确定的DataSource确定当前使用的数据库产品,然后在解析映射配置文件时,加载不带databaseld属性和带有匹配当前数据库databaseld属性的所有SQL语句。如果同时找到带有databaseld和不带databaseId相同语句,则后者会被舍弃,使用前者。
XMLConfigBuilder.databaseIdProviderElement()方法负责解析<databaseldProvider>节点,并创建指定的DatabaseldProvider对象。DatabaseldProvider会返回databaseld值,MyBatis会根据databaseld 选择合适的 SQL 进行执行。具体实现如下:
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// 为了保持兼容性
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
// 解析相关的配置信息
Properties properties = context.getChildrenAsProperties();
// 创建DatabaseIdProvider对象
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
// 配置DatabaseIdProvider对象
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// 通过前面确定的DataSource获取databaseId并记录Configuration.databaseId 字段
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
MyBatis提供的DatabaseldProvider接口及其实现比较简单,DatabaseldProvider接口的核心方法是getDatabaseld()方法,它主要负责通过给定的DataSource来查找对应databaseld,MyBatis 提供了 VendorDatabaseldProvider、DefaultDatabaseldProvider两个实现,其中 DefaultDatabaseldProvider 己过时,故不再分析。
VendorDatabaseldProvider.getDatabaseld()方法在接收到DataSource对象时,会先解析DataSource所连接的数据库产品名称,之后根据<databaseldProvider>节点配置的数据库产品名称与databaseld的对应关系确定最终databaseld。具体实现如下:
private String getDatabaseName(DataSource dataSource) throws SQLException {
// 解析DataSource连接数据库的产品名称
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
// 根据<databaseIdProvider>子节点自己置的数据库产品和databaseId之间对应关系,确定最终使用的databaseId
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// 找不到合适的则返回null
return null;
}
return productName;
}
// 解析DataSource连接数据库的产品名称具体实现
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
// 使用try-with-resource,连接自动关闭
try (Connection con = dataSource.getConnection()) {
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
}
}
(8)解析<mappers>节点
MyBatis初始化时,除了加载mybatis-config.xml配置文件,还会加载全部的映射配置文件,mybatis-config.xml配置文件中的<mappers>节点会告诉 MyBatis 去哪些位置查找映射配置文件以及使用了配置注解标识的接口。
XMLConfigBuilder.mapperElement()方法负责解析<mappers>节点,它会创建XMLMapperBuilder对象加载映射文件,如果映射配置文件存在相应的Mapper接口,也会加载相应的Mapper接口,解析其中的注解并完成向MapperRegistry的注册。具体实现如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 处理<mappers>的子节点
for (XNode child : parent.getChildren()) {
// 处理<package>节点
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// 扫描指定的包,并向MapperRegistry注册Mapper接口
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对象并通过该对象解析resource或是url属性指定的Mapper配置文件
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建XMLMapperBuilder对象,解析映射配置文件
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对象,解析映射配置文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 如果<mapper>节点指定了class属性,则向MapperRegistry注册该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.");
}
}
}
}
}
至此,MyBatis初始化过程中对mybatis-config.xml配置文件的解析过程到这就结束了。