前面三篇Blog详细的对MyBatis从使用层面上进行了实践,包括Mybatis的整体配置、基本操作:CURD以及模糊查询like语句,以及高级的结果集映射、分页查询和日志输出查看。那么使用到现在我觉得大家都会像我一样好奇,Mybatis的实现原理机制到底是什么样的,我们知道一个MyBatis方法执行要有这么几个步骤,拿update
操作举例:
@Test
public void testUpdatePerson() {
//1.获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//2.获取对应的DAO接口
PersonDao personDao = sqlSession.getMapper(PersonDao.class);
Person person=new Person();
person.setUsername("lisi");
person.setId(0);
person.setPhone(133154637);
//3.执行对应的接口方法
int result = personDao.updatePerson(person);
System.out.println(result);
//4.提交事务
sqlSession.commit(); //提交事务,重点!不写的话不会提交到数据库
//5.关闭sqlSession
sqlSession.close();
}
其中关键的三个步骤也是我们比较会产生疑问的步骤是:
- 获取SqlSession对象这一步,我们是如何从配置文件中获取到一个SqlSession会话对象
- 获取对应Dao接口(Mapper对象)这一步,我们是如何获取到一个Mapper对象的
- 执行对应的接口方法这一步,为什么我调用mapper对象方法就能执行sql语句操作数据库
要想知道这些,我们需要对MyBatis整体的运行机制和实现原理有个较为深入的认知。
MyBatis基本执行链路
结论先行,我们先对MyBatis的整体执行过程有个大致的理解,然后再对上述的三个执行步骤产生的问题进行一一解答,整体的全链路执行MyBatis需要如下几个核心部件,可以看到其实MyBatis是基于JDBC做的封装框架。
这几个核心部件的主要功能如下:
- SqlSession,作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
- Executor ,MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler,封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合
- ParameterHandler,负责对用户传递的参数转换成JDBC Statement 所需要的参数
- ResultSetHandler , 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
- TypeHandler ,负责java数据类型和jdbc数据类型之间的映射和转换
- MappedStatement,负责生成一个Mapper对象,维护了一条<select|update|delete|insert>节点的封装,Mapper配置文件中的节点和操作。
- SqlSource,负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
- ResultMap ,负责对结果集进行映射
- Configuration,MyBatis所有的配置信息都维持在Configuration对象之中。
整体的去看可以大致进行职责界定:SqlSession为我们开启一个连接会话,这样才能开始后续操作,MappedStatement是我们编写的Mapper配置文件的读取,获取我们在配置文件中配置的各种内容(方法、参数、结果映射)等,StatementHandler则帮助我们
SqlSession对象构建机制
回顾我们的SqlSession对象构建过程,从我们最初编写的util类就可以看出构建的步骤:
package com.example.MyBatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MybatisUtils {
static SqlSessionFactory sqlSessionFactory = null;
static {
try {
//1,从Resources加载全局配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//2,实例化SqlSessionFactoryBuilder构建器
//3.通过XMLConfigBuilder解析配置的XML文件,读出配置参数,包括基础配置XML文件和映射器XML文件, 生成Configuration对象
//4.使用Configuration对象创建SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession(){
//5.SqlSessionFactory是一个接口,提供了一个默认的实现类DefaultSqlSessionFactory
//6.DefaultSqlSessionFactory创建事务对象Transaction
//7.DefaultSqlSessionFactory创建执行器Executor
//8.DefaultSqlSessionFactory构造返回一个DefaultSqlSession
return sqlSessionFactory.openSession();
}
}
1 构建SqlSessionFactory
其中第2-4步的核心源码如下:
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
调用SqlSessionFactoryBuilder的其中一个重载方法
public SqlSessionFactory build(InputStream inputStream) {
return this.build((InputStream)inputStream, (String)null, (Properties)null);
}
调用另一个重载方法,依据此配置构造一个sqlSessionFactory
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
2 获取一个SqlSession
第5-8步的核心源码如下:
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
分别确定环境、创建事务、构造核心的Executor,最后生成一次会话SqlSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
Mapper对象获取机制
解决了SqlSession的获取问题,我们就需要搞明白Mapper对象的获取机制,也就是对应这样一行简单的代码的Mapper对象是怎么来的:
PersonDao personDao = sqlSession.getMapper(PersonDao.class);
在DefaultSqlSession
中我们可以看到Mapper的获取方法:
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
通过调用DefaultSqlSession的getMapper方法并且传入一个类型对象获取,底层调用的是配置对象configuration的getMapper方法:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
configuration对象是我们在加载DefaultSqlSessionFactory时传入的。然后我们再来看下这个配置对象的getMapper
方法,传入的type是我们的PersonDao,以及一个构建好的sqlSession对象
1 Mapper注册器获取Mapper
在Mapper注册器MapperRegistry代码中实际我们可以看的出就是从一个HashMap中取Mapper,如果我们之前配置过,那么初始化的时候配置的类型就会被add进去,这里我们就可以从knownMappers中获取key为类型对象的MapperProxyFactory对象。
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
配置过的意思是:我们在核心配置文件中添加过:
<?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核心配置文件-->
<configuration>
<!--导入properties文件-->
<properties resource="properties/db.properties"/>
<settings>
<setting name="logImpl" value="log4j"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<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>
<mappers>
<mapper resource="mappers/personMapper.xml"/>
</mappers>
</configuration>
2 构造MapperProxy代理类
如果能获取到该类型MapperProxyFactory对象,我们就调用MapperProxyFactory对象的newInstance方法返回,newInstance方法传入sqlSession对象:
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
这里我们可以看到MapperProxyFactory直接new了一个MapperProxy对象,然后调用另一重载newInstance方法传入MapperProxy对象。通过调用Proxy.newProxyInstance动态代理了我们的mapperProxy对象!这里的mapperInterface即我们的dao层(持久层)接口的类型对象。
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
总结下就是我们通过sqlSesssion.getMapper(clazz)得到的Mapper对象是一个MapperProxy的代理类
Mapper对象方法执行机制
看来第二个问题也解决了,我们的Mapper对象是一个构建好的动态代理类。这里先不纠结动态代理技术,主线流程梳理完之后我们再来了解下什么是动态代理技术。整体执行流程如下:
1 执行对应的excute
我们获取到映射对象,也就是JDK生成的接口的动态代理类org.apache.ibatis.binding.MapperProxy
,然后执行insert就进入MapperProxy中代理执行了,JDK动态代理类实现了InvocationHandler接口,所以这个mapperProxy必然实现了InvocationHandler接口。所以当我们调用我们的持久层接口的方法时必然就会调用到这个MapperProxy
对象的invoke
方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
接着执行this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession)
,在cachedInvoker方法中
private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return (MapperProxy.MapperMethodInvoker)MapUtil.computeIfAbsent(this.methodCache, method, (m) -> {
if (m.isDefault()) {
try {
return privateLookupInMethod == null ? new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava8(method)) : new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava9(method));
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
throw new RuntimeException(var4);
}
} else {
return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
}
});
} catch (RuntimeException var4) {
Throwable cause = var4.getCause();
throw (Throwable)(cause == null ? var4 : cause);
}
}
调用了methodCache.computeIfAbsent
这个方法,这个是映射代理对象保存我们的查询方法缓存,如果第二次相同的方法,就会直接返回,我们看看底层Map的实现,如果没有就执行PlainMethodInvoker方法:
private static class PlainMethodInvoker implements MapperProxy.MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
this.mapperMethod = mapperMethod;
}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return this.mapperMethod.execute(sqlSession, args);
}
}
然后执行execute方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
一方面是解析参数,另一方面是进行执行并构造返回结果。
2 与JDBC接口交互
我们继续关注更新语句的执行过程:
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
查看更新语句是如何执行的,在DefaultSqlSession
类中:
public int update(String statement, Object parameter) {
int var4;
try {
this.dirty = true;
MappedStatement ms = this.configuration.getMappedStatement(statement);
var4 = this.executor.update(ms, this.wrapCollection(parameter));
} catch (Exception var8) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8);
} finally {
ErrorContext.instance().reset();
}
return var4;
}
在首先是获取了一个MappedStatement 对象,通过方法我们可以判断,就是通过我们配置文件的id确认方法是哪个
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
然后调用executor执行器进行执行,BaseExecutor中的方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
再来接着看下doUpdate方法做了什么,在simpleExecutor子类中查看:
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
int var6;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var6 = handler.update(stmt);
} finally {
this.closeStatement(stmt);
}
return var6;
}
我们可以看到通过configuration对象的newStatementHandler方法构建了一个StatementHandler,然后在调用prepareStatement方法中获取连接对象,通过StatementHandler得到Statement对象。而这个Statement对象就到了JDBC的范畴了:
继续向下查看update语句, ps.execute()这个语句就是对JDBC的使用了:
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement)statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = this.boundSql.getParameterObject();
KeyGenerator keyGenerator = this.mappedStatement.getKeyGenerator();
keyGenerator.processAfter(this.executor, this.mappedStatement, ps, parameterObject);
return rows;
}
这个语句我们熟悉的很,就是JDBC的执行语句
动态代理技术
行文至此,我们已经对Mybatis的执行原理有了一个较为深入的理解了,接下里还剩下一个问题,就是上文我们提到的动态代理技术,动态代理是什么,为什么通过动态代理我们能在运行时获取代理类,并执行代理类里的逻辑。
动态代理基本概念
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作【有点像触发器的概念】
代理类在程序运行期间,创建的代理对象称之为动态代理对象。这种情况下,创建的代理对象,并不是事先在Java代码中定义好的。而是在运行期间,根据我们在动态代理对象中的“指示”,动态生成的。也就是说,你想获取哪个对象的代理,动态代理就会为你动态的生成这个对象的代理对象。动态代理可以对被代理对象的方法进行功能增强。有了动态代理的技术,那么就可以在不修改方法源码的情况下,增强被代理对象的方法的功能,在方法执行前后做任何你想做的事情
动态代理实现方式
一个完整的代理实现如下,包含三个内容,代理接口,代理接口实现类以及定义动态代理的接口调用处理器:
public interface Subject { // 1 定义代理接口
String sayHello();
}
public class SubjectImpl implements Subject { // 2 定义代理接口实现类
@Override
public String sayHello() {
System.out.println(" Hello World");
return "success";
}
}
public class ProxyInvocationHandler implements InvocationHandler { // 3 定义动态代理调用处理器
private Object target;
public ProxyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(" 进入代理调用处理器 ");
return method.invoke(target, args);
}
}
测试代码和测试结果如下:
public class ProxyTest {
public static void main(String[] args) {
Subject subject = new SubjectImpl();
Subject proxy = (Subject) Proxy
.newProxyInstance(
subject.getClass().getClassLoader(), //类加载器
subject.getClass().getInterfaces(), //代理的接口
new ProxyInvocationHandler(subject)); //动态代理处理逻辑
proxy.sayHello();
/**
* 打印输出如下
* 调用处理器:进入代理调用处理器
* 被代理实现类:Hello World
*/
}
}
问题来了,为什么 Mybatis Mapper 接口没有实现类也可以实现动态代理?这样也是可以实现的:
public interface Subject {
String sayHello();
}
public class ProxyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(" 进入代理调用处理器 ");
return "success";
}
}
测试实现及打印结果:
public class ProxyTest {
public static void main(String[] args) {
Subject proxy = (Subject) Proxy
.newProxyInstance(
subject.getClass().getClassLoader(),
new Class[]{Subject.class},
new ProxyInvocationHandler());
proxy.sayHello();
/**
* 打印输出如下
* 调用处理器:进入代理调用处理器
*/
}
}
可以看的出无实现类接口则是仅对 InvocationHandler#invoke 产生调用。所以有实现类接口返回的是被代理对象接口返回值,而无实现类接口返回的仅是 invoke 方法返回值。所以MyBatis实际上就是将方法的实际实现逻辑完全交给了代理。
Mybatis的动态代理
了解了基本概念后我们回过头来再看看我们的Mapper代理的实现方式:
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
这其实就是一个无实现类的动态代理,通过类型全限定名用反射去获取类型,然后在执行具体方法时,触发附加在该方法上的代理去执行,又因为该方法无实际执行内容,所以实现的逻辑其实是代理里的逻辑,这种编程模式有点像是面向接口编程,后续我们再讨论这种编程模式
总结一下
今天这篇长文耗时4h,以及零散时间看文章学习,总算对Mybatis的基本执行原理和源码有了个大致理解,可以这么说Mybatis就是基于动态代理技术和反射实现的,在具体定义方法执行时通过动态代理拦截并将执行逻辑层层封装直到调用JDBC接口后返回,这样就实现了SQL与代码的解耦,代码也没有复杂的具体实现,这种面向接口的编程模式后续可以继续探讨下。