上篇文章——《MyBatis是纸老虎吗?(二)》——梳理了MyBatis的执行流程,这篇文章想详细聊聊MyBatis的解析过程。当我把这个想法讲个同事时,他不可置信的说道:“这有什么好梳理的?难道你要介绍xml文件解析?”听了这话,我连忙解释道:“不是的,我就是想看一下MyBatis中的配置是如何解析出来的。”现在想想这个回答,我不禁有点疑惑:为了了解MyBatis的解析过程而单独出一篇文章,这有必要吗?如果没有必要,那出这篇文章的目的是什么?我想这些问题的答案还得从这篇文章中寻找。
通过上篇文章,我们知道new SqlSessionFactoryBuilder().build(inputStream)这样一句会加载MyBatis的配置文件config.xml并进行解析,其中文件解析的代码位于SqlSessionFactoryBuilder类的build(Configuration)中,在调用这段代码前,会先走这样一段代码,具体如下图所示:
这段代码会首先创建一个XMLConfigBuilder,注意这个对象持有InputStream(包含config.xml配置文件中的所有信息)类型的inputStream、String类型的environment以及Properties类型的properties三个参数;接着会直接调用SqlSessionFactoryBuilder中的build()方法,该方法的源码如下图所示(注意在调用该方法前会先调用XMLConfigBuilder类的parse()方法进行解析操作,该方法返回的是一个Configuration对象):
至此SqlSessionFactory对象就创建完成了。接着就可以正常执行第一篇文章示例二中SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)这句代码后的逻辑了。“等等,MyBatis的解析过程就这样结束了?那写这篇文章还有什么用?这不纯粹浪费时间嘛!”客官稍安勿躁,且让子弹飞一会!
1 创建XMLConfigBuilder对象
前面提到整个执行的关键是创建XMLConfigBuilder对象,那这个对象的创建过程究竟是怎样的?为了了解这个过程,先一起来了解一下XMLConfigBuilder类及Configuration类的继承结构,详情参见下面这幅图:
通过上面这幅图,我们可以了解到:Configuration是一个单独的类,其既未继承其他任何类,也未实现其他任何接口,同时还没有子类。而XMLConfigBuilder类继承了BaseBuilder抽象类,通过这个继承结构,XMLConfigBuilder类拥有下列属性:
- Configuration类型的configuration(定义在父类BaseBuilder中)
- TypeAliasRegistry类型的typeAliasRegistry(定义在父类BaseBuilder中)
- TypeHandlerRegistry类型的typeHandlerRegistry(定义在父类BaseBuilder中)
- boolean类型的parsed属性
- XpathParser类型的parser属性
- String类型的environment属性
- ReflectorFactory类型的localReflectorFactory属性,其值为new DefaultReflectorFactory()
前面说过在正式解析前会首先创建一个XMLConfigBuilder类型的对象,这个过程会执行以下代码:
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(Configuration.class, inputStream, environment, props);
}
/** 这个方法调用下一个构造方法时,会创建一个XpathParser对象 */
public XMLConfigBuilder(Class<? extends Configuration> configClass, InputStream inputStream, String environment,
Properties props) {
this(configClass, new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment,
Properties props) {
super(newConfig(configClass));
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
/** newConfig(Class)方法,通过反射创建一个Configuration对象 */
private static Configuration newConfig(Class<? extends Configuration> configClass) {
try {
return configClass.getDeclaredConstructor().newInstance();
} catch (Exception ex) {
throw new BuilderException("Failed to create a new Configuration instance.", ex);
}
}
/** XMLConfigBuilder 类的父类 BaseBuilder 的构造方法 */
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
通过上述代码我们可以看出最终创建出来的的XMLConfigBuilder对象中有这样一些初始信息:Configuration属性、TypeAliasRegistry属性、TypeHandlerRegistry属性、ReflectorFactory属性、XpathParser属性、parsed属性等,详细信息如下图所示:
接着代码会返回到SqlSessionFactoryBuilder的build()方法中,接着继续执行后续步骤,具体详情如下所示:
2 解析Config配置文件
上一小节我们梳理了XMLConfigBuilder对象的创建过程,本小节我们将继续梳理XMLConfigBuilder类中的parse()方法,这个方法及其周边方法的源码如下所示:
public Configuration parse() {
// 没什么可说的,如果正在解析中,则拒绝,防止重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 修改 parsed 的值,避免重复解析
parsed = true;
// 调用parseConfiguration()方法进行后续解析,比如解析properties节点、settings节点等等。在正式执行parseConfiguration()方法前,会首先执行XpathParser类中的evalNode方法
parseConfiguration(parser.evalNode("/configuration"));
// 返回Configuration对象(该对象在创建XMLConfigBuilder时就已经创建了)
return configuration;
}
// 解析 Configuration 节点
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析properties节点
propertiesElement(root.evalNode("properties"));
// 解析settings节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfsImpl(settings);
loadCustomLogImpl(settings);
// 解析typeAliases节点
typeAliasesElement(root.evalNode("typeAliases"));
// 解析plugins节点
pluginsElement(root.evalNode("plugins"));
// 解析objectFactory节点
objectFactoryElement(root.evalNode("objectFactory"));
// 解析objectWrapperFactory节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析reflectorFactory节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析environments节点
environmentsElement(root.evalNode("environments"));
// 解析databaseIdProvider节点
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析typeHandlers节点
typeHandlersElement(root.evalNode("typeHandlers"));
// 解析mappers节点
mappersElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
通过上述代码不难发现这段代码的执行逻辑是:解析xml文件,并将xml文件包含的信息转化为java对象,同时这个过程不能重复执行。这里不会对parser.evalNode("/configuration")这句代码的执行过程进行详细梳理,因为个人认为这是xml文件解析的知识(在spring中也存在这个步骤),如果继续深入,就偏离本篇文章的主旨了,所以还是让我们继续梳理与MyBatis有关的知识吧!从这段代码,个人认为MyBatis配置文件中可以配置的节点有:
- properties:属性标签。提供属性配置,方便文件中动态使用。通常都是在Java属性文件中配置数据,例如jdbc.properties配置文件 ,或通过properties元素中的子元素property来指定配置数据,然后在MyBatis的配置文件中使用。
- settings:设置。用于动态改变MyBatis的运行时行为。通过它可以配置缓存、延迟加载等属性。
- typeAliases:类型别名。可以为系统定义的Java Bean指定一个简单容易记忆的别名,在MyBatis中别名分为两大类:系统内置别名和自定义别名。
- plugins:插件。
- objectFactory:对象工厂
- objectWrapperFactory
- reflectorFactory
- environments:环境配置(用于配置MyBatis环境)。其中包含environment节点,该节点一般用于指定事务管理器、数据源等
- databaseIdProvider:数据库厂商标识
- typeHandlers:类型处理器
- mappers:映射器。该节点下可以多个mapper子节点。这个节点的主要作用便是告知MyBatis去哪个地方加载需要的sql映射语句。这些sql语句均由开发者自己编写
下面这段代码是MyBatis配置文件config.xml的配置示例,其中列出的是一些非常常用的配置项(关于这个配置文件有几点需要注意:1.configuration是顶级节点;2.配置文件中属性位置顺序是固定的,不允许颠倒顺序【上面所列的并不是MyBatis规定的顺序,具体顺序请参见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>
<!-- 1、属性:例如jdbc.properties -->
<properties resource="jdbc.properties"></properties>
<!-- 2、设置:定义全局性设置,例如开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 3、类型名称:为一些类定义别名 -->
<typeAliases>
<typeAlias type="com.panshenlian.pojo.User" alias="user"></typeAlias>
</typeAliases>
<!-- 4、类型处理器:定义Java类型与数据库中的数据类型之间的转换关系 -->
<typeHandlers></typeHandlers>
<!-- 5、对象工厂 -->
<objectFactory type=""></objectFactory>
<!-- 6、插件:mybatis的插件,支持自定义插件 -->
<plugins>
<plugin interceptor=""></plugin>
</plugins>
<!-- 7、环境:配置mybatis的环境 -->
<environments default="development">
<!-- 环境变量:支持多套环境变量,例如开发环境、生产环境 -->
<environment id="development">
<!-- 事务管理器:默认JDBC -->
<transactionManager type="JDBC" />
<!-- 数据源:使用连接池,并加载mysql驱动连接数据库 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
<!-- 8、数据库厂商标识 -->
<databaseIdProvider type=""></databaseIdProvider>
<!-- 9、映射器:指定映射文件或者映射类 -->
<mappers>
<mapper resource="UserMapper.xml" />
</mappers>
</configuration>
看完这个配置示例后,还是让我们一起看一下这些常用配置的具体含义和详细用法吧!因为习惯做南郭先生,对后面的工作极为不利!
2.1 properties
这是一个属性标签。提供属性配置,方便文件中动态使用。通常都是在Java属性文件中配置数据,例如jdbc.properties配置文件 ,或通过properties元素中的子元素property来指定配置数据,然后在MyBatis的配置文件中使用。通常是这样的:
- 在property标签配置,然后在datasource中引用,具体为:
<properties>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/myDB"/>
<property name="username" value="user1"/>
<property name="password" value="123456"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
- 在properties文件中配置,然后在datasource中引用,具体为:
driver=com.mysql.jdbc.Driver
url=jdbc\:mysql\://127.0.0.1\:3306/myDB
username=root
password=123456
<!-- 引入属性配置文件 -->
<properties resource="jdbc.properties"></properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
2.2 settings
settings 标签,是MyBatis中极为重要的调整设置,它们会动态改变MyBatis的运行时行为,这些配置就像Mybatis内置的许多功能,当你需要使用时可以根据需要灵活调整,并且settings能配置的东西特别多,下面一起来一个相对完整的属性配置示例:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
- 属性 cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存 支持 true | false 默认 true
- 属性 lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 支持 true | false 默认 false
- 属性 aggressiveLazyLoading 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。 支持 true | false 默认 false (在 3.4.1 及之前的版本中默认为 true)
- 属性 multipleResultSetsEnabled 是否允许单个语句返回多结果集(需要数据库驱动支持)。 支持 true | false 默认 true
- 属性 useColumnLabel 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 支持 true | false 默认 true
- 属性 useGeneratedKeys 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 支持 true | false 默认 false
- 属性 autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 支持 NONE, PARTIAL, FULL 默认 PARTIAL
- 属性 autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。 NONE: 不做任何反应 WARNING: 输出警告日志( org.apache.ibatis.session.AutoMappingUnknownColumnBehavior 的日志等级必须设置为 WARN) FAILING: 映射失败 (抛出 SqlSessionException) 支持 NONE, WARNING, FAILING 默认 NONE
- 属性 defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 支持 SIMPLE REUSE BATCH 默认 SIMPLE
- 属性 defaultStatementTimeout 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 支持 任意正整数 默认 未设置 (null)
- 属性 defaultFetchSize 动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 支持 任意正整数 默认 未设置 (null)
- 属性 defaultResultSetType 指定语句默认的滚动策略。(新增于 3.5.2) 支持 FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) 默认 未设置 (null)
- 属性 safeRowBoundsEnabled 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 支持 true | false 默认 false
- 属性 safeResultHandlerEnabled 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 支持 true | false 默认 true
- 属性 mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 支持 true | false 默认 false
- 属性 localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 支持 SESSION | STATEMENT 默认 SESSION
- 属性 jdbcTypeForNull 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 默认 OTHER
- 属性 lazyLoadTriggerMethods 指定对象的哪些方法触发一次延迟加载。 支持 用逗号分隔的方法列表。 默认 equals,clone,hashCode,toString
- 属性 defaultScriptingLanguage 指定动态 SQL 生成使用的默认脚本语言。 支持 一个类型别名或全限定类名。 默认 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
- 属性 defaultEnumTypeHandler 指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5) 支持 一个类型别名或全限定类名。 默认 org.apache.ibatis.type.EnumTypeHandler
- 属性 callSettersOnNulls 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 支持 true | false 默认 false
- 属性 returnInstanceForEmptyRow 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) 支持 true | false 默认 false
- 属性 logPrefix 指定 MyBatis 增加到日志名称的前缀。 支持 任何字符串 默认 未设置
- 属性 logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 支持 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 默认 未设置
- 属性 proxyFactory 指定 MyBatis 创建可延迟加载对象所用到的代理工具。 支持 CGLIB | JAVASSIST 默认 JAVASSIST (MyBatis 3.3 以上)
- 属性 vfsImpl 指定 VFS 的实现 支持 自定义 VFS 的实现的类全限定名,以逗号分隔。 默认 未设置
- 属性 useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) 支持 true | false 默认 true
- 属性 configurationFactory 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) 支持 一个类型别名或完全限定类名。 默认 未设置
- 属性 shrinkWhitespacesInSql 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) 支持 true | false 默认 false
- 属性 defaultSqlProviderType 指定一个本身拥查询方法的类( 从 3.5.6 开始 ),这个类可以配置在注解 @SelectProvider 的 type 属性值上。 支持 一个类型别名或完全限定类名。 默认 未设置
2.3 typeAliases
类型别名可以给Java类型设置一个简称。它仅用于XML配置,意在降低冗余的全限定类名书写,因为书写类的全限定名太长了,我们希望有一个简称来指代它。类型别名在Mybatis中分为系统内置和用户自定义两类,Mybatis会在解析配置文件时把typeAliases实例存储进入Configuration对象中,需要使用时直接获取。一般可以按照下面方式自定义别名:
<typeAliases>
<typeAlias alias="user" type="org.com.chinasofti.springtransaction.User"/>
<typeAlias alias="userDto" type="org.com.chinasofti.springtransaction.UserDto"/>
</typeAliases>
这样后面可以在任何需要使用org.com.chinasofti.springtransaction.User的地方,直接使用别名 user。有时我们会遇到项目中有特别多的java类需要配置别名,这时又该咋设置呢?可以指定一个包名进行扫描,MyBatis会在包名下面扫描需要的Java Bean。在没注解的情况下,会使用Bean的首字母小写的非限定类名来作为它的别名;在有注解的情况下,则别名为其自定义的注解值。MyBatis已经为许多常见的Java类型内建了相应的类型别名。下面就是一些为常见的Java类型内建的类型别名。它们都是不区分大小写的,注意:为了应对原始类型的命名重复,采取了特殊的命名风格,可以发现基本类型的别名前缀都有下划线“_”,而基本类型的包装类则没有。(可以通过源码查看内置的类型别名的注册信息,这个源码的具体路径为:org.apache.ibatis.type.TypeAliasRegistry#TypeAliasRegistry())具体为:
序号 | 别名 | 实际类型 |
1 | _byte | byte |
2 | _long | long |
3 | _short | short |
4 | _int | int |
5 | _integer | int |
6 | _double | double |
7 | _float | float |
8 | _boolean | boolean |
9 | string | String |
10 | byte | Byte |
11 | long | Long |
12 | short | Short |
13 | int | Integer |
14 | integer | Integer |
15 | double | Double |
16 | float | Float |
17 | boolean | Boolean |
18 | date | Date |
19 | decimal | BigDecimal |
20 | bigdecimal | BigDecimal |
21 | object | Object |
22 | map | Map |
23 | hashmap | HashMap |
24 | list | List |
25 | arraylist | ArrayList |
26 | collection | Collection |
27 | iterator | Iterator |
关于上述别名还有几点需要特别注意:别名不区分大小写;同时也支持数组类型,只需要在别名后加中括号([])即可,比如Long数组别名我们可以用long[]直接代替;另外在实际开发中,int、INT、integer、INTEGER都是代表Integer,这里主要由于MyBatis在注册别名的时候会全部转为小写字母进行存储。
2.4 plugins
MyBatis允许我们在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
插件功能主要开放拦截的对象就是以上列举的Mybatis四大组件,后续我们梳理Mybatis核心API的时候或者单独介绍自定义插件的时候会详细说明,这里先大致了解下,包括数据分页、操作日志增强、sql性能监控等都可以通过插件实现,不过会存储改造的风险,毕竟这些都是核心的API。在MyBatis中使用插件是非常简单的事情,只需实现框架提供的Interceptor接口即可,并指定想要拦截的类、方法、参数(由于有多态的情况)即可。下面看一个自定义的拦截器及其用法:
@Intercepts({
@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<!-- 注意下面这个配置要加在 mybatis 中的config.xml 配置文件中,一定要放置在configuration 节点下 -->
<plugins>
<plugin interceptor="包名.自定义拦截器名(比如:ExamplePlugin)">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
注意:上面的插件将会拦截在Executor实例中所有的update方法调用,这里的Executor是负责执行底层映射语句的内部对象。
2.5 environments
MyBatis可以适应多种环境,这种机制有助于将SQL映射应用于多种数据库之中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同Schema的多个生产数据库中使用相同的SQL映射。还有许多类似的使用场景。不过有一点一定要记住:尽管可以配置多个环境,但每个SqlSessionFactory实例只能选择一种环境。因此,如果想在项目中连接两个数据库,就需要创建两个SqlSessionFactory实例,每个数据库对应一个。而如果想连接三个数据库,就需要三个SqlSessionFactory实例,后面依次类推即可(每个数据库对应一个 SqlSessionFactory 实例)。为了指定创建哪种环境,只要将它作为可选的参数传递给SqlSessionFactoryBuilder即可。可以接受环境配置的两个方法分别是:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
如果忽略了环境参数,那么将会加载默认环境,如下所示:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);
environments元素定义了如何配置环境,具体如下所示:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
这里有几点需要注意:1.每个environment子节点都要有一个id属性,id属性值可以随意命名,但这些属性值必须是唯一的;2.要注意事务管理器和数据源的配置,即JDBC和POOLED
2.6 databaseIdProvider
MyBatis可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的databaseId属性实现的。MyBatis会加载带有匹配当前数据库databaseId属性和所有不带databaseId属性的语句。如果同时找到带有databaseId和不带databaseId的相同语句,则后者会被舍弃。为支持多厂商特性,只要像下面这样在config.xml文件中加入databaseIdProvider配置节点即可:
<databaseIdProvider type="DB_VENDOR" />
databaseIdProvider对应的DB_VENDOR实现会将databaseId设置为DatabaseMetaData中getDatabaseProductName()方法返回的字符串。由于通常情况下这些字符串都非常长,而且相同产品的不同版本会返回不同的值,不过这里可以通过设置属性别名来使其变短:
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
在提供了属性别名时,databaseIdProvider的DB_VENDOR实现会将databaseId设置为数据库产品名与属性中的名称第一个相匹配的值,如果没有匹配的属性,将会设置为null。在这个例子中,如果getDatabaseProductName()返回Oracle (DataDirect),databaseId将被设置为oracle。当然我们也可以通过实现接口org.apache.ibatis.mapping.DatabaseIdProvider来构建自己的DatabaseIdProvider并在mybatis-config.xml中进行注册:
public interface DatabaseIdProvider {
default void setProperties(Properties p) { // 从 3.5.2 开始,该方法为默认方法
// 空实现
}
String getDatabaseId(DataSource dataSource) throws SQLException;
}
2.7 typeHandlers
2.8 objectFactory
每次MyBatis创建结果对象的新实例时,都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:
class ExampleObjectFactory extends DefaultObjectFactory {
public Object create(Class type) {
return super.create(type);
}
public Object create(Class type, List constructorArgTypes, List constructorArgs) {
return super.create(type, constructorArgTypes, constructorArgs);
}
public void setProperties(Properties properties) {
super.setProperties(properties);
}
public boolean isCollection(Class type) {
return Collection.class.isAssignableFrom(type);
}
}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
ObjectFactory接口很简单,它包含两个创建用的方法,一个是处理默认构造方法的,另外一个是处理带参数的构造方法的。最后setProperties ()方法可以被用来配置ObjectFactory,在初始化自己的ObjectFactory实例后,objectFactory元素体中定义的属性会被传递给setProperties()方法。
注意:正常情况下我们不会用到,或者说不建议使用,除非业务上确实需要对一个特殊实体初始构造做一个默认属性值配置等处理,其余情况一律不推荐使用,避免产生不可控风险。
2.9 mappers
这个节点的作用是告诉MyBatis去哪个地方查找我们定义的sql映射语句。注意:这个节点下可以有多个mapper子节点,这些子节点用来指定sql映射文件的路径,这些映射文件均由开发者自己编写,这也是大多数人将MyBatis成为半自动化ORM工具的原因。其具体用法如下所示:
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
上面我们对MyBatis中各个配置项的作用进行了详细梳理。对了,这里我不得不感谢一下知乎大佬——敲代码的小叔叔,因为上面几个小节介绍的几个属性的详细信息均摘自他撰写的博客《Mybatis配置文件XML全貌详解,再不懂我也没招了》。好了,我们还是继续梳理MyBatis配置文件的解析流程吧!前面,我们梳理到了XMLConfigBuilder#parseConfiguration()方法,同时我们将其源码摘录出来,并添加了相关注释。下面我们将对这个方法中的常见节点的处理逻辑进行梳理。首先来看一下Configuration节点中properties节点的解析,这个解析是通过parseConfiguration(XNode)方法完成的,其源码如下所示:
private void propertiesElement(XNode context) throws Exception {
if (context == null) {
return;
}
Properties defaults = context.getChildrenAsProperties();
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) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
通过代码可以发现,这个方法的处理逻辑非常简单:解析有效数据XNode中的子元素,并将其处理成Properties对象(这里的有效数据是MyBatis配置文件config.xml中的properties节点,子元素就是property节点)。接着读取有效数据XNode上的resource及url属性,并对这些属性进行判断,这里一定要注意如果这两个属性值同时存在,MyBatis是会拒绝的;紧接着如果程序判定resource属性或url属性不为空,则程序会读取resource或url属性指定的配置资源,并将读取的结果存放到Properties对象中(这个对象是在执行Properties defaults = context.getChildrenAsProperties()这句代码时创建的)。再次会从Configuration对象中获取variables对象(其类型为Properties),如果其不为空,则一并放入defaults对象中,否则不会做任何操作。最后将defaults对象保存到XpathParser及Configuration对象中。从这段处理逻辑中,我们可以知道MyBatis配置文件config.xml中的properties节点中的数据会被保存到Configuration对象的variables属性中。这段逻辑执行前后的对比结果请参见下图:
注意:执行上述代码前,在config.xml配置文件的configuration节点中新增了下面这几行代码:
<properties>
<property name="id" value="aaaa"/>
<property name="name" value="bbbb"/>
</properties>
接着再来看一下Configuration节点中settings节点的解析,这个解析是通过settingsAsProperties (XNode)方法完成的,其源码如下所示:
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
// 校验配置文件settings节点中指定的配置项是否被Configuration对象接受
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
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;
}
与properties节点的解析过程一样,这个解析逻辑也非常清晰:若未配置settings节点,就这直接返回一个Properties对象;若配置了settings节点,就直接将其读取成一个Properties对象。接着执行MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory)这行代码,创建一个MetaClass对象,然后遍历Properties属性集,并调用MetaClass对象的hasSetter()方法进行处理(这个方法将接收的字符串包装为PropertyTokenizer对象,然后判断其是否有下一个元素,如果没有就直接调用MetaClass对象上的Reflector类型的属性上的hasSetter()方法,这个方法主要是调用setMethods对象上的containsKey()方法进行判断,注意setMethods对象是一个Map<String, Invoker>类型的对象,其值会在程序启动时完成数据的初始化,该对象的结构图如下所示),这个处理过程的本质就是校验,校验settings节点中指定的配置项是否符合Configuration的要求。如果校验通过,则直接返回Properties对象给上级调用者。
图中展示的55个数据,包含了setting小节中列出的所有项目(这个在梳理时做过比对)。所以前面括号中执行步骤的主要目的判断setting元素中指定的配置项在setMethods中是否存在,如果不存在就抛出异常,这个逻辑见于settingsAsProperties()方法。注意:括号中的执行逻辑涉及到了几个重要的数据——MetaClass、Reflector、ReflectorFactory。
紧接着程序会执行loadCustomVfsImpl(settings)和loadCustomLogImpl(settings)这两行代码。执行它们的主要目的就是变更Configuration对象中的vfsImpl及logImpl属性。
再接着我们一起看一下Configuration节点中typeAliases节点的解析,这个解析是通过typeAliasesElement (XNode)方法完成的,其源码如下所示:
private void typeAliasesElement(XNode context) {
if (context == null) {
return;
}
for (XNode child : context.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
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);
}
}
}
}
通过上述代码可以知道在config.xml中配置别名的方式有两种:通过package节点指定,通过typeAlias节点指定。仔细阅读这段源码,我们会发现其的处理逻辑与前面介绍的两个节点的逻辑一样,非常清晰:判断XNode是否为空,如果为空,则不做任何操作。否则遍历XNode中的子元素,如果子元素是package,则调用Configuration对象上的getTypeAliasRegistry()方法获取TypeAliasRegistry对象,然后调用该对象上的registerAliases()方法,这个方法最终会加载该包下的所有JavaBean并将其注册到Configuration对象的typeAliasRegistry属性中。如果子元素是typeAlias,则直接解析该节点上的alias和type属性,其中前者表示别名,后者表示java类。接着判断alias是否为空,如果为空则直接调用typeAliasRegistry(这个对象指向了Configuration对象上的typeAliasRegistry属性)对象上的registerAlias(class)方法进行注册(注意这个方法会获取class对象的类名,即getSimpleName()返回的类名)。如果不为空则直接调用typeAliasRegistry(这个对象指向了Configuration对象上的typeAliasRegistry属性)对象上的registerAlias(string, class)方法进行注册。
解析typeAliases节点这里有几点需要注意:
- 如果用package或未添加alias属性的typeAlias指定别名,最终都会走registerAlias(Class<?>)方法,这个方法会首先调用getSimpleName()方法获取简单类名,接着判断这个类上是否有Alias注解,如果有注解则直接取注解中的值作为别名,最终调用TypeAliasRegistry类中的registerAlias(String, Class<?>)方法完成别名的注册
- 在梳理过程中我们看到了TypeAliasRegistry类,在创建这个类对象时,其中的构造方法会像registerAlias属性中注入基本数据类型的别名,比如String的别名是string(这部分可以参考typeAlias小节)
- 最终存储别名的是TypeAliasRegistry类中类型为Map<String, Class<?>>的typeAliases属性。Configuration类中定义有TypeAliasRegistry对象,所以Configuration对象也变相的持有了类型为Map<String, Class<?>>的typeAliases属性(注意:Configuration的构造方法中也会向TypeAliasRegistry对象中注入一些别名,这个可以看一下Configuration类的源码)
- typeAliases中存储的key均为小写形式
下面看一下TypeAliasRegistry类的源码如下所示:
public class Configuration {
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
protected boolean shrinkWhitespacesInSql;
protected boolean nullableOnForEach;
protected boolean argNameBasedConstructorAutoMapping;
protected String logPrefix;
protected Class<? extends Log> logImpl;
protected Class<? extends VFS> vfsImpl;
protected Class<?> defaultSqlProviderType;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(
Arrays.asList("equals", "clone", "hashCode", "toString"));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ResultSetType defaultResultSetType;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
protected boolean lazyLoadingEnabled;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
/**
* Configuration factory class. Used to create Configuration for loading deserialized unread properties.
*
* @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
"Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and "
+ targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<>();
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
/*
* A map holds cache-ref relationship. The key is the namespace that references a cache bound to another namespace and
* the value is the namespace which the actual cache is bound to.
*/
protected final Map<String, String> cacheRefMap = new HashMap<>();
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);
}
public String getLogPrefix() {
return logPrefix;
}
public void setLogPrefix(String logPrefix) {
this.logPrefix = logPrefix;
}
public Class<? extends Log> getLogImpl() {
return logImpl;
}
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
LogFactory.useCustomLogging(this.logImpl);
}
}
public Class<? extends VFS> getVfsImpl() {
return this.vfsImpl;
}
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
VFS.addImplClass(this.vfsImpl);
}
}
/**
* Gets an applying type when omit a type on sql provider annotation(e.g.
* {@link org.apache.ibatis.annotations.SelectProvider}).
*
* @return the default type for sql provider annotation
*
* @since 3.5.6
*/
public Class<?> getDefaultSqlProviderType() {
return defaultSqlProviderType;
}
/**
* Sets an applying type when omit a type on sql provider annotation(e.g.
* {@link org.apache.ibatis.annotations.SelectProvider}).
*
* @param defaultSqlProviderType
* the default type for sql provider annotation
*
* @since 3.5.6
*/
public void setDefaultSqlProviderType(Class<?> defaultSqlProviderType) {
this.defaultSqlProviderType = defaultSqlProviderType;
}
public boolean isCallSettersOnNulls() {
return callSettersOnNulls;
}
public void setCallSettersOnNulls(boolean callSettersOnNulls) {
this.callSettersOnNulls = callSettersOnNulls;
}
public boolean isUseActualParamName() {
return useActualParamName;
}
public void setUseActualParamName(boolean useActualParamName) {
this.useActualParamName = useActualParamName;
}
public boolean isReturnInstanceForEmptyRow() {
return returnInstanceForEmptyRow;
}
public void setReturnInstanceForEmptyRow(boolean returnEmptyInstance) {
this.returnInstanceForEmptyRow = returnEmptyInstance;
}
public boolean isShrinkWhitespacesInSql() {
return shrinkWhitespacesInSql;
}
public void setShrinkWhitespacesInSql(boolean shrinkWhitespacesInSql) {
this.shrinkWhitespacesInSql = shrinkWhitespacesInSql;
}
/**
* Sets the default value of 'nullable' attribute on 'foreach' tag.
*
* @param nullableOnForEach
* If nullable, set to {@code true}
*
* @since 3.5.9
*/
public void setNullableOnForEach(boolean nullableOnForEach) {
this.nullableOnForEach = nullableOnForEach;
}
/**
* Returns the default value of 'nullable' attribute on 'foreach' tag.
* <p>
* Default is {@code false}.
*
* @return If nullable, set to {@code true}
*
* @since 3.5.9
*/
public boolean isNullableOnForEach() {
return nullableOnForEach;
}
public boolean isArgNameBasedConstructorAutoMapping() {
return argNameBasedConstructorAutoMapping;
}
public void setArgNameBasedConstructorAutoMapping(boolean argNameBasedConstructorAutoMapping) {
this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
}
public String getDatabaseId() {
return databaseId;
}
public void setDatabaseId(String databaseId) {
this.databaseId = databaseId;
}
public Class<?> getConfigurationFactory() {
return configurationFactory;
}
public void setConfigurationFactory(Class<?> configurationFactory) {
this.configurationFactory = configurationFactory;
}
public boolean isSafeResultHandlerEnabled() {
return safeResultHandlerEnabled;
}
public void setSafeResultHandlerEnabled(boolean safeResultHandlerEnabled) {
this.safeResultHandlerEnabled = safeResultHandlerEnabled;
}
public boolean isSafeRowBoundsEnabled() {
return safeRowBoundsEnabled;
}
public void setSafeRowBoundsEnabled(boolean safeRowBoundsEnabled) {
this.safeRowBoundsEnabled = safeRowBoundsEnabled;
}
public boolean isMapUnderscoreToCamelCase() {
return mapUnderscoreToCamelCase;
}
public void setMapUnderscoreToCamelCase(boolean mapUnderscoreToCamelCase) {
this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;
}
public void addLoadedResource(String resource) {
loadedResources.add(resource);
}
public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}
public Environment getEnvironment() {
return environment;
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
public AutoMappingBehavior getAutoMappingBehavior() {
return autoMappingBehavior;
}
public void setAutoMappingBehavior(AutoMappingBehavior autoMappingBehavior) {
this.autoMappingBehavior = autoMappingBehavior;
}
/**
* Gets the auto mapping unknown column behavior.
*
* @return the auto mapping unknown column behavior
*
* @since 3.4.0
*/
public AutoMappingUnknownColumnBehavior getAutoMappingUnknownColumnBehavior() {
return autoMappingUnknownColumnBehavior;
}
/**
* Sets the auto mapping unknown column behavior.
*
* @param autoMappingUnknownColumnBehavior
* the new auto mapping unknown column behavior
*
* @since 3.4.0
*/
public void setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior) {
this.autoMappingUnknownColumnBehavior = autoMappingUnknownColumnBehavior;
}
public boolean isLazyLoadingEnabled() {
return lazyLoadingEnabled;
}
public void setLazyLoadingEnabled(boolean lazyLoadingEnabled) {
this.lazyLoadingEnabled = lazyLoadingEnabled;
}
public ProxyFactory getProxyFactory() {
return proxyFactory;
}
public void setProxyFactory(ProxyFactory proxyFactory) {
if (proxyFactory == null) {
proxyFactory = new JavassistProxyFactory();
}
this.proxyFactory = proxyFactory;
}
public boolean isAggressiveLazyLoading() {
return aggressiveLazyLoading;
}
public void setAggressiveLazyLoading(boolean aggressiveLazyLoading) {
this.aggressiveLazyLoading = aggressiveLazyLoading;
}
public boolean isMultipleResultSetsEnabled() {
return multipleResultSetsEnabled;
}
public void setMultipleResultSetsEnabled(boolean multipleResultSetsEnabled) {
this.multipleResultSetsEnabled = multipleResultSetsEnabled;
}
public Set<String> getLazyLoadTriggerMethods() {
return lazyLoadTriggerMethods;
}
public void setLazyLoadTriggerMethods(Set<String> lazyLoadTriggerMethods) {
this.lazyLoadTriggerMethods = lazyLoadTriggerMethods;
}
public boolean isUseGeneratedKeys() {
return useGeneratedKeys;
}
public void setUseGeneratedKeys(boolean useGeneratedKeys) {
this.useGeneratedKeys = useGeneratedKeys;
}
public ExecutorType getDefaultExecutorType() {
return defaultExecutorType;
}
public void setDefaultExecutorType(ExecutorType defaultExecutorType) {
this.defaultExecutorType = defaultExecutorType;
}
public boolean isCacheEnabled() {
return cacheEnabled;
}
public void setCacheEnabled(boolean cacheEnabled) {
this.cacheEnabled = cacheEnabled;
}
public Integer getDefaultStatementTimeout() {
return defaultStatementTimeout;
}
public void setDefaultStatementTimeout(Integer defaultStatementTimeout) {
this.defaultStatementTimeout = defaultStatementTimeout;
}
/**
* Gets the default fetch size.
*
* @return the default fetch size
*
* @since 3.3.0
*/
public Integer getDefaultFetchSize() {
return defaultFetchSize;
}
/**
* Sets the default fetch size.
*
* @param defaultFetchSize
* the new default fetch size
*
* @since 3.3.0
*/
public void setDefaultFetchSize(Integer defaultFetchSize) {
this.defaultFetchSize = defaultFetchSize;
}
/**
* Gets the default result set type.
*
* @return the default result set type
*
* @since 3.5.2
*/
public ResultSetType getDefaultResultSetType() {
return defaultResultSetType;
}
/**
* Sets the default result set type.
*
* @param defaultResultSetType
* the new default result set type
*
* @since 3.5.2
*/
public void setDefaultResultSetType(ResultSetType defaultResultSetType) {
this.defaultResultSetType = defaultResultSetType;
}
public boolean isUseColumnLabel() {
return useColumnLabel;
}
public void setUseColumnLabel(boolean useColumnLabel) {
this.useColumnLabel = useColumnLabel;
}
public LocalCacheScope getLocalCacheScope() {
return localCacheScope;
}
public void setLocalCacheScope(LocalCacheScope localCacheScope) {
this.localCacheScope = localCacheScope;
}
public JdbcType getJdbcTypeForNull() {
return jdbcTypeForNull;
}
public void setJdbcTypeForNull(JdbcType jdbcTypeForNull) {
this.jdbcTypeForNull = jdbcTypeForNull;
}
public Properties getVariables() {
return variables;
}
public void setVariables(Properties variables) {
this.variables = variables;
}
public TypeHandlerRegistry getTypeHandlerRegistry() {
return typeHandlerRegistry;
}
/**
* Set a default {@link TypeHandler} class for {@link Enum}. A default {@link TypeHandler} is
* {@link org.apache.ibatis.type.EnumTypeHandler}.
*
* @param typeHandler
* a type handler class for {@link Enum}
*
* @since 3.4.5
*/
public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) {
if (typeHandler != null) {
getTypeHandlerRegistry().setDefaultEnumTypeHandler(typeHandler);
}
}
public TypeAliasRegistry getTypeAliasRegistry() {
return typeAliasRegistry;
}
/**
* Gets the mapper registry.
*
* @return the mapper registry
*
* @since 3.2.2
*/
public MapperRegistry getMapperRegistry() {
return mapperRegistry;
}
public ReflectorFactory getReflectorFactory() {
return reflectorFactory;
}
public void setReflectorFactory(ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
}
public ObjectFactory getObjectFactory() {
return objectFactory;
}
public void setObjectFactory(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
}
public ObjectWrapperFactory getObjectWrapperFactory() {
return objectWrapperFactory;
}
public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) {
this.objectWrapperFactory = objectWrapperFactory;
}
/**
* Gets the interceptors.
*
* @return the interceptors
*
* @since 3.2.2
*/
public List<Interceptor> getInterceptors() {
return interceptorChain.getInterceptors();
}
public LanguageDriverRegistry getLanguageRegistry() {
return languageRegistry;
}
public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
if (driver == null) {
driver = XMLLanguageDriver.class;
}
getLanguageRegistry().setDefaultDriverClass(driver);
}
public LanguageDriver getDefaultScriptingLanguageInstance() {
return languageRegistry.getDefaultDriver();
}
/**
* Gets the language driver.
*
* @param langClass
* the lang class
*
* @return the language driver
*
* @since 3.5.1
*/
public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
if (langClass == null) {
return languageRegistry.getDefaultDriver();
}
languageRegistry.register(langClass);
return languageRegistry.getDriver(langClass);
}
/**
* Gets the default scripting language instance.
*
* @return the default scripting language instance
*
* @deprecated Use {@link #getDefaultScriptingLanguageInstance()}
*/
@Deprecated
public LanguageDriver getDefaultScriptingLanuageInstance() {
return getDefaultScriptingLanguageInstance();
}
public MetaObject newMetaObject(Object object) {
return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
parameterObject, boundSql);
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
resultHandler, boundSql, rowBounds);
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
rowBounds, resultHandler, boundSql);
return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
return (Executor) interceptorChain.pluginAll(executor);
}
public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
keyGenerators.put(id, keyGenerator);
}
public Collection<String> getKeyGeneratorNames() {
return keyGenerators.keySet();
}
public Collection<KeyGenerator> getKeyGenerators() {
return keyGenerators.values();
}
public KeyGenerator getKeyGenerator(String id) {
return keyGenerators.get(id);
}
public boolean hasKeyGenerator(String id) {
return keyGenerators.containsKey(id);
}
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
public Collection<String> getCacheNames() {
return caches.keySet();
}
public Collection<Cache> getCaches() {
return caches.values();
}
public Cache getCache(String id) {
return caches.get(id);
}
public boolean hasCache(String id) {
return caches.containsKey(id);
}
public void addResultMap(ResultMap rm) {
resultMaps.put(rm.getId(), rm);
checkLocallyForDiscriminatedNestedResultMaps(rm);
checkGloballyForDiscriminatedNestedResultMaps(rm);
}
public Collection<String> getResultMapNames() {
return resultMaps.keySet();
}
public Collection<ResultMap> getResultMaps() {
return resultMaps.values();
}
public ResultMap getResultMap(String id) {
return resultMaps.get(id);
}
public boolean hasResultMap(String id) {
return resultMaps.containsKey(id);
}
public void addParameterMap(ParameterMap pm) {
parameterMaps.put(pm.getId(), pm);
}
public Collection<String> getParameterMapNames() {
return parameterMaps.keySet();
}
public Collection<ParameterMap> getParameterMaps() {
return parameterMaps.values();
}
public ParameterMap getParameterMap(String id) {
return parameterMaps.get(id);
}
public boolean hasParameterMap(String id) {
return parameterMaps.containsKey(id);
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
public Collection<String> getMappedStatementNames() {
buildAllStatements();
return mappedStatements.keySet();
}
public Collection<MappedStatement> getMappedStatements() {
buildAllStatements();
return mappedStatements.values();
}
public Collection<XMLStatementBuilder> getIncompleteStatements() {
return incompleteStatements;
}
public void addIncompleteStatement(XMLStatementBuilder incompleteStatement) {
incompleteStatements.add(incompleteStatement);
}
public Collection<CacheRefResolver> getIncompleteCacheRefs() {
return incompleteCacheRefs;
}
public void addIncompleteCacheRef(CacheRefResolver incompleteCacheRef) {
incompleteCacheRefs.add(incompleteCacheRef);
}
public Collection<ResultMapResolver> getIncompleteResultMaps() {
return incompleteResultMaps;
}
public void addIncompleteResultMap(ResultMapResolver resultMapResolver) {
incompleteResultMaps.add(resultMapResolver);
}
public void addIncompleteMethod(MethodResolver builder) {
incompleteMethods.add(builder);
}
public Collection<MethodResolver> getIncompleteMethods() {
return incompleteMethods;
}
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.get(id);
}
public Map<String, XNode> getSqlFragments() {
return sqlFragments;
}
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
public void addMappers(String packageName, Class<?> superType) {
mapperRegistry.addMappers(packageName, superType);
}
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public boolean hasMapper(Class<?> type) {
return mapperRegistry.hasMapper(type);
}
public boolean hasStatement(String statementName) {
return hasStatement(statementName, true);
}
public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.containsKey(statementName);
}
public void addCacheRef(String namespace, String referencedNamespace) {
cacheRefMap.put(namespace, referencedNamespace);
}
/*
* Parses all the unprocessed statement nodes in the cache. It is recommended to call this method once all the mappers
* are added as it provides fail-fast statement validation.
*/
protected void buildAllStatements() {
parsePendingResultMaps();
if (!incompleteCacheRefs.isEmpty()) {
synchronized (incompleteCacheRefs) {
incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
}
}
if (!incompleteStatements.isEmpty()) {
synchronized (incompleteStatements) {
incompleteStatements.removeIf(x -> {
x.parseStatementNode();
return true;
});
}
}
if (!incompleteMethods.isEmpty()) {
synchronized (incompleteMethods) {
incompleteMethods.removeIf(x -> {
x.resolve();
return true;
});
}
}
}
private void parsePendingResultMaps() {
if (incompleteResultMaps.isEmpty()) {
return;
}
synchronized (incompleteResultMaps) {
boolean resolved;
IncompleteElementException ex = null;
do {
resolved = false;
Iterator<ResultMapResolver> iterator = incompleteResultMaps.iterator();
while (iterator.hasNext()) {
try {
iterator.next().resolve();
iterator.remove();
resolved = true;
} catch (IncompleteElementException e) {
ex = e;
}
}
} while (resolved);
if (!incompleteResultMaps.isEmpty() && ex != null) {
// At least one result map is unresolvable.
throw ex;
}
}
}
/**
* Extracts namespace from fully qualified statement id.
*
* @param statementId
* the statement id
*
* @return namespace or null when id does not contain period.
*/
protected String extractNamespace(String statementId) {
int lastPeriod = statementId.lastIndexOf('.');
return lastPeriod > 0 ? statementId.substring(0, lastPeriod) : null;
}
// Slow but a one time cost. A better solution is welcome.
protected void checkGloballyForDiscriminatedNestedResultMaps(ResultMap rm) {
if (rm.hasNestedResultMaps()) {
final String resultMapId = rm.getId();
for (Object resultMapObject : resultMaps.values()) {
if (resultMapObject instanceof ResultMap) {
ResultMap entryResultMap = (ResultMap) resultMapObject;
if (!entryResultMap.hasNestedResultMaps() && entryResultMap.getDiscriminator() != null) {
Collection<String> discriminatedResultMapNames = entryResultMap.getDiscriminator().getDiscriminatorMap()
.values();
if (discriminatedResultMapNames.contains(resultMapId)) {
entryResultMap.forceNestedResultMaps();
}
}
}
}
}
}
// Slow but a one time cost. A better solution is welcome.
protected void checkLocallyForDiscriminatedNestedResultMaps(ResultMap rm) {
if (!rm.hasNestedResultMaps() && rm.getDiscriminator() != null) {
for (String discriminatedResultMapName : rm.getDiscriminator().getDiscriminatorMap().values()) {
if (hasResultMap(discriminatedResultMapName)) {
ResultMap discriminatedResultMap = resultMaps.get(discriminatedResultMapName);
if (discriminatedResultMap.hasNestedResultMaps()) {
rm.forceNestedResultMaps();
break;
}
}
}
}
}
protected static class StrictMap<V> extends ConcurrentHashMap<String, V> {
private static final long serialVersionUID = -4950446264854982944L;
private final String name;
private BiFunction<V, V, String> conflictMessageProducer;
public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}
public StrictMap(String name, int initialCapacity) {
super(initialCapacity);
this.name = name;
}
public StrictMap(String name) {
this.name = name;
}
public StrictMap(String name, Map<String, ? extends V> m) {
super(m);
this.name = name;
}
/**
* Assign a function for producing a conflict error message when contains value with the same key.
* <p>
* function arguments are 1st is saved value and 2nd is target value.
*
* @param conflictMessageProducer
* A function for producing a conflict error message
*
* @return a conflict error message
*
* @since 3.5.0
*/
public StrictMap<V> conflictMessageProducer(BiFunction<V, V, String> conflictMessageProducer) {
this.conflictMessageProducer = conflictMessageProducer;
return this;
}
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains key " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
@Override
public boolean containsKey(Object key) {
if (key == null) {
return false;
}
return super.get(key) != null;
}
@Override
public V get(Object key) {
V value = super.get(key);
if (value == null) {
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
protected static class Ambiguity {
private final String subject;
public Ambiguity(String subject) {
this.subject = subject;
}
public String getSubject() {
return subject;
}
}
private String getShortName(String key) {
final String[] keyParts = key.split("\\.");
return keyParts[keyParts.length - 1];
}
}
}
下面让我们一起看一下Configuration节点中environments节点的解析,这个解析是通过environmentsElement (XNode)方法完成的,其源码如下所示:
private void environmentsElement(XNode context) throws Exception {
if (context == null) {
return;
}
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
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());
break;
}
}
}
关于这段代码有以下几点需要注意:
- 该方法中的Environment.Builder用到了建造者模式,这里的这个写法是我熟悉的建造者模式的标准写法(定义主类及主类中的Builder类,注意这两个类的属性完全一致,主类中的所有属性只提供get方法,Builder类中既提供get方法,又提供set方法。这里一定要注意主类的构造方法会对相关属性进行判断(实际中不一定要有)。最后使用时先创建Builder对象,然后调用Builder对象上的build()方法创建主类对象)
- 该方法中的DataSourceFactory和TransactionFactory用到了工厂设计模式,其中TransactionFactory在上一篇文章中已经梳理过了,这里不再赘述,有兴趣的可以翻读一下前一篇文章《MyBatis是纸老虎吗?(二)》。这里我们主要介绍DataSourceFactory接口,该接口的主要作用就是创建DataSource对象。其主要提供了两个方法setProperties()及getDataSource()。其实现类有:JndiDataSourceFactory、UnpooledDataSourceFactory、PooledDataSourceFactory
- 这点非常重要,创建出来的DataSource对象最终会被设置到Configuration对象的environment属性中
下面让我们一起看依稀啊DataSourceFactory的继承体系,然后再梳理一下最终创建DataSource对象的逻辑:
通过源码可以知道DataSourceFactory实现类的getDataSource()方法都只有一行代码,即返回一个DataSource对象。具体源码如下所示:
public DataSource getDataSource() {
return dataSource;
}
那原本认为DataSourceFactory执行的判断逻辑必然会前移到XMLConfigBuilder类的dataSourceElement()方法中,该方法的源码为:
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
注意这个方法接收的是environments下的environment下的 dataSource节点。该节点上有一个type属性,这个属性可选的值有:POOLED|JNDI|UNPOOLED。注意这些别名的注入是在Configuration类的构造方法中完成的,具体如下图所示:
下面我们再来理解dataSourceElement()方法中的DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance()这句代码,其中resolveClass(type)方法的主要作用就是根据别名解析出实际的DataSource类型,然后通过反射创建一个DataSource对象。下面主要梳理一下resolveClass()方法,其源码及调用方法源码如下所示:
// 下面这个方法位于BaseBuilder类中,其会注解调用本类中的resolveAlias()方法
rotected <T> Class<? extends T> resolveClass(String alias) {
try {
return alias == null ? null : resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
// 下面这个方法位于BaseBuilder类中,其会注解调用TypeAliasRegistry中的resolveAlias()方法
protected <T> Class<? extends T> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
// 下面这个方法位于TypeAliasRegistry类中,其主要作用就是拿到别名对应value,本质上就是map的get操作
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (typeAliases.containsKey(key)) {
value = (Class<T>) typeAliases.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
至此我们已经完全弄懂了environment节点的解析流程。这也解决了前一篇文章遗留的第三个问题:Environment的初始化画流程是怎样的?下面我就用自己的理解来回答一下这个问题:
- 首先,解析MyBatis配置文件中的environments节点和其子节点environment,以及environment的子节点transactionManager和datasource(这个过程涉及到了xml文件解析的知识)
- 接着,通过TransactionFactory和DataSourceFactory来创建Transaction和DataSource对象(这里用到了MyBatis别名及反射的知识,同时也用到工厂设计模式)
- 通过Environment.Builder来创建Environment对象(这里用到了建造者设计模式)
- 将创建的Environment对象初始化到Configuration对象的environment属性中
好了,今天的文章就到这里吧!通过这篇文章,我们明白了MyBatis配置文件的解析流程;弄清楚了MyBatis配置文件中的常见属性及其意义;搞清楚了这些属性与MyBatis组件间的对应关系。所以本篇开头的那个疑问已经不是什么问题了。在结束前,我想再次对知乎大佬——敲代码的小叔叔——说声谢谢。因为他的文章《Mybatis配置文件XML全貌详解,再不懂我也没招了》,我很快弄懂了MyBatis配置文件中几个常用属性的意义,还是因为他的文章《Mybatis配置文件XML全貌详解,再不懂我也没招了》,我的文章内容变得更加丰富。