# MyBatis 源码分析(四):运行时配置解析
在 Spring
与 MyBatis
的集成中,通常需要声明一个 sqlSessionFactory
用于初始化 MyBatis
:
<!-- 注册 sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:config/mybatis-config.xml"/>
<property name="typeAliasesPackage" value="com.wch.base.domain"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
在 bean
初始化的时候会调用 SqlSessionFactoryBean
的 afterPropertiesSet
方法,在此方法中 MyBatis
使用 XMLConfigBuilder
对配置进行解析。
# BaseBuilder 体系
XMLConfigBuilder
是 XML
配置解析的入口,继承自 BaseBuilder
,其为 MyBatis
初始化提供了一系列工具方法,如别名转换、类型转换、类加载等。
# 全局配置对象
XMLConfigBuilder
在构造方法中创建了 Configuration
对象,这个对象中用于保存 MyBatis
相关的全部配置,包括运行行为、类型容器、别名容器、注册 Mapper
、注册 statement
等。通过 XMLConfigBuilder
的 parse
方法可以看出,配置解析的目的就是为了获取 Configuration
对象。
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;
// 包装配置 InputStream 的 XPathParser
this.parser = parser;
}
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 读取 configuration 元素并解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
# 解析配置文件
配置解析分为多步,详情可参考 XML 配置。MyBatis
源码内置 mybatis-config.xsd
文件用于定义配置文件书写规则。
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 properties 元素
propertiesElement(root.evalNode("properties"));
// 加载 settings 配置并验证是否有效
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 配置自定义虚拟文件系统实现
loadCustomVfs(settings);
// 配置自定义日志实现
loadCustomLogImpl(settings);
// 解析 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 配置设置到全局配置中
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 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);
}
}
# 解析 properties 元素
properties
元素用于将自定义配置传递给 MyBatis
,例如:
<properties resource="com/wch/mybatis/config.properties">
<property name="username" value="wch"/>
<property name="password" value="Noop"/>
</properties>
其加载逻辑为将不同配置转为 Properties
对象,并设置到全局配置中:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 获取子元素属性
Properties defaults = context.getChildrenAsProperties();
// 读取 resource 属性
String resource = context.getStringAttribute("resource");
// 读取 url 属性
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.");
}
// 加载指定路径文件,转为 properties
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);
}
}
# 解析 settings 元素
setteings
元素中的各子元素定义了 MyBatis
的运行时行为,例如:
<settings>
<!-- 缓存开关 -->
<setting name="cacheEnabled" value="true"/>
<!-- 懒加载开关 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 允许自动生成主键 -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 驼峰命名开关 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
...
</settings>
这些配置在 Configuration
类中都有对应的 setter
方法。settings
元素的解析方法对配置进行了验证:
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
// 获取 Configuration 类的相关信息
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
// 验证对应的 setter 方法存在,保证配置是有效的
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
如果不存在对应的配置,会抛出 BuilderException
异常,如果自定义配置都是生效的,随后会调用 settingsElement
方法将这些运行时行为设置到全局配置中。
# 解析 typeAliases 元素
typeAliases
元素用于定义类别名:
<typeAliases>
<package name="com.wch.mybatis.User"/>
<typeAlias alias="User" type="com.wch.mybatis.User"/>
<typeAlias type="com.wch.mybatis.Item"/>
</typeAliases>
如果使用 package
元素注册别名,则对应包下的所有类都会注册到 TypeAliasRegistry
别名注册容器中;如果使用 typeAlias
元素,则会注册指定类到别名容器中。注册逻辑如下,如果没有指定别名,则优先从类的 Alias
注解获取别名,如果未在类上定义,则默认使用简单类名:
/**
* 注册指定包下所有类型别名
*
* @param packageName
*/
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
/**
* 注册指定包下指定类型的别名
*
* @param packageName
* @param superType
*/
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 找出该包下superType所有的子类
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 注解获取
*
* @param type
*/
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
// 从Alias注解读取别名
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
/**
* 注册类型别名
*
* @param alias 别名
* @param 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);
}
# 解析 plugins 元素
插件是 MyBatis
提供的扩展机制之一,通过添加自定义插件可以实现在 SQL
执行过程中的某个时机进行拦截。 plugins
元素用于定义调用拦截器:
<plugins>
<plugin interceptor="com.wch.mybatis.ExamplePlugin">
<property name="name" value="ExamplePlugin"/>
</plugin>
</plugins>
指定的 interceptor
需要实现 org.apache.ibatis.plugin.Interceptor
接口,在创建对象后被加到全局配置过滤器链中:
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).newInstance();
interceptorInstance.setProperties(properties);
// 加入全局配置拦截器链
configuration.addInterceptor(interceptorInstance);
}
}
}
objectFactory
、 objectWrapperFactory
、reflectorFactory
元素的解析方式与 plugins
元素类似 ,指定的子类对象创建后被设置到全局对象中。
# 解析 environments 元素
在实际生产中,一个项目可能会分为多个不同的环境,通过配置enviroments
元素可以定义不同的数据环境,并在运行时使用指定的环境:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<environment id="prd">
...
</environment>
</environments>
在解析过程中,只有被 default
属性指定的数据环境才会被加载:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取指定的数据源名
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 环境配置 id
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 加载指定环境配置
// 解析 transactionManager 元素并创建事务工厂实例
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 dataSource 元素并创建数据源工厂实例
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());
}
}
}
}
/**
* 解析 transactionManager 元素并创建事务工厂实例
*
* @param context
* @return
* @throws Exception
*/
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
// 指定事务工厂类型
String type = context.getStringAttribute("type");
// 从子元素读取属性配置
Properties props = context.getChildrenAsProperties();
// 加载事务工厂并创建实例
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
/**
* 解析 dataSource 元素并创建数据源工厂实例
*
* @param context
* @return
* @throws Exception
*/
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
// 指定数据源工厂类型
String type = context.getStringAttribute("type");
// 从子元素读取属性配置
Properties props = context.getChildrenAsProperties();
// 加载数据源工厂并创建实例
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
# 解析 databaseIdProvider 元素
MyBatis
支持通过 databaseIdProvider
元素来指定支持的数据库的 databaseId
,这样在映射配置文件中指定 databaseId
就能够与对应的数据源进行匹配:
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
在根据指定类型解析出对应的 DatabaseIdProvider
后,MyBatis
会根据数据源获取对应的厂商信息:
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";
}
// 从子元素读取属性配置
Properties properties = context.getChildrenAsProperties();
// 加载数据库厂商信息配置类并创建实例
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// 获取数据库厂商标识
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
因为 DB_VENDOR
被指定为 VendorDatabaseIdProvider
的别名,所以默认的获取厂商信息的逻辑如下,当通过 property
属性指定了数据库产品名则使用指定的名称,否则使用数据库元信息对应的产品名。
/**
* 根据数据源获取对应的厂商信息
*
* @param dataSource
* @return
*/
@Override
public String getDatabaseId(DataSource dataSource) {
if (dataSource == null) {
throw new NullPointerException("dataSource cannot be null");
}
try {
return getDatabaseName(dataSource);
} catch (Exception e) {
LogHolder.log.error("Could not get a databaseId from dataSource", e);
}
return null;
}
@Override
public void setProperties(Properties p) {
this.properties = p;
}
/**
* 如果传入的属性配置包含当前数据库产品名,返回指定的值,否则返回数据库产品名
*
* @param dataSource
* @return
* @throws SQLException
*/
private String getDatabaseName(DataSource dataSource) throws SQLException {
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// no match, return null
return null;
}
return productName;
}
/**
* 获取数据库产品名
*
* @param dataSource
* @return
* @throws SQLException
*/
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
Connection con = null;
try {
con = dataSource.getConnection();
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException e) {
// ignored
}
}
}
}
# 解析 typeHandlers 元素
typeHandlers
元素用于配置自定义类型转换器:
<typeHandlers>
<typeHandler handler="com.wch.mybatis.ExampleTypeHandler"/>
</typeHandlers>
如果配置的是 package
元素,则会将包下的所有类注册为类型转换器;如果配置的是 typeHandler
元素,则会根据 javaType
、jdbcType
、handler
属性注册类型转换器。
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 注册指定包下的类作为类型转换器,如果声明了 MappedTypes 注解则注册为指定 java 类型的转换器
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 获取相关属性
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 加载指定 java 类型类对象
Class<?> javaTypeClass = resolveClass(javaTypeName);
// 加载指定 JDBC 类型并创建实例
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);
}
}
}
}
}
# 解析 mappers 元素
mappers
元素用于定义 Mapper
映射文件和 Mapper
调用接口:
<mappers>
<mapper resource="com/wch/mybatis/UserMapper.xml"/>
<mapper url="file://mappers/ItemMapper.xml"/>
<mapper class="com.wch.mybatis.UserMapper"/>
<package name="com.wch.mybatis.mappers"/>
</mappers>
如果定义的是 mapper
元素并指定了 class
属性,或定义了 package
元素,则会将指定类型在 MapperRegistry
中注册为 Mapper
接口,并使用 MapperAnnotationBuilder
对接口方法进行解析;如果定义的是 mapper
元素并指定了 resource
、或 url
属性,则会使用 XMLMapperBuilder
解析。对于 Mapper
接口和映射文件将在下一章进行分析。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 注册指定包名下的类为 Mapper 接口
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);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 加载指定 Mapper 文件并解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 加载指定 URL
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
// 加载指定 Mapper 文件并解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 注册指定类为 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.");
}
}
}
}
}
# 小结
XMLConfigBuilder
是 XML
配置解析的入口,通常 MyBatis
启动时会使用此类解析配置文件获取运行时行为。
org.apache.ibatis.builder.BaseBuilder
:为MyBatis
初始化过程提供一系列工具方法。如别名转换、类型转换、类加载等。org.apache.ibatis.builder.xml.XMLConfigBuilder
:XML
配置解析入口。org.apache.ibatis.session.Configuration
:MyBatis
全局配置,包括运行行为、类型容器、别名容器、注册Mapper
、注册statement
等。org.apache.ibatis.mapping.VendorDatabaseIdProvider
:根据数据源获取对应的厂商信息。