mybatis架构简介

Mybatis的架构可以大致分为以下三层:
1、API接口层:对外提供增删改查接口,使用传统的MyBatis提供的API或者使用Mapper代理的方式;
2、数据处理层:完成参数映射、SQL解析与执行、结果处理等;
3、框架支撑层:支持基于XML或者注解定义SQL、事务管理、连接池管理、缓存管理等;

img.png

Mybatis作为一个ORM框架,实际上底层还是通过JDBC来操作数据库,完成增删改查的。

想要详细了解底层的原理,首先需要知道Mybatis的九大核心的概念:

组件 描述
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 MappedStatement维护了一条<select | update | delete | insert>节点的封装
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息

Mybatis完成一次查询或者其他操作的大致流程如下图所示:

请求流程

层次结构图如下:

img.png

简单描述Mybatis执行过程如下:

  1. 加载核心配置文件,得到数据库连接信息,创建核心配置对象保存生成的连接池等信息;
  2. 加载映射配置文件,将namespace和SQL语句标签的id,组合作为id,最终封装成为一个MappedStatement对象,存放到内存中;
  3. 调用OpenSession方法获得会话,然后调用相应的增删改查方法,方法参数为namespace+id
  4. 接收到处理请求,由Executor的默认实现类BaseExecutor来处理,首先封装BoundSql对象,并根据分页等信息生成缓存的Key;
  5. 判断缓存中是否有,如果有则返回,如果没有进行数据库的查询;
  6. 进入SimpleExecutor中进行数据库的查询,调用StatementHandler来处理,而它先通过ParameterHandler进行参数设置,通过TypeHandler将java类型转换成为数据库类型;
  7. 最终调用JDBC的操作,完成查询,得到ResultSet
  8. 然后通过ResultSetHandler处理器将结果进行处理,最终返回指定的结果类型;

mybatis源码分析

首先,通过传统模式调用mybatis的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test() throws IOException {
//1.加载核心配置文件为字节流
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
//2.解析配置文件,并创建了sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//3.开启一个sqlSession会话,此处可以传参boolean类型,参数表示事务是否自动提交,目前是需要手动提交
SqlSession sqlSession = sqlSessionFactory.openSession();
//4.sqlSession调用方法,传入namespace+id
List<User> users = sqlSession.selectList("user.findAll");
for (User user : users) {
System.out.println(user);
}
// 最终关闭会话
sqlSession.close();
}

这种传统方式的缺点也很明显,namespace+id是硬编码,如果很多地方使用时,发生变化,则需要修改所有的地方。
针对这个问题,Mybatis还支持动态代理的方式,就是通过getMapper的方式,无需指定:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList= mapper.findAllUser();
for (User user : userList) {
System.out.println(user);
}
}

这种代理方式是如何实现的呢?
下面跟随源码进行查看,通过一步一步的进入,发现从SqlSession的实现类DefaultSqlSession一直到MapperProxyFactory最终找到具体的实现方式:

1
2
3
4
5
6
7
8
9
10
11
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}

//MapperProxyFactory类中的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 创建了JDK动态代理的invocationHandler接口的实现类mapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 调用了重载方法
return newInstance(mapperProxy);
}

到此处我们可以确定Mybatis是通过JDK动态代理的方式进行代理对象的产生。

同时,这里也可以解释为什么Mybatis规定Mapper的namespace要和mapper的全限定名一致,为什么SQL语句的id要与方法名一致。
因为,动态代理中只能获取到要执行哪个类下的哪个方法,无法获取xml里面的信息,所以让两者保持一致,可以直接定位到具体要执行的是哪个SQL。

通过JDK动态代理的参数可知,最终是由MapperProxy来进行的代理增强,进入其中查看invoke发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

所以最终通过method参数获取到要执行的是哪个SQL,最终交给mapperMethod完成执行,进行上面讲的步骤,最终得到结果。

插件原理与自定义

插件对mybatis来说就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的。

Mybatis对持久层的操作就是借助于四大核心对象,Executor、StatementHandler、ParameterHandler、ResultSetHandler,在上面分析源码时,会发现这四大核心对象都有拦截判断,我们就是通过拦截这四大对象来进行插件的开发。

进入org.apache.ibatis.session.Configuration核心配置类中,可以看到针对Executor的拦截器判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 获得执行器类型
executorType = executorType == null ? defaultExecutorType : executorType; // 使用默认
executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE
// 创建对应实现的 Executor 对象
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);
}
// 如果开启缓存,创建 CachingExecutor 对象,进行包装
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 此处是加载所有的针对Executor的插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

针对ParameterHandler的插件加载:

1
2
3
4
5
6
7
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 创建 ParameterHandler 对象
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 此处是加载所有的针对ParameterHandler的插件
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}

针对StatementHandler 插件加载:

1
2
3
4
5
6
7
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建 RoutingStatementHandler 对象
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 此处是加载所有的针对StatementHandler的插件
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

针对ResultSetHandler插件加载:

1
2
3
4
5
6
7
8
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
// 创建 DefaultResultSetHandler 对象
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
// 此处是加载所有的针对ResultSetHandler的插件
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}

通过上面的源码我们会发现,在这四大对象创建的时候会进行插件的加载,而且加载的方法是相同的,都是interceptorChain.pluginAll(),所以我们进入该方法,查看具体的加载逻辑:

1
2
3
4
5
6
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

在深入进入会发现如下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Object wrap(Object target, Interceptor interceptor) {
// 获得拦截的方法映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获得目标类的类型
Class<?> type = target.getClass();
// 获得目标类的接口集合
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 若有接口,则创建目标对象的 JDK Proxy 对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
}
// 如果没有,则返回原始的目标对象
return target;
}

通过上面的源码,我们发现最终拦截器或者插件的实现原理就是通过JDK动态代理来实现的。

interceptorChain保存了所有的拦截器(interceptors),在Mybatis初始化的时候创建。
当创建四大对象时,调用拦截器链中的拦截器依次的对目标进行拦截或增强。
interceptor.plugin(target)中的target就可以理解为要被拦截的四大对象。返回的target就是被重重代理后的对象。

这时候如果我们想要自定义一个插件,只需要实现Mybatis的Interceptor接口,重写其中的方法就行了,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Intercepts({
@Signature(type= StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class})
})
public class MyPlugin implements Interceptor {
// 只要被拦截的目标对象的目标方法被执行时,每次都会执行intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("对方法进行了增强....");
// 增强逻辑执行完毕后,原方法执行
return invocation.proceed();
}

// 主要为了把当前的拦截器生成代理存到拦截器链中
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);
return wrap;
}

// 获取配置文件的参数
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件的参数是:"+properties);
}
}

通过@Intercepts@Signature注解,可以指定自定义的拦截器(插件)是在哪个类的哪个方法执行时进行拦截,可以定义多个@Signature,同时对多个方法进行拦截。

当然,插件开发完毕后,也不是直接就会生效,还需要将其配置到Mybatis的核心配置文件中:

1
2
3
<plugins>
<plugin interceptor="com.jfl.test.plugin.MyPlugin"></plugin>
</plugins>

配置之后,在Mybatis启动时就会将这个插件放入interceptorChain中,最终生成代理对象完成增强的方法的执行。