配置文件解析入口
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先我们利用工具类 Resources
加载配置文件, 在通过 SqlSessionFactoryBuilder 类的 build()
方法构建出 SqlSessionFactory 对象,由此可以看到我们的入口程序就是 build() 方法。
//配置文件解析初始化入口
public SqlSessionFactory build(InputStream inputStream) {
//调用重载方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//初始化配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse() 开始解析配置文件,生成 Configuration 对象
// 调用重载的 build 方法初始化 SqlSessionFactory 对象
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) {
// 初始化 SqlSessionFactory 对象
return new DefaultSqlSessionFactory(config);
}
由此可以看出,mybatis 是利用 XMLConfigBuilder
这个类来进行对象解析的,真正解析的方法为 XMLConfigBuilder#parse()
方法。解析之后生成 Configuration
对象,这里包装着所有解析得到的内容。
mybatis中做的很好的一点就是,每个方法的职责很单一,像XMLConfigBuilder
这个类只做了解析配置文件这一件事,而后面的 mapper 文件解析则是由 XMLMapperBuilder
这个类来完成。
接下来我们继续看 XMLConfigBuilder#parse()
方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析全局配置文件中的 configuration 标签
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 properties 标签
propertiesElement(root.evalNode("properties"));
// 解析 settings 标签,得到 Properties 对象
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析 typeAliases 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 plugins 标签
pluginElement(root.evalNode("plugins"));
// 解析 objectFactory 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 reflectorFactory 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 中的信息设置到 Configuration 对象中
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 environments 标签
environmentsElement(root.evalNode("environments"));
// 解析 databaseIdProvider 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 typeHandlers 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 mappers 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
由此,我们才得到一个完整的解析配置文件的全过程,下面我们开始分解标签,一步步的开始阅读。由于一些配置很少会用到,我们这里不做阅读。
解析 properties 标签
解析properties
标签是由propertiesElement(root.evalNode("properties"))
方法完成;
先看一下我们配置以及属性文件:
<properties resource="database-config.properties">
<property name="db.passwords" value="admin"/>
<property name="transactionManagerType" value="JDBC"/>
</properties>
db.url=jdbc:mysql://localhost:3306/vd_mall?useSSL=false&useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
db.driver=com.mysql.cj.jdbc.Driver
db.username=root
db.password=root
现在我们正式开始分析我们的解析流程:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//解析所有属性节点
Properties defaults = context.getChildrenAsProperties();
//解析resource属性的值
String resource = context.getStringAttribute("resource");
//解析url属性的值
String url = context.getStringAttribute("url");
//如果 resource 与 url 同时配置则抛出异常
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
// * 这里 putAll 时,会对同名属性值进行覆盖,
// * 也就是说 properties 标签中配置的 property 配置,会被 resource 或 rul 中的同名属性覆盖
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;
}
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) {
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
这里我们总结下:首先是解析 properties 子标签属性值,然后在解析资源文件中的属性值,最后资源文件中和 property 标签中如果存在相同的key ,则保留资源文件中的值。
比如我们前文的配置被替换后会得到如下属性:
解析 settings 标签
解析settings
标签是由settingsAsProperties(root.evalNode("settings"))
方法完成;此方法将settings
标签下的属性解析成 properties 对象。
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
//解析所有setting 标签的子标签 得到对应属性
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 检测 Configuration 中是否存在相关属性,不存在则抛出异常
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;
}
将settings配置到Configuration对象
这里的逻辑很简单,就是将 props 中对应 key 的值取出,赋值给 configuration 取不到值的赋默认值.
/**
* 将 <settings></settings> 标签中解析出来的属性,装载到 configuration 对象中
* 这里的逻辑很简单,就是将 props 中对应 key 的值取出,赋值给 configuration 取不到值的赋默认值
* @param props
*/
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
//省略部分代码
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
解析 typeAliases 标签
在 Mybatis 中提供了两种 typeAliases
的配置方式,一种是通过 <package>
标签,这样会将我们所配置路径下的所有类全部注册,另一种则是通过 <typeAlias>
标签配置,这两种配置方式,很显然第一中的配置很简单,我觉得唯一的缺点可能就就是会造成将我们不需要的类也一起注册了。先来看一下配置:
<typeAliases>
<typeAlias type="org.bmth.mybatis.entity.User" alias="user"/>
<package name="org.bmth.mybatis.pojo"/>
</typeAliases>
下面一起分析下源码
private void typeAliasesElement(XNode parent) {
if (parent != null) {
//得到 typeAliases 下的子标签
for (XNode child : parent.getChildren()) {
// <package> 标签 解析
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else { //<typeAlias> 标签 解析
//获取别名
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);
}
}
}
}
}
这里一共分成了两个分支,第一个分支就是解析 <package>
标签的操作,另一个分支则是对于 <typeAlias>
标签的解析,首先我们先来看一下对于<typeAlias>
的解析,后面对于 <package>
标签的解析最终也会回归到这里。这里通过俩个注册器来完成 :
typeAliasRegistry.registerAlias(clazz);
typeAliasRegistry.registerAlias(alias, clazz)
//别名注册的容器
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(Class<?> type) {
//获取全路径名的简称
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
//如果类上存在 Alias 注解,则获取注解的值
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");
}
// issue #748
// 将别名转成小写
String key = alias.toLowerCase(Locale.ENGLISH);
//如果该别名已经注册过,则检查注册类型的一致性 不一致则抛出异常
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
//装载到 map 中
typeAliases.put(key, value);
}
a. 若显式的配置 alias 属性,比如我们上面的配置<typeAlias type="org.bmth.mybatis.entity.User" alias="user"/>
,此时mybatis 将会直接使用我们自己定义的别名。
b. 未显式的配置 alias 属性,mybatis 会使用类名的小写形式作为别名,
比如,全限定类名org.bmth.mybatis.entity.User
的别名为user
。
c. 未显式的配置 alias 属性,但类中有@Alias
注解,则从注解中取值作为别名。
对于 <package>
标签解析
//TypeAliasRegistry.java
public void registerAliases(String packageName) {
//调用重载方法 传入扫描路径 和一个 Object 类型
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 查找某个包下的父类为 superType 的类。
// 这里的superType = Object.class,所以 ResolverUtil 将查找所有的类。
// 查找完成后,查找结果将会被缓存到内部集合中。
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
//查找到的所有符合的结果集
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
//排除匿名类,接口,内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
//调用有类型,无别名注册方法
registerAlias(type);
}
}
}
这里的流程并不复杂,可以简单的概括下:
a. 查找指定包下的所有类
b. 遍历集合,为每个类型注册别名
c. 调用有类型,无别名注册方法,此方法已经在上面分析过,这里不再赘述。
当然,mybatis 还为我们初始化一些我们经常使用的类,会在初始化 TypeAliasRegistry
类的时候通过构造进行注册
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
// 省略部分代码
}
解析 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 中配置 plugins 的方式如下:
<plugins>
<plugin interceptor="org.bmth.mybatis.plugins.QueryLimitPlugins">
<property name="limit" value="10"/>
</plugin>
</plugins>
下面我们一起看下 plugins
标签是如何解析的
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//获取plugins拦截器的类路径
String interceptor = child.getStringAttribute("interceptor");
//获取plugins下所配置的 properties 属性
Properties properties = child.getChildrenAsProperties();
//实例化拦截器
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
//向拦截器内注入 properties
interceptorInstance.setProperties(properties);
//将初始化完成的拦截器放入configuration
configuration.addInterceptor(interceptorInstance);
}
}
}
解析 environments 标签
在 mybatis 中,事务和数据库连接的相关配置都在这里,配置方法如下:
<environments default="development">
<environment id="development">
<transactionManager type="${transactionManagerType}"/>
<dataSource type="POOLED">
<property name="driver" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</dataSource>
</environment>
</environments>
下面我们正式开始分析 environments 标签的解析过程,先看下面代码:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 获取 default 属性
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
//获取 environment 标签下的 id 属性
String id = child.getStringAttribute("id");
//检查配置的 default 属性和我们获取到的 id 属性是否一致
if (isSpecifiedEnvironment(id)) {
//解析 transactionManager 节点,此处参考 plugins的解析方式
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 dataSource 节点 此处参考 plugins的解析方式
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
//通过工厂 创建数据源
DataSource dataSource = dsFactory.getDataSource();
// builder 模式 构建 Environment 对象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将 Environment 对象 装载到 configuration中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
这里就是将数据库的配置进行了读取,然后通过DataSourceFactory
创建数据源,最后将数据源和事务构造成Environment
对象,装载到 configuration
对象中。
解析 typeHandlers 标签
我们先来看下关于 typeHandlers
的两种配置方式:
<!--扫描包-->
<typeHandlers>
<package name="org.bmth.mybatis.handler"/>
</typeHandlers>
<!--指定类配置-->
<typeHandlers>
<typeHandler handler="org.bmth.mybatis.handler.AddressTypeHandler" javaType="org.bmth.mybatis.entity.Address" jdbcType="VARCHAR"/>
</typeHandlers>
接下来我们就看 typeHandlers
标签是如何解析的:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//从指定的包中注册 typeHandler
if ("package".equals(child.getName())) {
//得到 <package name="org.bmth.mybatis.handler"/> 标签中的 name 属性值
String typeHandlerPackage = child.getStringAttribute("name");
//对该包下的类进行注册
//注册器 ① register(String packageName),此处进行包扫描后,会调用 注册器 ④
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// <typeHandler handler="" javaType="" jdbcType=""/>
// 获取 <typeHandler> 标签中的对应属性值
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);
//根据不同的条件策略调用不同的构造器
if (javaTypeClass != null) {
if (jdbcType == null) {
//注册器 ② register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
//注册器 ③ register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
//注册器 ④ register(Class<?> typeHandlerClass)
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
其实这里针对于标签的解析很简单,主要比较难读的地方就是注册 typeHandler
的时候,调用的重载方法比较绕。因此这里我们拆分成4部分来看,如图,对应的代码中注释的注册器,可以看到,从4个入口开始调用,经过一系列的重载之后,都会最终调用 register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
方法。
注册器③流程解析
由于 ③ 的流程比较独立,因此我们先看 注册器 ③ 的解析过程,进入此流程的条件 javaTypeClass != null && jdbcType != null
首先调用重载方法 register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)
然后直接调用最终方法:
@SuppressWarnings("cast")
public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
register((Type) type, jdbcType, handler);
}
/**
* 此方法是 typeHandler 注册器的最终调用方法
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
//通过JavaType 获取 该 JavaType 对应的 jdbcType 到 TypeHandler 的映射
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
//没找到时初始化map
map = new HashMap<>();
}
//构建 jdbcType 到 TypeHandler 的映射
map.put(jdbcType, handler);
// 重新存储到 javaType 到 Map<JdbcType, TypeHandler> 的映射中
typeHandlerMap.put(javaType, map);
}
//这里存储着所有 TypeHandler的映射
allTypeHandlersMap.put(handler.getClass(), handler);
}
这里就是将 javaType 和 Map<JdbcType, TypeHandler>做了映射。
注册器②流程解析
进入注册器② 的条件 javaTypeClass != null && dbcType == null
,首先调用重载方法register(Class<T> javaType, TypeHandler<? extends T> typeHandler)
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
register((Type) javaType, typeHandler);
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
//从 TypeHandler 类中获取 MappedJdbcTypes 注解
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
//遍历注解中
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
//调用最终方法
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
//调用最终方法
register(javaType, null, typeHandler);
}
} else {
//调用最终方法
register(javaType, null, typeHandler);
}
}
此流程主要解析MappedJdbcTypes.class
,然后调用最终方法,这里不再赘述。
注册器④流程解析
进入注册器④的条件 javaTypeClass == null
,首先调用重载方法register(Class<?> typeHandlerClass)
public void register(Class<?> typeHandlerClass) {
//这里是一个标记
boolean mappedTypeFound = false;
//获取 typeHandlerClass 类上的 MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
//获取注解中的value值 并循环
for (Class<?> javaTypeClass : mappedTypes.value()) {
//调用重载方法 register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
//此处为注册器②中的重载方法,这里不再重复
register(javaTypeClass, typeHandlerClass);
//标记为已找到
mappedTypeFound = true;
}
}
//MappedTypes 注解没有配置时
if (!mappedTypeFound) {
//调用重载方法 register(TypeHandler<T> typeHandler)
register(getInstance(null, typeHandlerClass));
}
}
首先获取 MappedTypes.class
注解,如果注解不为null,循环注解中的值,进行注册,此处为 注册器②的注册流程。
当没有配置MappedTypes.class
注解时,调用重载方法 register(TypeHandler<T> typeHandler)
。
public void register(Class<?> typeHandlerClass) {
//这里是一个标记
boolean mappedTypeFound = false;
//获取 typeHandlerClass 类上的 MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
//获取注解中的value值 并循环
for (Class<?> javaTypeClass : mappedTypes.value()) {
//调用重载方法 register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
register(javaTypeClass, typeHandlerClass);
//标记为已找到
mappedTypeFound = true;
}
}
//MappedTypes 注解没有配置时
if (!mappedTypeFound) {
//调用重载方法 register(TypeHandler<T> typeHandler)
register(getInstance(null, typeHandlerClass));
}
}
这里主要就是解析 javaType
的值。其他的重载方法我这之前的步骤中已经覆盖到了。
注册器①流程解析
该流程是通过扫描包的方式配置typeHandlers
,首先将我们配置的包中的所有TypeHandler
类进行扫描,然后通过调用注册器④进行注册,因此这里只关注扫描包:
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
//从指定包中扫描 TypeHandler 类
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
// 忽略 内部类、接口,抽象类
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
//调用 重载方法 register(Class<?> typeHandlerClass)
register(type);
}
}
}
结语
到这里全局配置文件的解析就基本告一段落了,剩余的mappers
标签的解析,由于涉及到 SQL的解析,而且又是MyBatis中比较核心的一部分,因此放到下一篇单独去讲解。
另外,在读取源码的时候,在代码中添加的注释,也已经提交到了gitee上,需要的自行克隆即可。