一、前言
在之前的文章中我们已经对Mybatis配置文件的各个配置简单的介绍了他对应的作用:Mybatis源码分析(2)之mybatis配置_jokeMqc的博客-CSDN博客一、前言 在上一篇我们已经介绍了mybatis的基本使用,这章我们的篇幅主要是介绍mybatis的配置,下一章我们在具体介绍对应的源码解析,因为我们首先要知道mybatis具体有哪些配置,然后再去看框架是怎么具体解析这些源码数据的。二、Mybatis配置 2.1属性(properties)这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。例如:...https://blog.csdn.net/jokeMqc/article/details/122025976有需要的小伙伴可以先去看下这一篇对mybatis的配置文件各个配置的作用做下简单的了解,从本章开始我们就要真正的介绍mybatis的源码了,我们知道mybatis最主要的有两个配置文件,一个是mybatis-config.xml文件,另外一个就是对应的mapper.xml文件,他们的文件结构如下:
mybatis-config.xml:
<configuration>
<properties/>
<settting/>
<typeHandlers/>
<..../>
<mappers/>
</configuration>
mybatis-mapper.xml:
<mapper >
<cache/>
<resultMap/>
<select/>
<update/>
<delete/>
<insert/>
</mapper>
配置文件的解析流程即是将上述XML描述元素转换成对应的JAVA对像过程,其最终转换对像及其关系如下图:
接下来让我们来看下对应的源码解析过程。
配置元素解析构建器
- org.apache.ibatis.builder.xml.XMLConfigBuilder :解析config配置文件
- org.apache.ibatis.builder.xml.XMLMapperBuilder:解析mapper.xml文件
- org.apache.ibatis.builder.xml.XMLStatementBuilder:解析mapper.xml中的每一个statemen
- org.apache.ibatis.builder.SqlSourceBuilder:解析sql
- org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:解析<if>等脚本;
- org.apache.ibatis.builder.annotation.MapperAnnotationBuilder:解析我们的mapper接口
二、配置文件解析过程分析
2.1配置文件解析入口
在单独使用 MyBatis 时,第一步要做的事情就是根据配置文件构建SqlSessionFactory
对象。相关代码如下:
@Test
public void test() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 1);
log.info("user:{}", user);
}
对应方法为SqlSessionFactoryBuilder#build(InputStream),
首先,我们使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的build
方法构建 SqlSessionFactory 对象。所以这里的 build 方法是我们分析配置文件解析过程的入口方法。那下面我们来看一下这个方法的代码:
// build在SqlSessionFactoryBuilder中是一个重载方法
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 创建配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 利用创建的XMLConfigBuilder去解析
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过XMLConfigBuilder
进行解析的。不过目前这里还没有非常明确的解析逻辑,所以我们继续往下看。这次来看一下 XMLConfigBuilder 的parse
方法,如下:
// XMLConfigBuilder
public Configuration parse() {
// 防止parse()方法被同一个实例多次调用
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 调用XPathParser.evalNode()方法,创建表示configuration节点的XNode对象。
// 调用parseConfiguration()方法对XNode进行处理
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里大家可以大概可以看出来了,因为我们的mybatis-config.xml的根节点就是以configuration开头的,那我们继续跟下去看;
private void parseConfiguration(XNode root) {
try {
//解析 properties 配置
propertiesElement(root.evalNode("properties"));
// 解析 settings 配置,并将其转换为 Properties 对象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载 vfs
loadCustomVfs(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 中的信息设置到 Configuration 对象中
settingsElement(settings);
// 解析 environments 配置,数据源也是在这里获取的
environmentsElement(root.evalNode("environments"));
// 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象
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);
}
}
这样子大家就比较清晰了,每个配置的解析逻辑都封装在了相应的方法中。接下来我们挑下经常使用的配置解析过程的源码出来看下。
2.2 解析 properties 配置
解析properties
节点是由propertiesElement
这个方法完成的,该方法的逻辑比较简单。在分析方法源码前,先来看一下 properties 节点的配置内容。如下:
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
</properties>
在上面的配置中,我为 properties 节点配置了一个 resource 属性,以及两个子节点。下面我们参照上面的配置,来分析一下 propertiesElement 的逻辑。相关分析如下。
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
Properties defaults = context.getChildrenAsProperties();
// 获取 propertis 节点中的 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.");
}
if (resource != null) {
// 从文件系统中加载并解析属性文件
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 通过 url 加载并解析属性文件
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 将属性值设置到 configuration 中
configuration.setVariables(defaults);
}
}
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 获取并遍历子节点
for (XNode child : getChildren()) {
// 获取 property 节点的 name 和 value 属性
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
// 设置属性到属性对象中
properties.setProperty(name, value);
}
}
return properties;
}
// -☆- XNode
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<XNode>();
// 获取子节点列表
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) {
// 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
上面是 properties 节点解析的主要过程,不是很复杂。主要包含三个步骤,一是解析 properties 节点的子节点,并将解析结果设置到 Properties 对象中。二是从文件系统或通过网络读取属性配置,这取决于 properties 节点的 resource 和 url 是否为空。第二步对应的代码比较简单,这里就不分析了。有兴趣的话,大家可以自己去看看。最后一步则是将解析出的属性对象设置到 XPathParser 和 Configuration 对象中。
需要注意的是,propertiesElement 方法是先解析 properties 节点的子节点内容,后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到 defaults 属性对象中。这就会存在同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性及属性值会覆盖掉 properties 子节点中同名的属性和及值。这就是为什么我们在之前说的通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。
2.3设置 settings 配置到 Configuration 中
settings 配置的解析过程,这些配置解析出来要有一个存放的地方,以使其他代码可以找到这些配置。这个存放地方就是 Configuration 对象,本节就来看一下这将 settings 配置设置到Configuration 对象中的过程。如下:
private void settingsElement(Properties props) throws Exception {
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.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")));
@SuppressWarnings("unchecked")
Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
configuration.setDefaultEnumTypeHandler(typeHandler);
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"));
@SuppressWarnings("unchecked")
Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
2.4解析 typeAliases 配置
在 MyBatis 中,可以为我们自己写的有些类定义一个别名。这样在使用的时候,我们只需要输入别名即可,无需再把全限定的类名写出来。在 MyBatis 中,我们有两种方式进行别名配置。第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合 Alias 注解使用,即通过注解为某个类配置别名,而不是让 MyBatis 按照默认规则生成别名。这种方式的配置如下:
<typeAliases>
<package name="xyz.coolblog.model1"/>
<package name="xyz.coolblog.model2"/>
</typeAliases>
对比这两种方式,第一种自动扫描的方式配置起来比较简单,缺点也不明显。唯一能想到缺点可能就是 MyBatis 会将某个包下所有符合要求的类的别名都解析出来,并形成映射关系。如果你不想让某些类被扫描。没发现 MyBatis 提供了相关的排除机制。不过我觉得这并不是什么大问题,最多是多解析并缓存了一些别名到类型的映射,在时间和空间上产生了一些额外的消耗而已。当然,如果无法忍受这些消耗,可以使用第二种配置方式,通过手工的方式精确配置某些类型的别名。不过这种方式比较繁琐,特别是配置项比较多时。至于两种方式怎么选择,这个看具体的情况了。配置项非常少时,两种皆可。比较多的话,还是让 MyBatis 自行扫描吧。
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
// 从指定的包中解析别名和类型的映射
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else { // 从 typeAlias 节点中解析别名和类型的映射
// 获取 alias 和 type 属性值,alias 不是必填项,可为空
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// 加载 type 对应的类型
Class<?> clazz = Resources.classForName(type);
// 注册别名到类型的映射
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);
}
}
}
}
}
在别名的配置中,type
属性是必须要配置的,而alias
属性则不是必须的。这个在配置文件的 DTD 中有规定。如果使用者未配置 alias 属性,则需要 MyBatis 自行为目标类型生成别名。对于别名为空的情况,注册别名的任务交由void registerAlias(Class<?>)
方法处理。若不为空,则由void registerAlias(String, Class<?>)
进行别名注册。这两个方法的分析如下:
2.4.1从 typeAlias 节点中解析并注册别名
public void registerAlias(Class<?> type) {
// 获取全路径类名的简称
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
// 从注解中取出别名
alias = aliasAnnotation.value();
}
// 调用重载方法注册别名和类型映射
registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// // 将别名转成小写
String key = alias.toLowerCase(Locale.ENGLISH);
/*
* 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型是否一致,
* 不一致则抛出异常,不允许一个别名对应两种类型
*/
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
// 缓存别名到类型映射
TYPE_ALIASES.put(key, value);
}
如上,若用户为明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。比如,全限定类名xyz.coolblog.model.Author
的别名为author
。若类中有@Alias
注解,则从注解中取值作为别名。上面的代码不是很复杂,注释的也比较清楚了,就不多说了。继续往下看。
2.4.2从指定的包中解析并注册别名
从指定的包中解析并注册别名过程主要由别名的解析和注册两步组成。下面来看一下相关代码:
public void registerAliases(String packageName){
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
/*
* 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
* superType = Object.class,所以 ResolverUtil 将查找所有的类。
* 查找完成后,查找结果将会被缓存到内部集合中。
*/
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 获取查找结果
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
// // 忽略匿名类,接口,内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 为类型注册别名
registerAlias(type);
}
}
}
上面的代码不多,相关流程也不复杂,可简单总结为下面两个步骤:
- 查找指定包下的所有类
- 遍历查找到的类型集合,为每个类型注册别名;
2.5解析 plugins 配置
插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor
接口。然后在插件类上添加@Intercepts
和@Signature
注解,用于指定想要拦截的目标方法。MyBatis 允许拦截下面接口中的一些方法,这里先跟大家说一下具体由几个地方可以进行拦截,在后面的sql执行流程源码分析中会具体分析:
- Executor: update 方法,query 方法,flushStatements 方法,commit 方法,rollback 方法, getTransaction 方法,close 方法,isClosed 方法;
- ParameterHandler: getParameterObject 方法,setParameters 方法;
- ResultSetHandler: handleResultSets 方法,handleOutputParameters 方法;
- StatementHandler: prepare 方法,parameterize 方法,batch 方法,update 方法,query 方法
比较常见的插件有分页插件、分表插件等,有兴趣的朋友可以去了解下。本节我们来分析一下插件的配置的解析过程,先来了解插件的配置。如下:
<plugins>
<plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
<property name="key" value="value"/>
</plugin>
</plugins>
源码解析过程分析如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取<plugin>标签的interceptor属性
String interceptor = child.getStringAttribute("interceptor");
// 获取拦截器属性,转换为Properties对象
Properties properties = child.getChildrenAsProperties();
// 创建拦截器实例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// 设置拦截器实例属性信息
interceptorInstance.setProperties(properties);
// 將拦截器实例添加到拦截器链中
configuration.addInterceptor(interceptorInstance);
}
}
}
如上,插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。好了,关于插件配置的分析就先到这,继续往下分析。
2.6解析 environments 配置
在 MyBatis 中,事务管理器和数据源是配置在 environments 中的。它们的配置大致如下:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/tl-vip"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
对应的解析源码分析如下:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取 default 属性
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 获取 id 属性
String id = child.getStringAttribute("id");
/*
* 检测当前 environment 节点的 id 与其父节点 environments 的属性 default
* 内容是否一致,一致则返回 true,否则返回 false
*/
if (isSpecifiedEnvironment(id)) {
// 解析 transactionManager 节点,逻辑和插件的解析逻辑很相似,不在赘述
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 dataSource 节点,逻辑和插件的解析逻辑很相似,不在赘述
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 创建 DataSource 对象
DataSource dataSource = dsFactory.getDataSource();
// 构建 Environment 对象,并设置到 configuration 中
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
environments 配置的解析过程没什么特别之处,按部就班解析就行了,不多说了。
2.7 解析 typeHandlers 配置
在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有CHAR
和VARCHAR
等类型,但 Java 中没有这些类型,不过 Java 有String
类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成 String 。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler
去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。这里我就不演示自定义类型处理器的编写方法了,没用过或者不熟悉的同学可以 MyBatis 官方文档,或者我之前的文章有专门介绍typeHandler:Mybatis之TypeHandler使用教程_jokeMqc的博客-CSDN博客_typehandler。
下面,我们来看一下类型处理器的配置方法:
<!-- 自动扫描 -->
<typeHandlers>
<package name="xyz.coolblog.handlers"/>
</typeHandlers>
<!-- 手动配置 -->
<typeHandlers>
<typeHandler jdbcType="TINYINT"
javaType="xyz.coolblog.constant.ArticleTypeEnum"
handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>
对应的解析源码流程分析如下:
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {// 从 typeHandler 节点中解析别名到类型的映射
// 获取 javaType,jdbcType 和 handler 等属性值
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 解析上面获取到的属性值
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
// 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
2.8解析 mappers 配置
接下来我们就要去看mybatis是如何去解析对应mapper文件的源码流程;
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 通过<package>标签指定包名
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 通过resource属性指定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) {
// 通过url属性指定XML文件路径
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.");
}
}
}
}
}
上面的代码比较简单,主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过什么方式加载映射文件或映射信息。这里,我把配置在注解中的内容称为映射信息
,以 XML 为载体的配置称为映射文件
。在 MyBatis 中,共有四种加载映射文件或信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 URL 的方式加载和解析映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
// XMLMapperBuilder
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 调用XPathParser的evalNode()方法获取根节点对应的XNode对象
configurationElement(parser.evalNode("/mapper"));
// 將资源路径添加到Configuration对象中
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
// 继续解析之前解析出现异常的ResultMap对象
parsePendingResultMaps();
// 继续解析之前解析出现异常的CacheRef对象
parsePendingCacheRefs();
// 继续解析之前解析出现异常<select|update|delete|insert>标签配置
parsePendingStatements();
}
如上,映射文件解析入口逻辑包含三个核心操作,分别如下:
- 解析 mapper 节点;
- 通过命名空间绑定 Mapper 接口;
- 处理未完成解析的节点;
这三个操作对应的逻辑,我将会在随后的章节中依次进行分析。下面,先来分析第一个操作对应的逻辑。
2.8.1解析 mapper 节点
在 MyBatis 映射文件中,可以配置多种节点。比如 <cache>,<resultMap>,<sql> 以及 <select | insert | update | delete> 等。下面我们来看一个映射文件配置示例。
<mapper namespace="xyz.coolblog.dao.AuthorDao">
<cache/>
<resultMap id="authorResult" type="Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- ... -->
</resultMap>
<sql id="table">
author
</sql>
<select id="findOne" resultMap="authorResult">
SELECT
id, name, age, sex, email
FROM
<include refid="table"/>
WHERE
id = #{id}
</select>
<!-- <insert|update|delete/> -->
</mapper>
上面是一个比较简单的映射文件,还有一些的节点没有出现在上面。以上每种配置中的每种节点的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder 类的 configurationElement 方法统一调用。该方法的源码解析逻辑如下:
private void configurationElement(XNode context) {
try {
// 获取命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置当前正在解析的Mapper配置的命名空间
builderAssistant.setCurrentNamespace(namespace);
// 解析<cache-ref>标签
cacheRefElement(context.evalNode("cache-ref"));
// 解析<cache>标签
cacheElement(context.evalNode("cache"));
// 解析所有的<parameterMap>标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析所有的<resultMap>标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析所有的<sql>标签
sqlElement(context.evalNodes("/mapper/sql"));
// 解析所有的<select|insert|update|delete>标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
上面代码的执行流程清晰明了。在阅读源码时,我们可以按部就班的分析每个方法调用即可。不过在写文章进行叙述时,需要做一些调整。下面我将会先分析 <cache> 节点的解析过程,然后再分析 <cache-ref> 节点,之后会按照顺序分析其他节点的解析过程。接下来,我们来看看 <cache> 节点的解析过程。
2.8.2解析 <cache> 节点
MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。如果没有特殊要求,二级缓存的配置很容易。如下:
<cache/>
如果我们想修改缓存的一些属性,可以像下面这样配置。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
根据上面的配置创建出的缓存有以下特点:
- 按先进先出的策略淘汰缓存项;
- 缓存的容量为 512 个对象引用;
- 缓存每隔60秒刷新一次;
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象;(是否序列化);
除了上面两种配置方式,我们还可以给 MyBatis 配置第三方缓存或者自己实现的缓存等。比如,我们将 Ehcache 缓存整合到 MyBatis 中,可以这样配置。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<property name="maxEntriesLocalHeap" value="1000"/>
<property name="maxEntriesLocalDisk" value="10000000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
下面我们来分析一下缓存配置的源码解析逻辑,如下:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// 获取各种属性
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取子节点配置
Properties props = context.getChildrenAsProperties();
// 构建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
上面代码中,大段代码用来解析 <cache> 节点的属性和子节点,这些代码没什么好说的。缓存的构建逻辑封装在 BuilderAssistant 类的 useNewCache 方法中,下面我们来看一下该方法的逻辑。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 使用建造模式构建缓存实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加缓存到 Configuration 对象中
configuration.addCache(cache);
// 设置 currentCache 遍历,即当前使用的缓存
currentCache = cache;
return cache;
}
上面使用了建造模式构建 Cache 实例,Cache 实例的构建过程略为复杂,我们跟下去看看。
public Cache build() {
// 设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
setDefaultImplementations();
// 通过反射创建缓存
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 仅对内置缓存 PerpetualCache 应用装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
// 遍历装饰器集合,应用装饰器
for (Class<? extends Cache> decorator : decorators) {
// 通过反射创建装饰器实例
cache = newCacheDecoratorInstance(decorator, cache);
// 设置属性值到缓存实例中
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 应用具有日志功能的缓存装饰器
cache = new LoggingCache(cache);
}
return cache;
}
上面的构建过程流程较为复杂,这里总结一下。如下:
- 设置默认的缓存类型及装饰器;
- 应用装饰器到 PerpetualCache 对象上,①遍历装饰器类型集合,并通过反射创建装饰器实例,②将属性设置到实例中;
- 应用一些标准的装饰器;
- 对非 LoggingCache 类型的缓存应用 LoggingCache 装饰器;
在以上4个步骤中,最后一步的逻辑很简单,无需多说。下面按顺序分析前3个步骤对应的逻辑,如下:
private void setDefaultImplementations() {
if (implementation == null) {
// 设置默认的缓存实现类
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
// 添加 LruCache 装饰器
decorators.add(LruCache.class);
}
}
}
以上逻辑比较简单,主要做的事情是在 implementation 为空的情况下,为它设置一个默认值。如果大家仔细看前面的方法,会发现 MyBatis 做了不少判空的操作。比如:
// 判空操作1,若用户未设置 cache 节点的 type 和 eviction 属性,这里设置默认值 PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
String eviction = context.getStringAttribute("eviction", "LRU");
// 判空操作2,若 typeClass 或 evictionClass 为空,valueOrDefault 方法会为它们设置默认值
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
// 省略部分代码
.build();
最后,我们来看一下设置标准装饰器的过程。如下:
private Cache setStandardDecorators(Cache cache) {
try {
// 创建“元信息”对象
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
// 设置 size 属性
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// clearInterval 不为空,应用 ScheduledCache 装饰器
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// readWrite 为 true,应用 SerializedCache 装饰器
cache = new SerializedCache(cache);
}
/*
* 应用 LoggingCache,SynchronizedCache 装饰器,
* 使原缓存具备打印日志和线程同步的能力
*/
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
// blocking 为 true,应用 BlockingCache 装饰器
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
以上代码用于为缓存应用一些基本的装饰器,除了 LoggingCache 和 SynchronizedCache 这两个是必要的装饰器,其他的装饰器应用与否,取决于用户的配置。到此,关于缓存的解析过程就分析完了。这一块的内容比较多,不过好在代码逻辑不是很复杂,耐心看还是可以弄懂的。其他的就不多说了,进入下一节的分析。