一条SQL调用前的准备:
基于之前的例子使用Mybatis进行SQL的执行都是通过SqlSession来完成的,目前就是有两种方式:
XXXMapper mapper = sqlSession.getMapper(XXXMapper.class);
mapper.xxxMethod;
OR
Object obj = sqlSession.selectOne("statement");
第一种方法显然更加直观,可以直接面对我们编写的接口去调用,所有的参数、返回值都一目了然。
而第二种方法则更加直接,传入namespace+methodName,就可以找到对应的MappedStatement,在学习的时候更加容易理解。
那么现在的问题就是第一种方式是如何实现的呢,尽管已经知道第一种方式的底层还是第二种方式,那么它们之间是如何进行转换的呢?这就是接下来要讨论的问题。
猜想
首先已经知道了第一种方式使用了代理模式,那不妨猜想一下调用流程。
代理模式的作用是对目标方法的或者目标类的一次包装,内部在调用这个方法之前或者之后采取一些措施,并且已知Mapper.xml的配置方式需要指定namespace为目标类的全类名+标签的id组成一个全局的MappedStatement标识,那么可以猜想代理类如下:
interface UserMapper(){
getUser()
}
MyBatisProxy implements UserMapper{
Object getUser(){
String MappedStatementId = UserMapper.class.getQualifiedName().getMethodName();
Object result = sqlSeesion.selectOne(MappedStatementId);
return result;
}
}
(后补)在了解原理之后自己写一个代理实现
当我们使用sqlSession.getMapper(XXXX.Class)时就会创建一个代理
**MyMapperProxy.java**
package mybatis.study.proxy;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author daxingxing
* @date 2023/7/8
* @description 只是针对于UserMapper的代理实现的一个Demo
*/
public class MyMapperProxyForUserMapper implements InvocationHandler {
private SqlSession sqlSession;
private Class interfaceClass;
public MyMapperProxyForUserMapper(SqlSession sqlSession, Class interfaceClass) {
this.sqlSession = sqlSession;
this.interfaceClass = interfaceClass;
}
/**
核心逻辑,在invoke中调用sqlSession.selectOne(proxy);
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String namespace = interfaceClass.getName();
String id = method.getName();
String mappedStatementId = namespace +"."+id;
System.out.println(mappedStatementId);
return sqlSession.selectOne(mappedStatementId);
}
}
然后使用Proxy.newProxyInstance()获取一个代理
public Object getProxy(){
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{UserMapper.class}, new MyMapperProxyForUserMapper(sqlSession,UserMapper.class));
}
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
**现在我们可以观察一下我们传入的参数: |
1. sqlSession:MyBatis的话事人,内部完整的封装了调用者-->JDBC的调用逻辑 |
2. UserMapper.class和invoke()提供的method参数: |
两者结合一下:UserMapper.class.getName()+"."+method.getName ==> mappedStatementId. |
有了这两样东西,我们就将之前提到的sqlSession的第二种调用方式结合了起来:sqlSession.selectOne(mappedStatementId) |**
问题:
首先我们需要获取一个Mapper的时候,它获取的肯定是一个对象,因为我们在定义的时候都是把XXXMapper定义为一个接口,然而接口不能直接使用的,那这个接口的的实现类在哪里呢?
这里就要使用了一个技术叫动态字节码,或者我们常说的动态代理,它们俩是一个依赖关系。动态代理这种设计思想,使用了动态字节码技术进行实现,这样的话我们就没有办法在我们写的Java文件中看到UserMapper的实现,换句话说UserMapper的实现只会在JVM运行的时候存在,当JVM停止的时候它也就是消逝了。但我们还是可以通过debug看一下这个proxy的样子:

那在这里思考一下,MyBatis使用动态代理的目的和Spring使用动态代理的目的似乎就有了一些差别,Spring中的AOP使用代理的目的是对原有目标类或者方法进行增强;而MyBatis则是为了给XXXMapper接口提供实现类,并且提供的是统一化的实现类,目的不是为了增强而是为了实现功能嫁接,将UserMapper.java与UserMapper.xml文件进行嫁接,通过代理将原本两个毫不相干的文件结合起来。牛逼!!!
MyBatis是如何获取代理的:

以SqlSession.getMapper()方法作为调用入口,我们可以看到代理的创建最后委托给了MapperProxyFactory的newInstance方法去实现的,那么具体就看一下这个方法是如何获取代理对象的。
MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
值得一提的是knownMappers.get(type) 这说明每个MapperProxy都对应了一个工厂并且这个工厂是根据加载的Mapper.xml的namespace属性作为key形成的一个Map集合(这个Map集合是在提前收集完成,收集的一个时机就是xml文件—>MappedStatement对象的封装),因此如果传进来的类型和所有Mapper.xml的namespace不匹配就会找不到对应的Factory下面就是报一个BindingException
MapperProxyFactory#newInstance
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<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
newInstance() 有两个重载方法,首先调用的是参数为sqlSession的,其结构也很简单,就是创建一个MapperProxy然后交给Proxy.newProxyInstance()去创建一个代理即可。
那我们知道对于JDK的动态代理来说Proxy.newProxyInstance()的第三个参数是一个InvocationHander类型,因此MapperProxy就必须作为InvocationHander的子类并且实现了invoke方法,这样当我们调用所有的代理类方法的时候都会进入该invoke()方法。
UserMapper.getUser(10)
getUser(10)实际上会进入invoke()方法,那现在我们就来可以一个MapperProxy的invoke方法是如何处理的:

通过上图我们可以发现在UserMapper.getUser,真正被SqlSession执行之前会创建一个**MapperMethod(很重要啊)**对象,这个对象和MappedStatement 有一个对应关系,换句话说它的很多属性都是从MappedStatement 中获取,但是为了做一些前期的判断而封装成一个单独的对象。这个对象很简单,只有两个属性一个SqlCommond、一个MethodSignature,同样这个两个属性也很简单,一个就是用来标识当前的这个getUser()(我们肯定不能根据方法名称来判断要执行的Sql操作)执行的是增删改查的哪一种,一个用于解析当前方法的入参和返回值类型。但是值得注意的是,这个对象在被创建出来之后会被加入到一个MapperProxy$methodCache 中防止重复创建,并且后续会调用此对象的execute()来执行真正的查询!这个缓存实际上是类级别的,也就是对应了MapperProxy,因此会存储这个类中的至少执行过一次的方法。
解释一下“**这个缓存实际上是类级别的,也就是对应了MapperProxy,因此会存储这个类中的至少执行过一次的方法**”,防止我后面忘掉。
MyBatis通过动态代理为UserMapper创建了一个实现类,用的是JDK的动态代理,因此必须实现一个InvocationHandler,在MyBatis中使用了MapperProxy去实现了InvocationHandler,因此某种程度上来说一个每个UserMapper都会对应一个MapperProxy,而UserMapper的方法:getUser、insertUser、updateUser...,则是会对应为一个MapperMethod对象,而这个对象就会存放在MapperProxy中的methodCache这个Map集合中。
复习时可以结合上图进行debug。

对于第三步,则是SQL的真正执行,则是下面的章节讨论的事情。

第四步入参的解析,可以查看源码,大致就是解析@Param主角,存入一个SortedMap中。