本系列文章皆在从细节 着手,由浅入深的分析Mybatis
框架内部的处理逻辑,带你从一个全新的角度来认识Mybatis
的工作原理。
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
前言
在上一章Mybatis流程分析(五): sql语句与接口中方法绑定的"细节"中,我们详细介绍了Mapper.xml
中配置的sql
语句与接口
中方法绑定的具体细节。具体来看,Mybatis
中会将配置文件中的sql
的语句封装进一个名为MappedStatement
对象,然后放入到的Configuration
中的一个Map
结构中,其中key
为namespace+statementId
的形式,value
为一个MappedStatement
对象。事实上,理解sql
与、接口
中方法绑定的具体细节是理解本章内容的关键。
(注:不熟悉Mybatis
中sql
语句和接口
方法绑定细节的小伙伴
可翻阅专栏之前的文章进行了解~~)
本章我们将主要介绍在Mybatis
中为什么操纵口实例对象方法就可以完成对数据库操纵。即分析如图所示的<3>执行接口方法,就能执行方法所绑定的sql语句
的背后逻辑。
在开始分析之前,我们先来看看动态代理
相关的知识。此时,可能你可能会疑惑,我想知道的是Mybatis
内部为什么通过调用接口中的方法,就能完成对数据库操作的,怎么现在又开始分析动态代理
了?
如果你有这样的疑惑,先别急,等我慢慢来给你分析。
我们在Mybatis流程分析(四):Mybatis构建Mapper背后的故事中曾经强调过Mybaits
内部之所以能根据传入的接口
,返回一个实现该接口
的对象的原理就在于动态代理
。所以为了读者能更好的理解后续的内容,此处就有必要对Java
中的动态代理进行一个简短的介绍。
动态代理
代理模式
主要用于完成低侵⼊式的功能的扩展。进一步,实现代理
的方式又可以分为:静态代理和动态代理两种类型。 其中静态代理的实现相对简单,大致逻辑如下:
- 编写⼀个
代理类
实现与⽬标对象相同的接⼝ - 在该
代理类
内部维护⼀个⽬标
对象的引⽤。代理类
通常会通过构造器来塞⼊⽬标对象
- 在
代理对象
中调⽤与⽬标对象的同名⽅法,并方法执行前后添加前拦截,后拦截等所需的业务功能。
而对于动态的代理通常也有两种方式:
-
基于接口的动态代理(使用
Jdk
中的Proxy
) :这种方式要求目标对象必须实现一个或多个接口,代理对象则是通过Proxy.newProxyInstance(...)
方法创建。 -
基于类的动态代理(使用
CGLIB
库) :这种方式可以代理没有实现任何接口的类。它通过继承来实现代理。
(注:因为Mybatis
中是的Java
中基于接口
形式的动态代理,所以我们主要介绍Java
中动态代理的相关内容)。
- 创建实现
InvocationHandler
接口的代理处理类 : 首先,你需要创建一个类,实现InvocationHandler
接口。这个类将定义代理对象的行为,包括在原始方法执行前后插入的逻辑。这个类的invoke
方法会在代理对象的方法被调用时被触发,你可以在其中编写自定义的逻辑。 - 创建代理对象 : 使用
Proxy.newProxyInstance(...)
方法来创建代理对象。该方法需要传入目标类的类加载器、目标类实现的接口列表以及之前创建的InvocationHandler
实例。 - 调用代理对象的方法 : 创建代理对象后,通过调用代理对象的方法来触发代理处理类的
invoke
方法。在invoke
方法中,你可以根据需要在方法调用前后添加自定义逻辑。
事实上,所谓的动态代理类就是在运⾏时⽣成指定接⼝的代理类。
而Jdk
中动态代理的实现有两个核心要素: InvocationHandler
和公共接⼝
。 具体来看,每个代理实例(即实现需要代理的接⼝)都有⼀个关联的调⽤处理程序对象,此对象会实现InvocationHandler
接口,并将相关的增强逻辑都定义在InvocationHandler
类中的invoke
⽅法之内。
MapperProxy
相关的逻辑
在之前Mybatis流程分析(四):Mybatis构建Mapper背后的故事中我们曾分析到 Mybatis
中的MapperProxyFactory
内部的newInstance
方法会根据我们传入的接口信息返回一个Mapper
实例对象。
java
public class MapperProxyFactory<T> {
// 待实现的接口信息
private final Class<T> mapperInterface;
// ....省略其他无关代码
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 动态代理的逻辑
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
进一步,其整个调用过程如下所示:
接下来,我们便看看MapperProxyFacory
中的newInstance
方法内部到底做了哪些工作。其内部代码如下:
MapperProxyFacory # newInstance()
java
protected T newInstance(MapperProxy<T> mapperProxy) {
// 动态代理的逻辑
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
可以看到,newInstance
构建对象的方式使用了我们之前介绍的的Jdk
中的动态代理进行实现。此外,我们还注意到在使用Proxy
构建代理对象时,其中方法newProxyInstance
需要如下三个参数:
- 类加载器
ClassLoader
- 接口数组
Class[]{}
MapperProxy
不难发现,MapperProxy
在上述使用过程中会作为第三个参数进行传入,根据我们之前对于Jdk
动态代理机制的分析,此时我们有理由猜测,不管MapperProxy
内部逻辑如何复杂,其一定会实现InvocationHandler
接口,并同时实现InvocationHandler
中的invoke
方法。
而InvocationHandler
中的invoke
方法其实相当于逻辑的增强处,代理类
的增强逻辑基本都会在此进行实现。
至此,虽然我们还没有分析MapperProxy
的相关内容,但通过我们对于Jdk
动态代理机制的理解,其实我们已经知道了对于MapperProxy
类我们应该关注的重点------invoke
方法。
进一步,MapperProxy
类的相关代码如下:
MapperProxy
java
public class MapperProxy<T>
implements InvocationHandler, Serializable {
// sqlSession会话信息
private final SqlSession sqlSession;
// getMapper使用时传入的Mapper接口信息
private final Class<T> mapperInterface;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果getDeclaringClass方法信息则直接进行调用
// 例如:toString,equals等
return method.invoke(this, args);
} else {
// 执行Mapper接口中定义的方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
}
}
可以看到,MapperProxy
中invoke
方法的逻辑大致如下:
- 如果执行方法为
Object
类型中的方法,则无任何增强逻辑,直接执行; - 如果方法为
Mapper
接口中所定义的方法,则执行逻辑又委托给cacheInvoker
进行执行。
相信读到此处的你一定会有一种恍然大悟的感觉。原来在Mybatis
中,我们通过getMapper
返回一个实例对象,调用其内部的方法。进而调用到方法所对应的sql
的语句。这背后的一切原因都依赖于MapperProxy
中的invoke
方法 。更具体一点,其调用过程其实逻辑是委托给方法cachedInvoker
来完成的。
那cachedInvoker
又会执行哪些逻辑呢?接下来,我们便进入到cachedInvoker
中,看看其相应的逻辑。
java
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
// 此处是一个lambda表示
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
}
可以看到cachedInvoker
在返回对象时,会使用到一个lambda
表达式,相关逻辑无非就是根据条件返回不同的MapperMethodInvoker
的实现。
看来如果我们要明白MapperProxy
的invoke
逻辑,我们便需要进入到 MapperMethodInvoker
实现类中的invoke
方法。进一步,对于MapperMethodInvoker
而言,其主要有两个默认实现,一个为DefaultMethodInvoker
和PlainMethodInvoker
。此处我们仅选取其中的PlainMethodInvoker
进行分析。
java
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
可以看到,在PlainMethodInvoker
内部,invoke
方法又会将逻辑委托给MapperMethod
的execute
方法。
MapperMethod
事实上,在 MyBatis
中MapperMethod
是一个重要的内部类。它负责将 Java
接口中的方法映射为实际的 Sql
操作。MapperMethod
的作用是解析接口方法的元数据,包括方法名、参数等信息,并根据这些信息生成对应的 Sql
语句。总结来看,有其内容如下:
-
作用 :
MapperMethod
负责将接口中的方法转换为实际的Sql
操作。它根据方法的名称、参数类型等信息,动态生成执行的Sql
语句,并执行查询、更新等操作。 -
工作原理 : 当你调用代理对象的接口方法时,代理会将方法调用传递给
MapperMethod
,它会根据方法名和参数类型等信息,决定执行的Sql
操作。MapperMethod
会构建一个MappedStatement
对象,该对象包含了Sql
语句以及其他执行相关的信息。 -
结构 :
MapperMethod
主要包含以下属性:SqlCommand
: 表示Sql
操作的类型,比如SELECT、INSERT、UPDATE、DELETE
等。MethodSignature
: 用于描述接口方法的签名,包括方法名、参数类型等。SqlSource
: 用于生成Sql
语句的源信息。
(注:MappedStatement
相关信息我们在前一章有过介绍)
java
public class MapperMethod {
// .... 省略其他无关代码
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
}
// ....省略其他相似逻辑的d代码
return result;
}
private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
// 相当于对sql内容进行封装
MappedStatement ms = sqlSession.getConfiguration().
Object param = method.convertArgsToSqlCommandParam(args);
// 通过sqlSession中的select方法进行执行
sqlSession.select(command.getName(), param, method.extractResultHandler(args));
}
}
可以看到,当我们调用接口
中相关方法时,其本质是从Configurtaion
对象中获取缓存的MappedStatement
对象,提取出其中的sql
信息,然后将sql
执行逻辑委托于SqlSession
来进行执行。
至此,我们应该明白。在Mybatis
中,传入一个Mapper
接口,Mybatis
内部就会通过代理的方式为我们生成一个该接口的代理对象------MapperProxy
。进一步,当调用接口中方法时,会调用到对象中的方法所绑定的sql
语句。
事实上,结合之前的文章:
再加上本章,我们已经利用三章
的篇幅来叙述Mybatis
中getMapper
的相关逻辑。虽然看着很多,但却可以通过如下的一张图来进行总结。
总结
事实上,Mybatis
中getMapper
获取实例对象的背后的逻辑就是 通过动态代理的方式生成一个实现该接口的代理类 。进一步,调用该实例对象
执行对应sql
的背后的逻辑也全部交给了SqlSession
来处理。至于SqlSession
中是如何执行sql
的且听后续分解~~
读源码,分析源码本身就是一件枯燥的事情。作者在行文排版
上已经尽可能减少代码的的排版,因为作者觉得大段
的粘贴代码只会降低行文的可读性。事实上,作者更喜欢用图示的信息来展现代码间的调用逻辑。希望文章中的图能对你理解MyBatis
有所帮助。
此外,读源码的本身并不是让我们再复现一个框架,读源码等多的是窥探源码的中的设计以及让我们可以更加深刻的认清楚框架的"底层"逻辑,好让你在工作中快速定位问题。
最后,还是希望文章能给你带来一点收获,毕竟花费时间来看文章本身就是一种对作者的信任,真的很感谢你们的信任。我所能做就是不断提升文章的质量,让读者能真正有所收获。🌹
我是毅航
,一名练习时长两年半的六十九岁扶墙java开发
。针对文章如果有疑问的话可以在评论区留言,作者抽空会逐一回复。🙋
共勉,一起成长。