1.2万字深度解析MyBatis源码(完结)
1.2 万字深度解析 MyBatis 源码(完结)
昨天有球友问 MyBatis 源
例
入
手,
接着
就
是一
步一
步
的剥
开
码
方
面的
学
?
习
资
料,
刚好
之
前整
理过
,
这里
我们
就
从
MyBatis 的源,配合大量的高清无,
一
个简
单
案
码图,。 发车? 。
MyBatis 使用案例
添加 MyBatis 和 MySQL 相关 pom 依赖。
<!-- MyBatis 依赖 -->
<dependency>
<groupId>org.MyBatis</groupId>
<artifactId>MyBatis</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
<scope>runtime</scope>
</dependency>
本地创建数据库,创建一张表。同时可以初始化几条数据,方便后面 debug。
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT
NULL,
`pwd` varchar(255) DEFAULT NULL,
`gender` int DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
实体类(entity):
public class User {
private Integer id;
private String userName;
private Integer age;
private Integer gender;
//省略.....
}
创建 UserMapper.java 接口:
public interface UserMapper {
User selectById(Integer id);
}
创建 UserMapper.xml 配置:
<?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="com.tian.MyBatis.mapper.UserMapper">
<resultMap id="User" type="com.tian.MyBatis.entity.User">
<id column="id" property="id"/>
<result column="name" property="userName"/>
</resultMap>
<select id="selectById" resultMap="User">
select * from t_user
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
</mapper>
创建 MyBatis-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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/tian?
useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
使用案例:
public class MyBatisApplication {
public static final String URL = "jdbc:mysql://localhost:3306/mblog?
useUnicode=true";
public static final String USER = "root";
public static final String PASSWORD = "123456";
public static void main(String[] args) {
String resource = "MyBatis-config.xml";
InputStream inputStream = null;
SqlSession sqlSession = null;
try {
//读取 MyBatis-config.xml
inputStream = Resources.getResourceAsStream(resource);
//解析 MyBatis-config.xml 配置文件,创建 sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
//创建 sqlSession
sqlSession = sqlSessionFactory.openSession();
//创建 userMapper 对象(UserMapper 并没有实现类)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用 userMapper 对象的方法
User user = userMapper.selectById(1);
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
sqlSession.close();
}
}
}
运行输出:
User{id=1, userName='沉默王二', age=23, gender=1}
就这么简单的搞定了,下面我们来详细的分析这个案例背后的设计原理。
猜想 MyBatis 是如何设计的
从上面的案例中,我们可以大致可以猜测到 MyBatis 一共做了哪些步骤。
1.定位到 MyBatis-config.xml 并读取装载。获取输入流 InputStream。
2.解析输入流 InputStream,把 MyBatis-config.xml 配置文件中相关配置项解析,校验,保
存起来。
3.创建 sqlSessionFactory 对象,在我们的印象里,session 就是一次会话,所以我们可以
理解 sqlSessionFactory 就是个工厂类,就专门创建 sqlSession 对象,并且这个
sqlSessionFactory 工厂类是唯一不变的(单例)。
4.创建 sqlSession,SqlSession 中肯定保存了配置文件内容信息和执行数据库相关的操作。
5.获取 userMapper 对象,但是 UserMapper 是接口,并且没有实现类。怎么就可以调用
其方法呢?这里猜想可能用到了动态代理。
6.userMapper 接口中的方法是如何关联到 SQL 的,这个猜想可能是有个专门映射的类,
另外,肯定使用到了接口全路径名+方法名称,这个才能确保方法和 SQL 关联(主要是使
用的时候,都是方法名必须和 SQL 中 statementId 一致,由此猜想的)。
7.最后底层使用 JDBC 去操作数据库。
8.作为一个持久化框架,很有可能会使用到缓存,用来存储每次查询数据。
面试中遇到,让你来设计一个 MyBatis 如何设计?
配置文件解析
因为第一步是读取 MyBatis-config.xml 配置文件,这里我们就没必要阅读这部分源码了,
这里得到是 InputStream 输入流。接下来就是基于这个输入流进行一系列牛逼的操作。
我们将从下面这行代码开始:
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
SqlSessionFactory 没有构造方法,那么这里使用的就是默认无参构造方法,所以我们直接
进去 build 方法。
//这个方法啥也没干
public SqlSessionFactory build(InputStream inputStream) {
//调用的是另外一个 build 方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment,
Properties properties) {
try {
//创建一个 XMLConfigBuilder 对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream,
environment, properties);
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) {
return new DefaultSqlSessionFactory(config);
}
public SqlSessionFactory build(Reader reader, String environment, Properties
properties) {
try {
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.
}
}
}
该类中的 build 重载的方法如下:
5236024a-e978-4508-b6c7-d4cbd9ea42a4.png
SqlSessionFactory 中提供了三种读取配置信息的方法后:字节流、字符流和
Configuration 配置类。
创建 XMLConfigBuilder 对象,这个类是 BaseBuilder 的子类,BaseBuilder 类图:
792bd358-3362-4aa5-9731-038ba59aeca9.png
看到这些子类基本上都是以 Builder 结尾,所以这里使用的是建造者设计模式。
这个类名可以猜出给类就是解析 xml 配置文件的。然后我们继续进入
public XMLConfigBuilder(InputStream inputStream, String environment,
Properties props) {
this(new XPathParser(inputStream,...);
}
MyBatis 对应解析包 org.apache.ibatis.parsing:
07c49c58-1893-4b8d-a56d-1c9c7cc22d59.png
XPathParser 基于 Java XPath 解析器,用于解析 MyBatis 中
• MyBatis-config.xml
• mapper.xml
等 XML 配置文件 。
XPathParser 主要内容:
dbdee088-13f9-4824-84ff-f20b10c85bd0.png
继续上面的源码分析:
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;
}
构造一个 XMLConfigBuilder 对象,给属性设置相应值。
然后我们再回到 SqlSessionFactoryBuilder 中的 build 方法里:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment,
properties);
build(parser.parse());
先看 parser.parse()方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//MyBatis-config.xml 的一级标签
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
继续 parseConfiguration()方法:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
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);
}
}
结合 MyBatis-config.xml 配置文件和解析方法,可以得出如何关联:
cc3935ee-c6dd-495c-b675-0fc21d20eb6c.png
MyBatis-config.xml 中如果把标签当做一级标签,那么有多少个二级标签可与定义呢?
在 org.apache.ibatis.builder.xml 下的 MyBatis-3-config.dtd 中已经定义了
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?,
objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?,
environments?, databaseIdProvider?, mappers?)>
与之对应的 MyBatis-config.xsd 中
<xs:element name="configuration">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" ref="properties"/>
<xs:element minOccurs="0" ref="settings"/>
<xs:element minOccurs="0" ref="typeAliases"/>
<xs:element minOccurs="0" ref="typeHandlers"/>
<xs:element minOccurs="0" ref="objectFactory"/>
<xs:element minOccurs="0" ref="objectWrapperFactory"/>
<xs:element minOccurs="0" ref="reflectorFactory"/>
<xs:element minOccurs="0" ref="plugins"/>
<xs:element minOccurs="0" ref="environments"/>
<xs:element minOccurs="0" ref="databaseIdProvider"/>
<xs:element minOccurs="0" ref="mappers"/>
</xs:sequence>
</xs:complexType>
</xs:element>
同理,我们最关心的 Mapper.xml 中能定义哪些标签,也在 MyBatis-3-mapper.dtd 中也定
义了,另外也有与之对应的
MyBatis-mapper.xsd 文件中也能找到:
e9d8ff5d-c42c-4f6c-bc92-5a49e3481902.png
关于 MyBatis 中标签相关就介绍到此。关于 MyBatis 配置文件中有哪些标签现在是不是觉
得很轻松就能找到了?其实在 IDEA 中也会提示的。
下面我们来看看这些标签内容是如何存入 configuration 对象中?(这里例举部分,挑几
个相对重要的)。
propertiesElement()方法:
5dd7b818-5ff6-4b34-8c55-95fa89cd8d08.png
typeAliasesElement()方法:
67e1a50e-e661-4b92-a224-d2cb4edb4a02.png
插件 plugins 解析
pluginElement()方法:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//可以定义多个插件
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor)
resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
Configuration 中 interceptorChain 用来存储所有定义的插件。
//interceptorChain 中有个 List<Interceptor> interceptors
protected final InterceptorChain interceptorChain = new InterceptorChain();
//存入 interceptors 中
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
InterceptorChain 插件链(连接链),责任链模式。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
}
我们继续看看 Mapper.xml 是如何解析的。
mapper.xml 解析
我们的 Mapper.xml 在 MyBatis-config.xml 中的配置是这样的:
f6750035-1bc9-4eae-9e12-ac902bf0754c.png
使用方式有以下四种:
<--! 1 使用类路径 -->
<mappers>
<mapper resource="org/MyBatis/builder/AuthorMapper.xml"/>
<mapper resource="org/MyBatis/builder/BlogMapper.xml"/>
<mapper resource="org/MyBatis/builder/PostMapper.xml"/>
</mappers>
<--! 2 使用绝对 url 路径 -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<--! 3 使用 java 类名 -->
<mappers>
<mapper class="org.MyBatis.builder.AuthorMapper"/>
<mapper class="org.MyBatis.builder.BlogMapper"/>
<mapper class="org.MyBatis.builder.PostMapper"/>
</mappers>
<--! 4 自动扫描包下所有映射器 -->
<mappers>
<package name="org.MyBatis.builder"/>
</mappers>
源码分析:
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 中
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//使用 java 类名
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
//根据文件存放目录,读取 XxxMapper.xml
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用 XMLMapperBuilder
//注意在 for 循环里每个 mapper 都重新 new 一个 XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new
XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
mapperParser.parse();
//使用绝对 url 路径
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比较复杂,调用 XMLMapperBuilder
XMLMapperBuilder mapperParser = new
XMLMapperBuilder(inputStream, configuration, url,
configuration.getSqlFragments());
mapperParser.parse();
//使用类路径
} 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.");
}
}
}
}
}
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
private XMLMapperBuilder(.....) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
把这些 UserMapper 类似接口保存到 configuration 对象中。
configuration.addMapper(mapperInterface);
到这里,配置文件 MyBatis-config.xml 和我们定义映射文件 XxxMapper.xml 就全部解析完
成。
关于其他配置项,解析方式类似,最终都保存到了一个 Configuration 大对象中。
Configuration 对象类似于单例模式,就是整个 MyBatis 中只有一个 Configuration 对象。
回到 SqlSessionFactoryBuilder 类
前面讲到了 XMLConfigBuilder 中的 parse 方法,并返回了一个 Configuration 对象。
build(parser.parse());
这个 build 方法就是传入一个 Configuration 对象,然后构建一个 DefaultSqlSession 对
象。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
继续回到我们的 demo 代码中这一行代码里。
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
这一行代码就相当于:
SqlSessionFactory sqlSessionFactory = new new DefaultSqlSessionFactory();
到此。配置文件解析完毕。
配置文件解析流程
90ee49ba-5977-4e93-ae4d-2440eaa25cb8.png
既然已经获取到了 SqlSessionFactory,那么我们就可以构建 SqlSession 了。下面我们来看
看构建 SqlSession 的整个过程。
构建 SqlSession
前面已经做了配置文件的解析,那么现在我们来构建 SqlSession。
sqlSession = sqlSessionFactory.openSession();
前面已经分析了,这里的 sqlSessionFactory 是 DefaultSqlSessionFactory。那么此时调用
的 openSession()方法为 DefaultSqlSessionFactory 中的方法。
public class DefaultSqlSessionFactory implements SqlSessionFactory {
//配置文件所有内容
private final Configuration configuration;
//创建 session
@Override
public SqlSession openSession() {
//调用的是另外一个 openSessionFromDataSource 方法
return openSessionFromDataSource(configuration.getDefaultExecutorType(),
null, false);
}
//其实是调用这个方法
//protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
private SqlSession openSessionFromDataSource(ExecutorType execType,
TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//对应 xml 标签<environments> ,这个在配置文件解析的时候就已经存放到
configuration 中了。
final Environment environment = configuration.getEnvironment();
//构建事务工厂
final TransactionFactory transactionFactory =
getTransactionFactoryFromEnvironment(environment);
//构建一个失误对象对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level,
autoCommit);
//创建一个 executor 来执行 SQL
final Executor executor = configuration.newExecutor(tx, execType);
//创建一个 DefaultSqlSession 对象并返回
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e,
e);
} finally {
ErrorContext.instance().reset();
}
}
private TransactionFactory
getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
return environment.getTransactionFactory();
}
创建事务 Transaction
Transaction 类图:
5957bd44-d29e-4528-bb8a-54d0904aebba.png
事务工厂类型可以配置为 JDBC 类型或者 MANAGED 类型。
• JdbcTransactionFactory 生产 JdbcTransaction。
• ManagedTransactionFactory 生产 ManagedTransaction。
如果配置的 JDBC,则会使用 Connection 对象的 commit()、rollback()、close()方法来管
理事务。
如果我们配置的是 MANAGED,会把事务交给容器来管理,比如 JBOSS,Weblogic。因为我
们是本地跑的程序,如果配置成 MANAGED 就会不有任何事务。
但是,如果是 Spring+MyBatis,则没有必要配置,因为我们会直接在
applicationContext.xml 里配置数据源和事务管理器,从而覆盖 MyBatis 的配置。
把事务传给 newExecutor()方法创建执行器 Executor 对象。
configuration.newExecutor(tx, execType)
创建 Executor
调用 configuration 的 newExecutor 方法创建 Executor。
final Executor executor = configuration.newExecutor(tx, execType);
//Configuration 中
public Executor newExecutor(Transaction transaction, ExecutorType
executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//第一步
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//第二步
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//第三步
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
此方法分三个步骤。
第一步:创建执行器
Executor 的基本类型有三种:SIMPLE 为默认类型。
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
Executor 类图:
1d5193eb-f1cc-44ea-9f4d-b735a92d4fc8.png
为什么要让抽象类 BaseExecutor 实现 Executor 接口,然后让具体实现类继承抽象类呢?
这就是模板方法模式的实现。
模板方法模式就是定义一个算法骨架,并允许子类为一个或者多个步骤提供实现。模板方
法是得子类可以再不改变算法结构的情况下,重新定义算法的某些步骤。
抽象方法是在子类汇总实现的,每种执行器自己实现自己的逻辑,BaseExecutor 最终会
调用到具体的子类。
抽象方法
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object
parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql
boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms,
Object parameter, RowBounds rowBounds, BoundSql boundSql) throws
SQLException;
第二步:缓存装饰
在上面代码中的第二步
if (cacheEnabled) {
executor = new CachingExecut