本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!
大家好,我是 王有志,一个分享硬核 Java 技术的金融摸鱼侠,欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。
前面我们用了 12 篇文章 学习如何使用 MyBatis,内容涵盖了绝大部分 MyBatis 的常规用法,当然也有一些知识点我们没有介绍到,如:延迟加载,鉴别器映射,枚举类型映射和自定义缓存等,主要是这些功能的使用场景相对较少,一股脑的全部学下来只会徒增大家的负担,后面如果有需求的话我们再来补充。
那么从今天开始,我们就正式进入 MyBatis 源码篇的学习了,关于这部分内容我的规划是分为 3 部分和大家分享:
- MyBatis 应用程序初始化流程分析
- MyBatis 应用程序执行流程分析
- MyBatis 插件的使用与开发
今天我们就从一个简单的应用案例入手,从整体的视角来分析 MyBatis 应用程序在初始化的过程中都做了哪些事情,有哪些技巧和设计是我们可以借鉴到日常开发工作中的。
构建简单的 MyBatis 应用案例
我们直接使用 《MyBatis 入门》 中构建的简单的 MyBatis 应用程序,该程序中只包含以下 4 部分内容:
- MyBatis 核心配置文件(mybatis-config.xml)
- 映射器接口 UserMapper
- Java 持久化对象 UserDO
- 映射器文件(UserMapper.xml)
我们来为这个简单的程序写一个单元测,代码如下:
public void testSelectOrderItemByOrderId() {
// MyBatis 应用程序初始化阶段
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
// MyBatis 应用程序执行阶段
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserDO userDO = userMapper.selectUserByUserId(1);
}
这段单元测试中的代码可以分为两个部分:
- 第 3 ~ 6 行代码,加载 MyBatis 核心配置文件,创建 SqlSessionFactory 实例,这也就是 MyBatis 应用程序的初始化阶段;
- 第 8 ~ 10 行代码,获取 SqlSession 实例,通过 SqlSession 实例获取 MyBatis 映射器对应的接口 UserMapper,并执行查询动作,这也就是 MyBatis 应用程序的执行阶段。
今天我们的重点是 MyBatis 应用程序初始化阶段的源码实现,也就是第 3 ~ 6 行代码背后的实现原理。
使用 Resources 工具加载 MyBatis 核心配置文件
单元测试的第 3 行代码,使用了 MyBatis 提供的工具类 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;
}
}
这里需要注意下,经过Resources#getResourceAsReader方法后,MyBatis 核心配置文件已经从 mybatis-config.xml 转变为了字符输入流 Reader 实例。
使用 SqlSessionFactoryBuilder 创建 SqlSessionFactory 实例
单元测试中的第 4 行代码和 5 行代码,创建了 SqlSessionFactoryBuilder 实例,并调用SqlSessionFactoryBuilder#build方法创建了 SqlSessionFactory 实例。这里创建 SqlSessionFactory 实例的过程是建造者模式的简单应用,只使用了建造者 SqlSessionFactoryBuilder 和产品 SqlSessionFactory,省略了导演和建造者接口这两个角色。
SqlSessionFactoryBuilder 中有多个SqlSessionFactoryBuilder#build方法的重载方法,以下 3 个是我们这次会使用到的,修改后的源码如下:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
// 创建 XMLConfigBuilder 实例
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析 MyBatis 核心配置文件
Configuration configuration = parser.parse();
// 创建 SqlSessionFactory 实例
return build(configuration);
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
我们重点关注第 7 行到第 14 行的SqlSessionFactoryBuilder#build方法,这个方法中的 3 行代码做了 3 个操作:
- 创建 Configuration 的建造者 XMLConfigBuilder 实例;
- 解析 MyBatis 核心配置文件,并初始化 Configuration 实例;
- 创建 SqlSessionFactory 的默认实现类。
我依次对这 3 个操作中涉及到的内容进行分析。
创建 XMLConfigBuilder 实例
XMLConfigBuilder 的部分源码如下:
public class XMLConfigBuilder extends BaseBuilder {
// 用于标记是否已经被解析
private boolean parsed;
// MyBatis 对 Java 中 XPath 的封装,用于处理 XPath 表达式
private final XPathParser parser;
// 用于记录当前使用的配置环境
private String environment;
// MyBatis 封装的反射工厂,提供了缓存反射元数据的能力
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
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;
}
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);
}
}
}
第 16 行代码构建 XPathParser 实例时使用了 Reader 实例,这时我们可以认为 MyBatis 核心配置文件已经转变 XPathParser 实例,另外活还需要关注下第 24 行代码,将 XMLConfigBuilder 的 parser 字段指向了 XPathParser 实例,也就是说SqlSessionFactoryBuilder#build方法中构建出的 XMLConfigBuilder 实例包含了 MyBatis 核心配置文件的配置。
第 20 行代码,这一行中有两个方法调用,首先调用XMLConfigBuilder#newConfig 方法创建 Configuration 实例,该方法中使用了 Java 的反射技术(果然,反射是框架的必备技术),接着调用了父类 BaseBuilder 的构造方法。
BaseBuilder 的结构
BaseBuilder 是 MyBatis 中定义的抽象类,它是 MyBatis 中大部分建造者的父类,它的部分源码如下:
public abstract class BaseBuilder {
// MyBatis 核心配置文件在 MyBatis 应用程序中的映射
protected final Configuration configuration;
// MyBatis 的别名注册器
protected final TypeAliasRegistry typeAliasRegistry;
// MyBatis 的类型处理注册器
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
BaseBuilder 是在构造方法中完成了成员变量的赋值,其中 Configuration 是通过 XMLConfigBuilder#newConfig 方法获取的,而 TypeAliasRegistry 和 TypeHandlerRegistry 都是通过 Configuration 获取的,这也就是说在 Configuration 实例的创建过程中,就已经完成了 TypeAliasRegistry 实例和 TypeHandlerRegistry 实例的创建。
Configuration 的结构
Configuration 是 MyBatis 核心配置文件在 MyBatis 应用程序中的映射,它的部分源码如下:
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 中声明了大量的成员变量,其中大部分都为 MyBatis 核心配置文件中的元素的映射项,上面的源码中我们忽略了这部分内容。另外,Configuration 中还声明了两个注册器:
- TypeHandlerRegistry,类型处理器注册器
- TypeAliasRegistry,别名注册器
Configuration 的构造方法中,针对于 TypeAliasRegistry 进行了大量的别名注册,同时在 TypeAliasRegistry 的构造方法中也进行了大量的别名注册,部分源码如下:
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("char", Character.class);
registerAlias("character", Character.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("ResultSet", ResultSet.class);
}
两处别名注册有什么差别呢? Configuration 的构造方法中,只注册了于 MyBatis 核心配置文件有关的别名,而在 TypeAliasRegistry 的构造方法中进行了大量通用别名的注册,这样做的好处是通过构造方法创建的 TypeAliasRegistry 始终保持着它作为工具的“纯粹性”,不包含与 Configuration 相关的逻辑。
另外,TypeHandlerRegistry 的初始化与 TypeAliasRegistry 类似,虽然 Configuration 的构造方法中没有进行任何类型处理器的注册,但是在 TypeHandlerRegistry 的构造方法中注册了大量的类型处理器。
解析 MyBatis 核心配置文件
上面我们已经分析了 XMLConfigBuilder 实例的构建过程,下面我们来看XMLConfigBuilder#parse方法的处理过程,该方法部分源码如下:
public class XMLConfigBuilder extends BaseBuilder {
public Configuration parse() {
parseConfiguration(parser.evalNode("/configuration"));
return 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#parse方法是通过调用XMLConfigBuilder#parseConfiguration方法完成的 MyBatis 核心配置文件的解析。
Tips:下面涉及到解析 MyBatis 核心配置文件中配置项的源码,如果你忘记了这些配置项的功能,可以回顾下 《MyBatis核心配置讲解(上)》 和 《MyBatis核心配置讲解(下)》。
解析 properties 元素
解析 properties 元素调用的是XMLConfigBuilder#propertiesElement方法,部分源码如下:
private void propertiesElement(XNode context) throws Exception {
// 创建用于存储 properties 元素配置的容器
Properties defaults = context.getChildrenAsProperties();
// 解析 properties 元素的 resource 属性
String resource = context.getStringAttribute("resource");
// 解析 properties 元素的 url 属性
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.");
}
// 分别解析通过 resource 属性或 url 属性的配置,并存储到容器中
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 解析通过 Java 方法添加的配置,并存储到容器中
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
// 将解析后的 properties 元素存储到 Configuration 实例中
configuration.setVariables(defaults);
}
我们重点关注第 8 行代码的条件语句,这里对 properties 元素的 resource 属性和 url 属性的互斥性进行了判断,如果两者同时存在,则抛出异常。
这里正是我们在《MyBatis核心配置讲解(上)》中提到的:
properties 元素的 resource 属性与 url 属性是互斥的。
现在你能够通过源码很清晰的看到它们为什么是互斥的了。
解析 settings 元素
解析 settings 元素相对比较复杂,需要调用 4 个方法完成:
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfsImpl(settings);
loadCustomLogImpl(settings);
settingsElement(settings);
首先是调用XMLConfigBuilder#settingsAsProperties 方法解析 settings 元素下的所有配置项,这里使用了MetaClass 检测 Configuration 中是否包含该配置项的 set 方法,该方法的源码如下:
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;
}
接着调用了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 语句的解析,所以这部分内容我们就留到下一篇文章中再来和大家详细的分析。
回顾与思考
到这里,我们已经从整体上了解了 MyBatis 应用程序初始化的流程,现在我们用一张图来总结下源码的执行流程,如下:
前前后后涉及到的 MyBatis 源码比较多,但最关键的也就是 SqlSessionFactoryBuilder 和 XMLConfigBuilder 这两个类了。
最后,我们来看一下通过上述的源码,我们能够借鉴哪些技巧并运用到我们日常的开发工作中。
职责分离
我们可以看到,TypeAliasRegistry 初始化数据是分别在多处进行的:
- TypeAliasRegistry 的构造方法中,只注册了通用别名;
- Configuration 的构造方法中,注册了与 MyBatis 核心配置文件相关的别名。
这种模块化职责分离的设计,保证了每个模块的“纯粹性”,每个模块负责处理各自的逻辑,保证了各个模块的功能不会耦合到一起。
另外,这种设计方式,也为自定义别名注册和实现别名配置的优先级提供了便利,用户可以在 MyBatis 核心配置文件中自定义别名,职责分离后 MyBatis 只需要在 Configuration 中实现加载自定义别名配置的逻辑即可,而无需在 TypeAliasRegistry 中实现解析 MyBatis 核心配置文件并加载别名配置;其次,TypeAliasRegistry 中初始化数据的顺序会早于 Configuration 中注册别名,这样也可以保证当我们需要配置与 MyBatis 中预置别名重名的自定义配置时,自定义配置可以覆盖掉 MyBatis 的预置配置。
建造者模式的应用
建造者模式,也叫做生成器模式,它将一个复杂对象的表示与构建过程分离,降低了对象构建过程与构建结果之间的耦合程度,当我们使用对象时,只需要关注对象所表示的内容,而无需关注构建细节。
通常运用得当的建造者模式会具有良好的扩展性,各个建造者之间相互独立,添加新的建造者不会对现有内容产生影响,这点也就是我们常说的对扩展是开放的,符合 SOLID 原则中的“开闭原则”,另外建造者模式也符合“单一职责原则”,建造者是独立于被建造对象的,只负责建造工作,而被建造的对象将更专注于对象的表示与逻辑。 建造者模式是 MyBatis 中运用的最广泛的设计模式之一,无论是 SqlSessionFactoryBuilder,还是 XMLConfigBuilder,以及未来还会见到的 XMLScriptBuilder 和 XMLStatementBuilder,都是建造者模式的应用。
不过 MyBatis 中只是对建造者模式的简单应用,只使用了建造者和产品(被建造的对象)这两个角色,而抛弃了建造者接口和导演(负责调度具体的建造者)这两个角色,这是因为在 MyBatis 中无论是 SqlSessionFactoryBuilder 还是 XMLConfigBuilder 都有明确的调用场景,不需要使用导演这一角色去调度具体的建造者,只需要在具体的场景中使用相应的建造者即可,这样可以避免过度设计,减少不必要的复杂性。