MyBatis应用程序初始化流程分析
从今天开始,我们就正式进入 MyBatis 源码篇的学习了,关于这部分内容我的规划是这样的:
- 分析 MyBatis 应用程序的整体流程,包括初始化阶段和运行阶段;
- 划分 MyBatis 源码的模块(按照常规的 3 层模块划分),通过源码分析它们的功能,技术和设计模式;
- 介绍 MyBatis 插件的使用与开发,并和大家一起开发 MyBatiis 的插件。
下面,我们就一起来学习今天的内容:MyBatis 应用程序初始化流程分析。
构建 MyBatis 应用程序
这里我们直接使用 《MyBatis 入门》 中构建的 MyBatis 应用程序,该程序中只包含以下 4 部分内容:
- MyBatis 核心配置文件(mybatis-config.xml)
- 映射器接口 UserMapper
- Java 持久化对象 UserDO
- 映射器文件(UserMapper.xml)
接下来我们写一段测试代码,如下:
@Test
public void testSelectOrderItemByOrderId() {
// 构建 MyBatis 应用
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// 获取 SqlSessionFactory 实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 获取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取 Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行根据用户ID
UserDO userDO = userMapper.selectUserByUserId(1);
}
上面的测试代码可以分为两个部分:
- 第 3 行代码到第 6 行代码,加载 MyBatis 核心配置文件和创建 SqlSessionFactory 实例,也就是 MyBatis 应用程序的初始化阶段;
- 第 8 行代码到第 13 行代码,获取 SqlSession 实例,并通过 SqlSession 实例获取 UserMapper 实例,执行查询动作,也就是 MyBatis 应用程序的运行阶段。
今天我们的重点是 MyBatis 应用程序初始化阶段的源码实现,也就是第 3 行代码到第 6 行代码背后的实现。
加载配置文件
测试代码的第 4 行代码中,使用了工具类 Resources 加载 MyBatis 的核心配置文件。
Resources 是 MyBatis 提供的用于加载资源文件的工具类,采用了 Java 对工具类的命名风格(如,Java 中 Collection 的工具类是 Collections),Resources 提供了一系列的静态方法实现了以不同的方式加载资源文件。
关于工具类 Resources,我们不需要深入探究它的实现原理(实际上实现原理也并不复杂),只需要了解它的常用方法即可,部分常用方法的源码如下:
public class Resources {
/**
* 通过资源文件路径加载资源文件
* 返回 InputStream 对象
*/
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}
/**
* 通过资源文件路径加载资源文件
* 返回 Reader 对象
*/
public static Reader getResourceAsReader(String resource) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getResourceAsStream(resource));
} else {
reader = new InputStreamReader(getResourceAsStream(resource), charset);
}
return reader;
}
/**
* 通过 URL 加载资源文件
* 返回 InputStream 对象
*/
public static InputStream getUrlAsStream(String urlString) throws IOException {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
return conn.getInputStream();
}
/**
* 通过 URL 加载资源文件
* 返回 Reader 对象
*/
public static Reader getUrlAsReader(String urlString) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getUrlAsStream(urlString));
} else {
reader = new InputStreamReader(getUrlAsStream(urlString), charset);
}
return reader;
}
}
创建 SqlSessionFactory 实例
测试代码的第 6 行,创建了 SqlSessionFactoryBuilder 实例,从类型的名称上我们可以看到,SqlSessionFatoryBuilder 使用了建造者模式。
接着调用了 SqlSessionFactoryBuilder#build
方法创建 SqlSessionFactory 实例,该方法拥有多个重载方法,主要的区别在于入参的类型是 Reader 还是 InputStream。
因为前面是通过 Resources#getResourceAsReader
方法加载的 MyBatis 核心配置文件,因此这里使用的是入参为 Reader 类型的 SqlSessionFactoryBuilder#build
方法,该方法的部分源码如下:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}
}
SqlSessionFactoryBuilder#build
方法最核心的代码是第 8 行和第 9 行,这两行代码做了 3 个操作:
- 使用 Reader 实例,调用 XMLConfigBuilder 的构造方法创建 XMLConfigBuilder 实例;
- 调用
XMLConfigBuilder#parse
方法解析 Reader 实例(即解析 MyBatis 的核心配置文),并“创建” Configuration 实例(即 MyBatis 核心配置文件在 MyBatis 应用程序中的映射); - 使用 Configuration 实例,调用
SqlSessionFactoryBuilder#build
方法,创建 SqlSessionFactory 实例。
注意,这里的创建两个字上打了引号,是因为创建 Configuration 实例的动作是在 XMLConfigBuilder 的构造方法中,而 XMLConfigBuilder#parse
方法的主要功能是解析 MyBatis 的核心配置文件,并将其保存到 Configuration 实例中。
因为第 3 步的操作非常简单,我们先来看一下。首先是另一个 SqlSessionFactoryBuilder#build
方法的重载方法,源码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
这里只是里调用了 DefaultSqlSessionFactory 的构造方法,我们再来看下 DefaultSqlSessionFactory 的类型声明,成员变量和构造方法,如下:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
可以看到 DefaultSqlSessionFactory 只有一个成员变量 Configuration,并且构造方法中也只是为 Configuration 进行了赋值,没有其它额外的动作了。这也就是说我们使用 SqlSessionFactoryBuilder#build
方法创建的 SqlSessionFactory 实例只是持有了赋值后的 Configuration 实例。
下面我们就来分析另外两个操作的具体实现。
创建 XMLConfigBuilder 实例
创建 XMLConfigBuilder 实例的过程比较简单,我们直接来看 XMLConfigBuilder 的构造方法,部分源码如下:
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(Configuration.class, reader, environment, props);
}
public XMLConfigBuilder(Class<? extends Configuration> configClass, Reader reader, String environment, Properties props) {
this(configClass, new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment, Properties props) {
super(newConfig(configClass));
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
}
第 7 行中,使用 Reader 实例创建了 XPathParser 实例,底层使用了 Java 提供的 java.xml,将 Reader 解析为 XPathParser ,并在第 15 行的代码中赋值给 XMLConfigBuilder 的成员变量 parser。这个赋值动作完成后,XMLConfigBuilder 实例中的成员变量 parser 就存储了 MyBatis 核心配置文件中的所有配置项。
接着来到第 11 行的代码,我们先暂时跳过调用 XMLConfigBuilder#newConfig
方法创建 Configuration 实例的部分,直接来看调用 XMLConfigBuilder 父类构造方法的部分。从 XMLConfigBuilder 的类型声明上可以看到 XMLConfigBuilder 的父类是 BaseBuilder,它的类型声明和构造方法的源码如下:
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
父类 BaseBuilder 中声明了 3 个成员变量:
- Configuration:MyBatis 核心配置文件在 MyBatis 应用程序中的映射
- TypeAliasRegistry:MyBatis 的别名注册器
- TypeHandlerRegistry:MyBatis 的类型处理注册器
BaseBuilder 在构造方法中完成了这 3 个成员变量的赋值。其中 Configuration 是通过 XMLConfigBuilder#newConfig
方法获取的,而 TypeAliasRegistry 和 TypeHandlerRegistry 都是通过 Configuration 获取的,这也就是说在 Configuration 实例的创建过程中,就已经完成了 TypeAliasRegistry 实例和 TypeHandlerRegistry 实例的创建。
XMLConfigBuilder 构造方法中的剩余部分,只进行了简单的赋值操作,这里需要注意下变量 environment,如果在调用 XMLConfigBuilder#build
方法时不会单独传入,后面会根据 MyBatis 核心配置文件中 environments 元素的配置进行赋值。
最后,我们来看一下创建完成的 XMLConfigBuilder 实例中都保存了那些关键的信息。
首先是 3 个继承自父类的成员变量:Configuration 实例,TypeAliasRegistry 实例和 TypeHandlerRegistry 实例,它们与 XMLConfigBuilder#newConfig
方法息息相关,因此当前它们的状态对我们来说还是未知的;最后是 XMLConfigBuilder 自身独有的成员变量 XPathParser 实例,它保存了 MyBatis 核心配置文件中所有的配置信息。
创建 Configuration 实例
回到在 XMLConfigBuilder 构造方法中调用的 XMLConfigBuilder#newConfig
方法中,该方法部分源码如下:
private static Configuration newConfig(Class<? extends Configuration> configClass) {
return configClass.getDeclaredConstructor().newInstance();
}
XMLConfigBuilder#newConfig
方法使用了 Java 的反射技术获取 Configuration 的无参构造器,并调用无参构造器创建 Configuration 实例。
Configuration 的类型声明,成员变量和无参构造器,部分源码如下:
public class Configuration {
// 省略其它成员变量
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
// 省略其它的别名注册
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
}
}
Configuration 的成员变量 TypeHandlerRegistry 和 TypeAliasRegistry 是 final 类型的,final 要求必须在声明变量或者是在构造方法中进行初始化,在 Configuration 中,这两个变量在声明时进行了初始化,分别调用的它们各自的构造方法。
TypeHandlerRegistry 和 TypeAliasRegistry 的构造方法中,注册了大量的类型处理器和类型别名,因为源码太长,我把它们整理到了一张图中,如下:
除了 TypeAliasRegistry 的构造方法中进行了类型别名的注册外,Configuration 的构造方法中也进行了大量的类型别名注册。
再回过头来看 BaseBuilder 的构造方法,以及创建完成的 XMLConfigBuilder 实例,这时我们就能知道 TypeAliasRegistry 实例和 TypeHandlerRegistry 实例中都存储了哪些信息了。
Configuration 实例赋值
上面我们看到,虽然是 XMLConfigBuilder#parse
方法返回了 Configuration 实例,但实际创建 Configuration 实例的过程还是在 XMLConfigBuilder 的构造方法中。那么 XMLConfigBuilder#parse
方法做了什么呢?
我们通过源码来寻找答案,该方法部分源码如下:
public Configuration parse() {
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
第 2 行代中出现了“/configuration”,还记得我们的 MyBatis 核心配置文件吗?它就是以 configuration 元素未开始的,而通过前面的分析我们也可以得知 XMLConfigBuilder 的成员变量 parser 中存储的是 MyBatis 核心配置文件的全部配置项,通过追踪 XPathParser#evalNode
方法的源码可以看到,这里是利用 java.xml 解析并获取 MyBatis 核心配置文件中的 configuration 中的内容,并将其封装为 MyBatis 内部 XNode 实例进行返回。
接着调用了 XMLConfigBuilder#parseConfiguration
方法解析 XNode 实例(此时为 MyBatis 核心配置文件中的 configuration 元素),该方法的部分源码如下:
private void parseConfiguration(XNode root) {
// 解析 properties 元素
propertiesElement(root.evalNode("properties"));
// 解析 settings 元素
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfsImpl(settings);
loadCustomLogImpl(settings);
settingsElement(settings);
// 解析 typeAliases 元素
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 typeHandlers 元素
typeHandlersElement(root.evalNode("typeHandlers"));
// 解析 plugins 元素
pluginsElement(root.evalNode("plugins"));
// 解析 objectFactory 元素
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory 元素
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 reflectorFactory 元素
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 解析 environments 元素
environmentsElement(root.evalNode("environments"));
// 解析 databaseIdProvider 元素
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 mappers 元素
mappersElement(root.evalNode("mappers"));
}
XMLConfigBuilder#parseConfiguration
方法是按照 MyBatis 核心配置文件中 configuration 元素下子元素的的顺序依次进行解析的。
Tips:下面涉及到解析 MyBatis 核心配置文件中配置项的源码,如果你忘记了这些配置项的功能,可以回顾下 《MyBatis核心配置讲解(上)》 和 《MyBatis核心配置讲解(下)》。
解析 properties 元素
解析 properties 元素调用的是 XMLConfigBuilder#propertiesElement
方法,部分源码如下:
private void propertiesElement(XNode context) throws Exception {
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);
}
第 2 行代码,解析了 properties 元素的子元素 property,并创建了 Properties 实例,用于存储配置。
第 3 行和第 4 行代码,解析了 properties 元素的 resource 属性和 url 属性,第 5 行代码开始的 if 语句中,对 resource 属性和 url 属性的互斥性进行了判断,还记得我们在 MyBatis核心配置讲解(上) 中,我们介绍 properties 元素时说到过“properties 元素的 resource 属性与 url 属性是互斥的”吗?现在通过源码,可以很清晰的看到它们为什么互斥了。
最后我们来看第 18 行代码,解析后的 properties 元素的配置内容都被存储到了 XMLConfigBuilder 的成员变量 Configuration 实例中了。
解析 settings 元素
解析 settings 元素相对比较复杂,需要通过 4 个方法完成,首先是调用了 XMLConfigBuilder#settingsAsProperties
方法,该方法的部分源码如下:
private Properties settingsAsProperties(XNode context) {
Properties props = context.getChildrenAsProperties();
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;
}
该方法负责获取 settings 元素下的所有配置项,并且使用 MetaClass 检测 Configuration 中是否包含该配置项的 set 方法。
接着调用了 XMLConfigBuilder#loadCustomVfsImpl
方法和 XMLConfigBuilder#loadCustomLogImpl
用于设置虚拟文件系统和日志。
最后调用了XMLConfigBuilder#settingsElement
方法设置将配置项存储到 Configuration 实例中,该方法部分源码如下:
private void settingsElement(Properties props) {
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
// 其它配置项
}
另外,我们也可以通过 XMLConfigBuilder#settingsElement
方法的源码来查看 MyBatis 都支持那些插件。
解析 typeAliases 元素和 typeHandlers 元素
解析 typeAliases 元素和 typeHandlers 元素分别调用了 XMLConfigBuilder#typeAliasesElement
方法和 XMLConfigBuilder#typeHandlersElement
方法,这两个方法非常相似,因此我们放在一起说明:
可以看到两者分别通过 Configuration 实例获取 typeAliasRegistry 和 typeHandlerRegistry,并将 MyBatis 核心配置文件中的相应配置进行注册。
解析 plugins 元素,objectFactory 元素,objectWrapperFactory 元素和 reflectorFactory 元素
解析 plugins 元素,objectFactory 元素,objectWrapperFactory 元素和 reflectorFactory 元素分别调用了 XMLConfigBuilder#pluginsElement
方法,XMLConfigBuilder#objectFactoryElement
方法,XMLConfigBuilder#objectWrapperFactoryElement
方法和 XMLConfigBuilder#reflectorFactoryElement
方法,它们的源码非常相似,我们还是用一张图来展示:
从源码的角度上看,这 4 个方法非常简单,解析 MyBatis 核心配置文件中相应的配置项,并存储到 Configuration 中,这里我们就不过多赘述了。
解析 environments 元素
解析 environments 元素调用的是 XMLConfigBuilder#environmentsElement
方法,该方法的部分源码如下:
private void environmentsElement(XNode context) throws Exception {
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 遍历 environments 元素的子元素,也即是每个环境的配置
for (XNode child : context.getChildren()) {
// 获取环境的 Id
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;
}
}
}
第 3 行的代码,如果没有在执行 SqlSessionFactoryBuilder#build
方法时指定默认环境的名称,那么就会使用 MyBatis 核心配置文件中配置的默认环境名称,这也从侧面印证了在 MyBatis 中,Java 配置的优先级大于 XML 配置的优先级。
第 9 行代码,调用了 XMLConfigBuilder#isSpecifiedEnvironment
方法,用于判断该环境是否为指定环境,该方法的源码如下:
private boolean isSpecifiedEnvironment(String id) {
if (environment == null) {
throw new BuilderException("No environment specified.");
}
if (id == null) {
throw new BuilderException("Environment requires an id attribute.");
}
return environment.equals(id);
}
通过源码我们可以看到,在配置环境时,MyBatis 要求必须指定默认环境,并且每个环境配置都必须指定 Id,最后会判断当前环境是否为配置的默认环境。
我们回到 XMLConfigBuilder#environmentsElement
方法的第 9 行,结合 XMLConfigBuilder#isSpecifiedEnvironment
方法,我们可以看到,如果当前环境为默认,则会进入条件语句中,加载环境配置,否则跳过当前环境,不进行加载。
另外,我们注意到条件语句的最后使用了关键字 break,也就是说,如果你在 MyBatis 核心配置文件中,配置了多个 Id 重复的环境,那么只有第一个会被加载。
解析 databaseIdProvider 元素
解析 databaseIdProvider 元素调用的是 XMLConfigBuilder#databaseIdProviderElement
方法,该方法的部分源码如下:
private void databaseIdProviderElement(XNode context) throws Exception {
String type = context.getStringAttribute("type");
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
DatabaseIdProvider databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor() .newInstance();
databaseIdProvider.setProperties(properties);
Environment environment = configuration.getEnvironment();
if (environment != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
关于 databaseIdProvider 元素的使用,我们已经在 《MyBatis核心配置讲解(下)》 中和大家聊过了,如果你已经忘了它的功能,可以回过头来看看之前的文章。
第 7 行代码,根据我们在 MyBatis 核心配置文件中配置的 databaseIdProvider 元素,获取 DatabaseIdProvider 的实现类,目前的版本下,MyBatis 官方已经将实现类 DefaultDatabaseIdProvider 标记为废弃,所以实际上 DatabaseIdProvider 的可用实现类只有 VendorDatabaseIdProvider 了。
第 8 行代码,将我们在 MyBatis 核心配置文件中配置的数据库 Id 存储到 DatabaseIdProvider 实例中。
第 11 行代码,调用 DatabaseIdProvider#getDatabaseId
方法,获取数据库的 Id,这里会与我们在 environments 元素配置的数据源做一个匹配,如果能够匹配成功,则返回我们在 databaseIdProvider 元素中配置的 Id,否则返回数据源实际的名称作为 Id,修改后的部分源码如下:
public String getDatabaseId(DataSource dataSource) {
return getDatabaseName(dataSource);
}
private String getDatabaseName(DataSource dataSource) throws SQLException {
// 获取数据源使用的数据库名称
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
// 匹配数据源使用的数据库名称与 MyBatis 核心配置文件中配置的名称
return properties.entrySet().stream().filter(entry -> productName.contains((String) entry.getKey())).map(entry -> (String) entry.getValue()).findFirst().orElse(null);
}
return productName;
}
// 通过 Connection 获取数据源使用的数据库的产品名称
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
try (Connection con = dataSource.getConnection()) {
return con.getMetaData().getDatabaseProductName();
}
}
解析 mappers 元素
解析 mappers 元素调用的是 XMLConfigBuilder#mappersElement
方法,修改后的部分源码如下:
private void mappersElement(XNode context) throws Exception {
for (XNode child : context.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 使用 resource 属性配置的映射器
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
}
/// 使用 url 属性配置的映射器
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try (InputStream inputStream = Resources.getUrlAsStream(url)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
}
// 使用 mapperClass 属性配置的映射器
else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
如果单纯的看 XMLConfigBuilder#mappersElement
方法的话,整体的逻辑非常简单,根据 mappers 元素的子元素 mapper 使用的配置属性,分成 3 种情况进行解析。
但是一旦结合 XMLMapperBuilder#parse
方法来看整个解析映射器文件的过程,内容就会变得非常多,而且远比 XMLConfigBuilder#parse
方法更加复杂,涉及到映射器 Mapper.xml 文件的解析,缓存的解析,结果集映射的解析和 SQL 语句的解析,所以这部分内容我们就留到下一篇文章中再来和大家详细的分析。
转载自:https://juejin.cn/post/7390642614884450331