mybatis源码分析-mybatis配置阶段的执行流程分析
hello world
看源码到底怎么去看,我总结的是要有目的的去看源码。例如mybatis,我们在github拉下来源码后发现一堆代码,没头没尾无从下手。但是如果我们先写一个Demo,来分析这个demo的每一步到底干了什么,就好分析了。这也就是为啥学习一门新技术都需要先学习怎么使用。
创建一个maven项目,引入mybatis的依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
项目目录结构
创建一个实体类和对应的mapper接口
/** 实体类 */
public class File {
private Integer id;
private Integer parentId;
private String name;
private Integer isFolder;
.... 省略get/set
}
/** mapper接口 */
public interface FileMapper {
List<File> selectList();
}
mapper.xml
一段很简单的sql语句,就是查询全部的file文件,我已经提前在数据库中插入了几条数据。数据库就不展示了,很简单就这几个字段。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.internal.example.mapper.FileMapper">
<select id="selectList" resultType="org.mybatis.internal.example.pojo.File">
select id,parent_id parentId, name, is_folder isFolder from file;
</select>
</mapper>
在resource文件夹下创建一个myabtis-config.xml
全局配置文件
<?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>
<properties>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</properties>
<!-- 配置别名 可以在mapper.xml中 使用简称就可以不用使用群全名了-->
<typeAliases>
<!-- 指定类设置别名-->
<!-- <typeAlias alias="user" type="org.mybatis.internal.example.pojo.User"/>-->
<!-- <typeAlias alias="str" type="java.lang.String"/>-->
<!-- 包扫面式的添加类别名,默认为类名首字母变为小写 如果个别的类需要指定别名可以使用 @Alias("us") 添加在实体类上-->
<package name="org.mybatis.internal.example.pojo"/>
</typeAliases>
<!-- 多数据源-->
<environments default="development2">
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 数据源模式 POOLED
dataSource的类型可以配置成其内置类型之一,如UNPOOLED、POOLED、JNDI。
如果将类型设置成UNPOOLED,mybaties会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。
如果将属性设置成POOLED,mybaties会创建一个数据库连接池,连接池的一个连接将会被用作数据库操作。一旦数据库操作完成,mybaties会将此连接返回给连接池。在开发或测试环境中经常用到此方式。
如果将类型设置成JNDI。mybaties会从在应用服务器向配置好的JNDI数据源DataSource获取数据库连接。在生产环境中优先考虑这种方式。
-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<environment id="development2">
<transactionManager type="JDBC"/>
<!-- 数据源模式 POOLED
dataSource的类型可以配置成其内置类型之一,如UNPOOLED、POOLED、JNDI。
如果将类型设置成UNPOOLED,mybaties会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。
如果将属性设置成POOLED,mybaties会创建一个数据库连接池,连接池的一个连接将会被用作数据库操作。一旦数据库操作完成,mybaties会将此连接返回给连接池。在开发或测试环境中经常用到此方式。
如果将类型设置成JNDI。mybaties会从在应用服务器向配置好的JNDI数据源DataSource获取数据库连接。在生产环境中优先考虑这种方式。
-->
<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>
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
<!-- 第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,-->
<!-- <mappers>-->
<!-- <mapper resource="mapper\UserMapper.xml"/>-->
<!-- <mapper resource="mapper\FileMapper.xml"/>-->
<!-- </mappers>-->
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.internal.example.mapper"/>
</mappers>
<!-- 另外一类是明确指定mapper,这又可以通过resource、url或者class进行细分。例如:-->
<!-- <mappers>-->
<!-- <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>-->
<!-- <mapper class="org.mybatis.builder.AuthorMapper"/>-->
<!-- <mapper url="file:///var/mappers/PostMapper.xml"/>-->
<!-- </mappers>-->
</configuration>
现在创建main方法继续测试
public class MybatisHelloWorld {
// 配置阶段
// 得到mybatis 配置类的文件路径
String resource = "mybatis/Configuration.xml";
Reader reader = null;
SqlSession session = null;
try{
Yaml yml = new Yaml();
LinkedHashMap map = yml.loadAs(MybatisHelloWorld.class.getClassLoader().getResourceAsStream("config.yml"), LinkedHashMap.class);
System.out.println(map);
Map dataSourceMap = (Map)map.get("dataSource");
System.out.println(dataSourceMap.get("username"));
// 通过Resources.getResourceAsReader 得到配置文件的字节流
reader = Resources.getResourceAsReader(resource);
// 这里的Properties 和配置文件Configuration.xml 里面的一致,使用Java配置可以实现动态的properties
Properties properties = new Properties();
// properties.setProperty("username", dataSourceMap.get("username") + "");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
// 执行SQL阶段
// 得到一个session连接
session = sqlSessionFactory.openSession();
FileMapper fileMaper = session.getMapper(FileMapper.class);
List<File> fileList = fileMaper.selectList();
list.forEach(System.out::println);
}catch (Exception e){
e.printStackTrace();
}finally {
session.commit();
session.close();
}
}
源码分析阶段
这段代码中核心的代码只有四句代码,这四句我把整个mybatis 从配置到执行sql语句得到想要的结果 分为两个阶段:
配置阶段
执行阶段
// 配置阶段
// 创建sqlsession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
// 得到一个session连接
session = sqlSessionFactory.openSession();
// 执行阶段
// 获得mapper代理类
FileMapper fileMaper = session.getMapper(FileMapper.class);
// 执行查询方法
List<File> fileList = fileMaper.selectList();
我们一个一个的分析每个阶段的源码,首先是配置阶段
,这个阶段我们首先要提出问题。
- SqlSessionFactory得到build需要将xml配置文件,他拿到这配置文件做了什么?怎么做的?
// 创建sqlsession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
// 得到一个session连接
session = sqlSessionFactory.openSession();
我们点击 build(reader,properties) 方法里面看一看他到底做了什么,点击去我们发现他最终调用了这个build方法,在这个方法里面创建了一个XMLConfigBuilder对象
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 解析配置文件的关键逻辑都委托给XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
解析配置文件的关键逻辑都委托给XMLConfigBuilder,我们进入这个类一探究竟。这个类中最重要的三个方法都在这里
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
/**
* 外部调用此方法对mybatis配置文件进行解析
* 第四步 真正Configuration构建逻辑就在XMLConfigBuilder.parse()里面
*/
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//mybatis配置文件解析的主流程
//从根节点configuration
// 返回根节点 parser.evalNode("/configuration")
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//此方法就是解析configuration节点下的子节点
//由此也可看出,我们在configuration下面能配置的节点为以下11个节点
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 这里是按照官网的配置文件顺序进行解析的 https://mybatis.org/mybatis-3/zh/configuration.html
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们发现在parseConfiguration()方法中对xml配置文件进行了解析,此方法就是解析configuration节点下的子节点,解析的顺序是按照配置文件的配置顺序依次进行的解析。其中最重要得到的解析,这里就是对mapper的解析处理,我们进去方法mapperElement看一看是怎么实现的。
/**
* 加载mapper文件mapperElement
* mapper文件是mybatis框架的核心之处,所有的用户sql语句都编写在mapper文件中,所以理解mapper文件对于所有的开发人员来说都是必备的要求
* @param parent
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果要同时使用package自动扫描和通过mapper明确指定要加载的mapper,一定要确保package自动扫描的范围不包含明确指定的mapper,
// 否则在通过package扫描的interface的时候,尝试加载对应xml文件的loadXmlResource()的逻辑中出现判重出错,报org.apache.ibatis.binding.BindingException异常,
// 即使xml文件中包含的内容和mapper接口中包含的语句不重复也会出错,包括加载mapper接口时自动加载的xml mapper也一样会出错。
if ("package".equals(child.getName())) {
// <package name="org.mybatis.internal.example.mapper"/>
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// mapperParser.parse()方法就是XMLMapperBuilder对Mapper映射器文件进行解析
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
// <mapper url="file:///var/mappers/PostMapper.xml"/>
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
// 解析映射配置文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
// <mapper class="org.mybatis.builder.AuthorMapper"/>
// 反射加载对象
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.");
}
}
}
}
}
这段代码长一点,我们把它分一下类,从代码上我们可以看出,他按照child.getName()
的不同分别进行了处理
一共四种情况,也就是mybatis.xml配置文件四种不同标签加载mapper的方法。虽然分为了四种情况但是他最终执行的代码是相同的,我们只看package
包扫描的方式,因为这种方式用的比较多。
/**
* <mappers>
* <package name="org.mybatis.internal.example.mapper"/>
* </mappers>
*/
if ("package".equals(child.getName())) {
// 包名:org.mybatis.internal.example.mapper
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
}
/**
* <mappers>
* <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
* </mappers>
*/
if (resource != null && url == null && mapperClass == null) {
...
}
/**
* <mappers>
* <mapper class="org.mybatis.builder.AuthorMapper"/>
* </mappers>
*/
if (resource == null && url == null && mapperClass != null) {
...
}
/**
* <mappers>
* <mapper url="file:///var/mappers/PostMapper.xml"/>
* </mappers>
*/
if (resource == null && url != null && mapperClass == null) {
...
}
我们看一下包扫描这种方法到底干了什么?
点击这个方法 configuration.addMappers(“mapper接口的包名”),我就康康不乱动!(●ˇ∀ˇ●)
点击config的addMappers方法发现又是老样子,他又交给了别人处理了
mapperRegistry.addMappers(packageName);
config是调用了mapperRegistry的addMappers方法,好吧!我进入的深一点,我再跟进去。
public void addMappers(String packageName, Class<?> superType) {
// mybatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或者继承于某个类/接口)的类,默认使用Thread.currentThread().getContextClassLoader()返回的加载器,和spring的工具类殊途同归。
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 无条件的加载所有的类,因为调用方传递了Object.class作为父类,这也给以后的指定mapper接口预留了余地
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 所有匹配的calss都被存储在ResolverUtil.matches字段中
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
//调用addMapper方法进行具体的mapper类/接口解析
addMapper(mapperClass);
}
}
当我进入这个方法的时候发现,哎,这里的代码多肯定是这里没错了。我们看看这里做了什么操作。
首先得到了一个包搜索工具 resolverUtil
通过这个工具拿到了这个包下所有的mapper接口的class对象mapperSet,是一个set集合,然后遍历集合,他又调用了 addMappers方法,这次传递的参数是每个mapper接口的class对象。没办法再进去看看
public <T> void addMapper(Class<T> type) {
// 对于mybatis mapper接口文件,必须是interface,不能是class,因为mybatis用的是jdk动态代理
if (type.isInterface()) {
// 判重,确保只会加载一次不会被覆盖
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 生成一个MapperProxyFactory,用于之后生成动态代理类
// 为mapper接口创建一个MapperProxyFactory代理工厂,将mapper接口与工厂建立关系,当使用这个mapper的代理类时再去生产一个代理类
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// MapperAnnotationBuilder进行具体的解析
//以下代码片段用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
//剔除解析出现异常的接口
knownMappers.remove(type);
}
}
}
}
这片代码写了啥,一堆的判断… 嗯就一两句我们想看到的
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
第一个 让我们终于知道了,我们那些mapper接口到底去哪里了,原来在这个他为每个mapper接口创建了一一对应的MapperProxyFactory代理工厂,并将这些工厂存放到了 map对象中。
-
key: mapperClass
-
value:MapperProxyFactory<>(mapperClass)
第二个 用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况。这里不在深入,谁想看自己去看。
最后还执行了一句解析 parser.parse()
,这个解析到底在解析什么,我们在~~~~点击去看一下。
这里东西有点多,看一下的我的源码片段,这里我都进行了注释。
/**
* MapperBuilderAssistant初始化完成之后,就调用build.parse()进行具体的mapper接口文件加载与解析
*/
public void parse() {
String resource = type.toString();
//首先根据mapper接口的字符串表示判断是否已经加载,避免重复加载,正常情况下应该都没有加载
if (!configuration.isResourceLoaded(resource)) {
//⭐⭐⭐⭐⭐ 加载Mapper.xml资源,这里面就是解析mapper.xml的方法
loadXmlResource();
configuration.addLoadedResource(resource);
// 命名空间 每个mapper文件自成一个namespace,通常自动匹配就是这么来的,约定俗成代替人工设置最简化常见的开发
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
loadXmlResource这个方法将mapper.xml进行了解析,具体怎么解析自己看。
private void loadXmlResource() {
// 判断资源是否已经加载过
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 得到xml的相对路径 例如:com.xx.mapper.UserMapper.xml 注意这是指在resources 问价夹下的资源目录
// type值得是mapper接口文件的信息
// type.getName() = com.xx.mapper.UserMapper
// 他会解析成 com/xx/mapper/UserMapper.xml
String xmlResource = type.getName().replace('.', '/') + ".xml";
// 加载xml 的文件流
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
try {
// ClassLoader.getSystemClassLoader(); 通过类加载器加载
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
}
}
if (inputStream != null) {
// 调用XMLMapperBuilder 解析mapper.xml文件
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
到现在我们知道了mybatis配置阶段到底执行了那东西。
// ClassLoader.getSystemClassLoader(); 通过类加载器加载
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
}
}
if (inputStream != null) {
// 调用XMLMapperBuilder 解析mapper.xml文件
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
到现在我们知道了mybatis配置阶段到底执行了那东西。
接下来就是sql执行阶段了,欲知后事如何,且听下次讲解