第二章-Mybatis源码解析-以xml方式走流程
2.1 准备阶段
xml执行大概经过以下几个步骤:
1)xml配置资源位置
2)根据配置资源构建Environment(环境)、Configuration(配置信息)、TransactionFactory(事务工厂类)、typeAliases(类型别名)、typeHandlers(类型处理器)
3)生成SqlSessionFactory
4)创建SqlSession
5)包装语句(包括xml语句的解析)
6)处理类型
7)处理参数
8)执行语句(涉及缓存)
9)处理结果
10)包装结果并返回,结束流程
实际流程比上面的还是要复杂很多,但是Mybatis帮我们都封装的很好,实际使用时,就几行代码,如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
Blog blog = sqlSession.selectOne("polyphagic.code.mybatis.BlogMapper.selectBlog1", 1);
那我们结合流程和以上的几行代码来看看Mybatis到底做了什么。
String resource = “mybatis-config.xml”;
InputStream inputStream = Resources.getResourceAsStream(resource);
这两行代码的作用是根据地址信息,查找xml配置资源,并转化成输入流
引用官方文档的一句话: 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。
好,那我们看看 new SqlSessionFactoryBuilder().build(inputStream) 这个方法做了什么
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);
// 最终看这一行,先做xml解析,然后再build构建,继续往里面深挖
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.
}
}
}
XML配置信息解析
xml的解析过程读者可以去看看有关xml解析的文章,我这里就不讲xml如何解析xml文件和结构。解析完后,就能得到完整的Configuration对象,里面包括typeAliases、typeHandler、映射器mapper、全局属性、日志实现、插件、环境等内容,后续流程中都会用到这些。
public Configuration parse() {
if (parsed) { // 重复解析判断
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true; // 设置解析标志
// 从xml的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 settings = settingsAsProperties(root.evalNode("settings"));
/*
通过settings中的配置,拿到vfs的实现,并设置到configuration中
这个vfs网上很多文章都说是“虚拟文件系统”,其实这压根就不是什么文件系统,这个我们可以看VFS的类实现逻辑和提供的方法入口来看,实际上它只是用于更方便的访问应用程序服务器中的资源,比如查找类文件实现定义时,需要列出某个目录的地址列出所有文件来查找;判断某个文件是不是jar文件等等;其实把它理解成一个工具类就行,扯上“虚拟文件系统”这个概念就有点大了
*/
loadCustomVfs(settings);
/*
设置日志实现方式:SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
*/
loadCustomLogImpl(settings);
// 类型别名解析,解析后的类型别名都存放在类TypeAliasRegistry的typeAliases(HashMap)中,读者有兴趣的可以去看下这个类的,里面包括了8个基础类型及其数组和包装类和数组、日期类型、数字类型、Object类型、List、Map、HashMap、ArrayList等常用类型的另外定义,这就是为什么我们在xml写sql语句时,类型Map可以直接写,而不需要写成全限定名的原因。
typeAliasesElement(root.evalNode("typeAliases"));
// 解析自定义的插件设置,具体细节等实际做插件处理时再讲
pluginElement(root.evalNode("plugins"));
// 对象工厂解析:每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现,这块自定义实现,官方文档中有描述。
objectFactoryElement(root.evalNode("objectFactory"));
// 解析对象包装类工厂配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析反射器工厂配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 将settings中设置的内容都设置到 configuration 中,有兴趣的可以回头看官网对settings可设置的内容详细了解
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 像出现 issue #631 这种注释时,就表示是为了解决编号631的bug,这是业界规范描述
// 解析环境设置,MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
environmentsElement(root.evalNode("environments"));
// 解析数据库厂商标识配置
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析类型处理器配置:类型处理器也都是通过类TypeHandlerRegistry中的HashMap来维护的,里面有定义Mybatis帮我们预先设置的许多默认数据类型的类型处理器,读者可以打开TypeHandlerRegistry类源码看一下就知道,这块内容也可以自定义
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析映射器设置,映射器说白了,就是写sql的地方,Mybatis支持xml和注解形式,所以,这里可以配置两种形式,一是xml文件目录;二是Java接口类文件目录,Mybatis官网也有描述,可以自己去看,这一步还实现了对sql语句的进一步解析,这块可以看`章节2.2`
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
构建SqlSessionFactory
上面解析配置信息后,就要构建SqlSessionFactory了,通过类SqlSessionFactoryBuilder来构建,看如下代码
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config); // 实际上就是创建DefaultSqlSessionFactory实例
}
2.2 映射器配置解析
代码实现在XMLConfigBuilder.mapperElement方法中
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 将包内的映射器接口实现全部注册为映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 使用相对于类路径的资源引用
String resource = child.getStringAttribute("resource");
// 使用完全限定资源定位符(URL)
String url = child.getStringAttribute("url");
// 使用映射器接口实现类的完全限定类名
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// mapper文件内容解析,主要就是sql、参数map、结果map、缓存配置的解析,看`图2-1`,后续章节会对这块内容做细致的解析工作
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// mapper文件内容解析,主要就是sql、参数map、结果map、缓存配置的解析,看`图2-1`,后续章节会对这块内容做细致的解析工作
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 加载mapper接口类
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 添加到configuration的mapperRegistry注册器的knownMappers(HashMap)中,里面也涉及对mapper接口类的解析,主要应用在注解方式,所以留待后续讲解注解方式时再讲
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
图2-1
后续将通过抖音视频/直播的形式分享技术,由于前期要做一些准备和规划,预计2024年6月开始,欢迎关注,如有需要或问题咨询,也可直接抖音沟通交流。