上篇文章中,我们介绍了如何通过JDBC和Mybatis来连接并操作数据库。通过对Mybatis连接数据库的介绍,我们发现,Mybatis使用的核心是那份配置文件mybatis-config.xml,这是我们使用Mybatis的入口。本篇文章,我们就来看一下,该配置文件是如何解析并生效的。
还有一点,我们需要提前明确。就是我们使用JDBC时,大致存在如下步骤:
- 加载驱动程序
- 获得数据库连接
- 创建Statement
- 执行Statement
而使用Mybatis时,对于开发人员来说就不用关心获取数据库连接以及通过连接获取Statement等操作了。那么是不是Mybatis废弃了这些操作,使用了其它“高端”的操作呢?
答案是否定的,Mybatis本质上说就是对JDBC的包装。通过Mybatis可以让开发人员不用过多关注与数据库连接,以及一些结果映射的工作上,而是让开发人员可以集中于SQL本身,简化JDBC操作,Mybatis本质上也是使用JDBC操作数据库的。之后的文章,我们会陆续去介绍Mybatis使用JDBC操作数据库的原理。
1. 配置文件解析入口XMLConfigBuilder
我们上篇文章中,我们是通过如下方式使用的Mybatis。
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
// 使用Mapper中的方法操作数据库
} finally {
sqlSession.close();
}
}
不难发现,对于我们的配置文件mybatis-config.xml,我们首先将配置文件转换成InputStream,然后将InputStream作为参数传给了SqlSessionFactoryBuilder的build方法。通过调用SqlSessionFactoryBuilder的build方法,我们得到了一个SqlSessionFactory对象(生产SqlSession的Factory)。所以配置文件的解析肯定在SqlSessionFactoryBuilder中。我们继续跟进来看一下SqlSessionFactoryBuilder的build方法。
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 1. 创建一个配置文件解析器XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 2. 调用配置文件解析器的parse方法,将配置文件解析为Configuration对象
// 调用build方法,通过Configuration对象构建DefaultSqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
所以配置文件解析,其实就是使用XMLConfigBuilder将配置文件解析为Configuration对象。XMLConfigBuilder就是用于解析Mybatis配置文件的,继承于BaseBuilder。BaseBuilder有很多子类实现,每一种子类实现都对应一种xml或者xml中的节点解析。
比如XMLConfigBuilder用于解析解析Mybatis配置文件生成Configuration对象,XMLMapperBuilder用于解析Mybatis配置文件中的”/mapper”节点,并将”/mapper”节点的子节点解析到Configuration对应的成员变量中。
首先来看一下XMLConfigBuilder的构造方法。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(inputStream));
}
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;
}
最终XMLConfigBuilder的parser成员变量类型为XPathParser,通过调用XPathParser构造函数生成的。同时为其父类BaseBuilder的configuration成员变量传入一个通过调用Configuration默认构造函数获得的对象。
public Configuration(Environment environment) {
this();
this.environment = environment;
}
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
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);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
Configuration类的默认构造函数,核心就是注册一些别名,这些别名可以再mapper.xml配置文件中使用,我们也可以在mapper.xml中自定义一些别名,这部分以及typeAliasRegistry的作用我们后面再介绍。
这个图可以比较清晰地说明这三个类之间的关系,Configuration其实就是Mybatis配置文件最终解析生成对象的类型,所以包含了很多成员变量,这些成员变量都在Mybatis配置文件中有对应的标签;而XMLConfigBuilder是用来生成Configuration对象的,继承自BaseBuilder。
介绍完XMLConfigBuilder的构造函数后,我们进入核心方法的介绍——parse()方法,我们上面介绍过XMLConfigBuilder的parse方法,完成了xml配置文件到Configuration对象的解析。所以接下来,我们先来看一下一份完整的需要解析的Mybatis配置文件中有哪些内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入外部配置文件,也可以自定义一些属性 -->
<properties resource="dataSource.properties">
<property name="pKey" value="pValue"/>
</properties>
<!-- 类型别名 -->
<typeAliases>
<!-- 在用到UserInfo类型的时候,可以直接使用别名,不需要再输入UserInfo类的全路径 -->
<typeAlias type="com.zhuoli.service.mybatis.explore.model.UserInfo" alias="userInfo"/>
</typeAliases>
<!-- 类型处理器 -->
<typeHandlers>
<!-- 类型处理器的作用是完成JDBC类型和java类型的转换,mybatis默认已经定义了很多类型处理器,正常无需自定义-->
</typeHandlers>
<!-- 对象工厂 -->
<!-- mybatis创建结果对象的新实例时,会通过对象工厂来完成,mybatis中定义了默认的对象工厂,正常无需配置 -->
<objectFactory type=""></objectFactory>
<!-- 插件 -->
<plugins>
<!-- 可以通过plugin标签,添加拦截器,比如分页拦截器 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 分页合理化参数,默认文false;pageNum <= 0,查询第一页;pageNum > 总页数,查询最后一页-->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
<!-- 全局配置参数,Mybatis中定义了默认值,这里无定制需求,也可以不定义 -->
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="useGeneratedKeys" value="true" /> <!-- 是否自动生成主键 -->
<setting name="defaultExecutorType" value="REUSE" />
<setting name="lazyLoadingEnabled" value="true"/> <!-- 延迟加载标识 -->
<setting name="aggressiveLazyLoading" value="true"/> <!--有延迟加载属性的对象是否延迟加载 -->
<setting name="multipleResultSetsEnabled" value="true"/> <!-- 是否允许单个语句返回多个结果集 -->
<setting name="useColumnLabel" value="true"/> <!-- 使用列标签而不是列名 -->
<setting name="autoMappingBehavior" value="PARTIAL"/> <!-- 指定mybatis如何自动映射列到字段属性; NONE:自动映射;PARTIAL:只会映射结果没有嵌套的结果;FULL:可以映射任何复杂的结果 -->
<setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认执行器类型 -->
<setting name="defaultFetchSize" value=""/>
<setting name="defaultStatementTimeout" value="5"/> <!-- 驱动等待数据库相应的超时时间,单位是秒-->
<setting name="safeRowBoundsEnabled" value="false"/> <!-- 是否允许使用嵌套语句RowBounds -->
<setting name="safeResultHandlerEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/> <!-- 下划线列名是否自动映射到驼峰属性:如user_id映射到userId -->
<setting name="localCacheScope" value="SESSION"/> <!-- 本地缓存(session是会话级别) -->
<setting name="jdbcTypeForNull" value="OTHER"/> <!-- 数据为空值时,没有特定的JDBC类型的参数的JDBC类型 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> <!-- 指定触发延迟加载的对象的方法 -->
<setting name="callSettersOnNulls" value="false"/> <!--如果setter方法或map的put方法,如果检索到的值为null时,数据是否有用 -->
<setting name="logPrefix" value="XXXX"/> <!-- mybatis日志文件前缀字符串 -->
<setting name="logImpl" value="SLF4J"/> <!-- mybatis日志的实现类 -->
<setting name="proxyFactory" value="CGLIB"/> <!-- mybatis代理工具 -->
</settings>
<!-- 环境配置 -->
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${dataSource.driver}"/>
<property name="url" value="${dataSource.url}"/>
<property name="username" value="${dataSource.username}"/>
<property name="password" value="${dataSource.password}"/>
</dataSource>
</environment>
</environments>
<!-- mapper文件映射配置 -->
<mappers>
<mapper resource="mapper/user_info.xml"/>
</mappers>
</configuration>
可以看到,根节点是<configuration>标签,标签下存在很多子标签。接下来开始介绍XMLConfigBuilder的parse方法。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
方法中会使用上面初始化的XPathParser的evalNode方法获取配置文件中<configuration>节点对应的XNode,其实也比较简单,我们先来介绍一下XPathParser取xml节点的逻辑。
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
return new XNode(this, node, variables);
}
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
这里的document其实是XPathParser的成员变量,通过xml配置文件流创建的,类型为org.w3c.dom.Document。而最终调用的xpath.evaluate,其实就是使用JDK中自带的工具类XPath解析xml的指定节点到具体Java类型,这里解析为XNode类型。所以我们通过XPathParser的evalNode方法,传入了”configuration”,获取到<configuration>节点对应的XNode。
接下来继续来看parseConfiguration方法。
private void parseConfiguration(XNode root) {
try {
// 1. 解析<configuration>节点下<properties>配置
propertiesElement(root.evalNode("properties"));
// 2. 解析<configuration>节点下<settings>配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 从settings中,加载vfs
loadCustomVfs(settings);
// 从settings中,加载logImpl属性
loadCustomLogImpl(settings);
// 3. 解析<configuration>节点下<typeAliases>配置
typeAliasesElement(root.evalNode("typeAliases"));
// 4. 解析<configuration>节点下<plugins>配置
pluginElement(root.evalNode("plugins"));
// 5. 解析<configuration>节点下<objectFactory>配置
objectFactoryElement(root.evalNode("objectFactory"));
// 6. 解析<configuration>节点下<objectWrapperFactory>配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 7. 解析<configuration>节点下<reflectorFactory>配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 8. 将settings中的内容设置到Configuration对象中
settingsElement(settings);
// 9. 解析<configuration>节点下<environments>配置
environmentsElement(root.evalNode("environments"));
// 10. 解析<configuration>节点下<databaseIdProvider>配置
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 11. 解析<configuration>节点下<typeHandlers>配置
typeHandlerElement(root.evalNode("typeHandlers"));
// 12. 解析<configuration>节点下<mappers>配置
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
不难发现,该方法其实就是Mybatis配置文件解析的入口。下面我们就来重点介绍一下如上parseConfiguration中调用的一些方法。
2. 配置文件解析
2.1 properties解析
<configuration>节点下的<properties>节点,主要用于配置一些KV对,如下:
<!-- 引入外部配置文件,也可以自定义一些属性 -->
<properties resource="dataSource.properties">
<property name="pKey" value="pValue"/>
</properties>
解析上述这段<properties>内容的方法是propertiesElement。
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 1. 获取<properties>节点下子节点<property>配置的KV对
Properties defaults = context.getChildrenAsProperties();
// 2. 获取<properties>节点的resource|url属性
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.");
}
// 3. 从resource|url中获取KV对,并添加到defaults中
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 4. 从configuration中获取KV对存储的成员variables值
Properties vars = configuration.getVariables();
// 如果configuration中已经存在KV对,则合并到<properties>解析的KV结果中
if (vars != null) {
defaults.putAll(vars);
}
// 5. 将合并后的结果设置到XPathParser和Configuration的variables成员中
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<>();
NodeList nodeList = node.getChildNodes();
if (nodeList != null) {
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
解析properties主要分三个步骤:
- 解析properties节点的子节点,并将解析结果设置到Properties对象中
- <properties>节点的resource和url属性不为空,则从文件系统或网络中读取属性配置
- 将解析出的Properties对象设置到XPathParser和Configuration对象中
需要注意的是,propertiesElement方法是先解析properties节点的子节点内容,再从文件系统或者网络读取属性配置,会存在同名属性覆盖的问题,也就是从文件系统,或者网络中读取到的属性及属性值会覆盖掉properties子节点中同名的属性和及值。
2.2 settings解析
<configuration>节点下的<settings>节点,主要用于对Mybatis进行一些设置,如下:
<!-- 全局配置参数,Mybatis中定义了默认值,这里无定制需求,也可以不定义 -->
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="useGeneratedKeys" value="true" /> <!-- 是否自动生成主键 -->
<setting name="defaultExecutorType" value="REUSE" />
<setting name="lazyLoadingEnabled" value="true"/> <!-- 延迟加载标识 -->
<setting name="aggressiveLazyLoading" value="true"/> <!--有延迟加载属性的对象是否延迟加载 -->
<setting name="multipleResultSetsEnabled" value="true"/> <!-- 是否允许单个语句返回多个结果集 -->
<setting name="useColumnLabel" value="true"/> <!-- 使用列标签而不是列名 -->
<setting name="autoMappingBehavior" value="PARTIAL"/> <!-- 指定mybatis如何自动映射列到字段属性; NONE:自动映射;PARTIAL:只会映射结果没有嵌套的结果;FULL:可以映射任何复杂的结果 -->
<setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认执行器类型 -->
<setting name="defaultFetchSize" value=""/>
<setting name="defaultStatementTimeout" value="5"/> <!-- 驱动等待数据库相应的超时时间,单位是秒-->
<setting name="safeRowBoundsEnabled" value="false"/> <!-- 是否允许使用嵌套语句RowBounds -->
<setting name="safeResultHandlerEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/> <!-- 下划线列名是否自动映射到驼峰属性:如user_id映射到userId -->
<setting name="localCacheScope" value="SESSION"/> <!-- 本地缓存(session是会话级别) -->
<setting name="jdbcTypeForNull" value="OTHER"/> <!-- 数据为空值时,没有特定的JDBC类型的参数的JDBC类型 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> <!-- 指定触发延迟加载的对象的方法 -->
<setting name="callSettersOnNulls" value="false"/> <!--如果setter方法或map的put方法,如果检索到的值为null时,数据是否有用 -->
<setting name="logPrefix" value="XXXX"/> <!-- mybatis日志文件前缀字符串 -->
<setting name="logImpl" value="SLF4J"/> <!-- mybatis日志的实现类 -->
<setting name="proxyFactory" value="CGLIB"/> <!-- mybatis代理工具 -->
</settings>
解析上述<settings>节点的方法是settingsAsProperties。
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 1. 获取子节点的name -> value对,设置到Properties对象中
Properties props = context.getChildrenAsProperties();
// 2. 创建Configuration类的“元信息”对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
// 3. 遍历上述第1步获取的Properties对象,检查Configuration类中是否存在相关set方法,如果不存在抛异常
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).");
}
}
// 4. 返回<settings>解析出的Properties对象
return props;
}
关于MetaClass检查,其实就是检查<setttings>子节点<setting>的name属性对应的值,必须在Configuration类有相应的Setter,比如设置了一个属性useGenerateKeys,那么必须在Configuration类中有setUseGenerateKeys方法才行。
settingsAsProperties方法,只是将xml配置文件中<settings>节点解析为Properties对象,最终起作用,肯定还需要将该Properties对象设置回Configuration对象中,而设置回Configuration的过程就在settingsElement方法中。
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")));
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
}
可以看到,设置每个属性时,都有默认值。所以我们的xml配置文件中没有自定义配置<settings>节点,在settingsAsProperties方法中解析出一个空的Properties对象,Mybatis也可以进行默认设置。
2.3 typeAliases解析
MyBatis中,可以为我们自定义的类定义一个别名。这样在使用的时候,我们只需要用别名即可,无需再把全限定的类名写出来。
在MyBatis中,有两种方式进行别名配置。第一种是仅配置包名,让MyBatis去扫描包中的类型,并根据类型得到相应的别名,如下:
<typeAliases>
<package name="com.zhuoli.service.mybatis.explore.model"/>
</typeAliases>
第二种方式是通过手动的方式,明确为某个类型配置别名,如下:
<!-- 类型别名 -->
<typeAliases>
<!-- 在用到UserInfo类型的时候,可以直接使用别名,不需要再输入UserInfo类的全路径 -->
<typeAlias type="com.zhuoli.service.mybatis.explore.model.UserInfo" alias="userInfo"/>
</typeAliases>
解析上述<typeAliases>节点的方法是typeAliasesElement。
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 1. 从指定的包中解析别名和类型的映射
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//2. 从typeAlias节点中解析别名和类型的映射
// 获取<typeAlias>节点alias和type属性值,alias不是必填项,可为空
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// 3. 加载type对应的类型
Class<?> clazz = Resources.classForName(type);
// 4. 注册别名到类型的映射
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);
}
}
}
}
}
2.3.1 从指定的包中解析别名和类型的映射
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 1. 查找包下的父类为Object.class的类,将查找结果缓存到resolverUtil的内部集合中
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 2. 获取包下所有的查找结果
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
// 3. 遍历查找结果,注册别名和类型
for (Class<?> type : typeSet) {
// 忽略匿名类,接口,内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
2.3.2 从typeAlias节点中解析并注册别名
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(Class<?> type) {
// 1. 获取全路径类名的简称
String alias = type.getSimpleName();
// 2. 如果类上存在@Alias注解,则从注解中获取简称
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 3. 调用registerAlias重载方法注册别名和类型
registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// 1. 将别名转成小写
String key = alias.toLowerCase(Locale.ENGLISH);
// 2. 如果typeAliases已经存在某个名称的映射,并且类型跟要注册的类型相等,抛异常
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() + "'.");
}
// 3. 注册别名和类型到typeAliases
typeAliases.put(key, value);
}
2.4 plugins解析
插件是MyBatis提供的一个拓展机制,通过插件机制我们可在SQL执行过程中的某些点上做一些自定义操作,比如分页插件,在SQL执行之前动态拼接分页语句。插件的配置如下:
<!-- 插件 -->
<plugins>
<!-- 可以通过plugin标签,添加拦截器,比如分页拦截器 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 分页合理化参数,默认文false;pageNum <= 0,查询第一页;pageNum > 总页数,查询最后一页-->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
解析上述<plugins>节点的方法是typeAliasesElement。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历<plugins>节点下所有的<plugin>节点
for (XNode child : parent.getChildren()) {
// 1. 获取<plugin>节点的interceptor属性(plugin类的全限定名)
String interceptor = child.getStringAttribute("interceptor");
// 2. 获取<plugin>节点下的子节点name-value对,生成properties
Properties properties = child.getChildrenAsProperties();
// 3. 反射实例化plugin拦截器
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 4. 为拦截器设置属性
interceptorInstance.setProperties(properties);
// 5. 添加拦截器到Configuration中
configuration.addInterceptor(interceptorInstance);
}
}
}
// Configuration
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
这里interceptorChainf为Configuration类成员变量,类型为org.apache.ibatis.plugin.InterceptorChain。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
其实就是维护了一个拦截器集合。
2.5 environments解析
<environments>主要用于配置环境,比如我们有多个环境,开发环境,预发环境,线上环境,每个环境对应的Mysql资源是不一样的,这时候我们可以通过<environments>配置多套环境的Mysql资源。不用在发布到不同环境之前,修改Mysql连接。其实<environments>也主要是用于配置数据源和事务管理器,如下:
<!-- 环境配置 -->
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${dataSource.driver}"/>
<property name="url" value="${dataSource.url}"/>
<property name="username" value="${dataSource.username}"/>
<property name="password" value="${dataSource.password}"/>
</dataSource>
</environment>
</environments>
解析上述<environments>节点的方法是environmentsElement。
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// 1. 获取默认的JDBC环境名称
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 2. 遍历<environments>标签下的每一个<environment>标签
for (XNode child : context.getChildren()) {
// 2.1 获取<environment>标签id属性
String id = child.getStringAttribute("id");
// 2.2 判断当前的<environment>是不是默认的JDBC环境,如果是则解析该环境下的TransactionFactory和DataSource
if (isSpecifiedEnvironment(id)) {
// 解析当前环境下配置的TransactionFactory
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());
}
}
}
}
2.2判断当前的<environment>是不是默认的JDBC环境,也就<environments>配置的default属性值。所以不难看出:
- <environments>标签下的default属性是一个必填属性(否则Configuration的transactionFactory和dataSource无法被赋值)
- 可以对多个环境配置多个的<environment>标签,MyBatis只会加载其中的一个<environment>(default配置的环境)
transactionManagerElement方法是根据<transactionManager>标签,获取事物管理器。上述配置文件中配置的是”JDBC”,那么实例化出来的是JdbcTransactionFactory(”JDBC“到JdbcTransactionFactory的对应关系在Configuration对象的typeAliasRegistry中)。除了JdbcTransactionFactory之外,还有ManagedTransactionFactory和SpringManagedTransactionFactory,其中JdbcTransactionFactory和ManagedTransactionFactory是MyBatis原生支持的,SpringManagedTransactionFactory是Spring框架支持的。
dataSourceElement方法是根据<dataSource>标签,获取数据源工厂DataSourceFactory。本系列文章配置的是”POOLED”,那么实例化出来的是PooledDataSourceFactory(”POOLED“到PooledDataSourceFactory的对应关系在Configuration对象的typeAliasRegistry中),除了PooledDataSourceFactory之外,还有UnpooledDataSourceFactory和JndiDataSourceFactory,都是MyBatis原生支持的。
getDataSource方法,根据DataSourceFactory获取DataSource,在MyBatis中根据配置分三种类型:
- PooledDataSourceFactory对应的DataSource是PooledDataSource
- UnpooledDataSourceFactory对应的DataSource是UnpooledDataSource
- JndiDataSourceFactory对应的DataSource要去JNDI服务上去找
2.6 mappers解析
<mappers>节点用于配置sql映射文件,一般有如下几种配置方式:
<!-- mapper文件映射配置 -->
<mappers>
<package name="com.zhuoli.service.mybatis.explore.mapper"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
<mapper resource="mapper/user_info.xml"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
<mapper url="/Users/chenhao/Documents/IdeaProject/mybatis-explore/src/main/resources/mapper/user_info.xml"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
<mapper class="com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper"/>
</mappers>
解析上述<environments>节点的方法是mapperElement。
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");
configuration.addMappers(mapperPackage);
} else {
// 否则子节点为<mapper>,则获取子节点<mapper>的"resource"、"url"、"class"属性,三个属性只能定义其中的一个
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// "resource"不为空
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) {
// "url"属性不为空
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) {
// "class"属性不为空
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.");
}
}
}
}
}
关于sql映射文件到的解析过程,我们放在下篇文章详细介绍,这里不过多阐述了。我们只需要明确,通过mapperElement,我们将sql映射文件解析到了Configuration类的某些成员变量中。而之后我们通过解析得到的Configuration对象构建出SqlsessionFactory,通过SqlsessionFactory可以构建出Sqlsession,而构建出的Sqlsession是包含我们解析的Configuration信息的(最终mybatis配置文件可以生效,就是因为解析得到的Configuration对象被传递到Sqlsession中)。
3. 构建SqlsessionFactory
回到SqlSessionFactoryBuilder类的build方法:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 1. 创建一个配置文件解析器XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 2. 调用配置文件解析器的parse方法,将配置文件解析为Configuration对象
// 调用build方法,通过Configuration对象构建DefaultSqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
通过调用XMLConfigBuilder的parse方法,我们将mybatis配置文件解析为Configuration对象,之后我们调用XMLConfigBuilder中重载的build方法,构建出一个DefaultSqlSessionFactory对象。
参考链接:
1. Mybatis源码