今天我们来看下Mybatis
里MapperProxy
的实现,为什么它能只写一个接口和xml文件,就能自动把两者关联起来,你可能会很奇怪,java里面的接口是不能去实际执行的,它必须有具体的实现类,那这个类是怎么生成的呢?生成这个类之前会有什么操作?这就要说道java里面的一大利器-动态代理了。
Mybatis的动态代理
首先说一下什么是代理,在设计模式里面有一种代理模式,我们有一个目标对象,但是这个目标对象的实现不能满足我们的需求,我可以构造一个代理对象,代理对象里面会有目标对象的实现,我还可以在上面添加逻辑,最后实际调用的是代理对象的实现。整个过程调用方几乎无感知,就像用原对象一样在使用代理对象。
你可能听过静态代理,但是我们基本用的都是动态代理,因为静态代理需要你自己去写,侵入性太强,而动态代理是在运行时自动生成对象,就是这个自动省去了很多功夫。
动态代理作为一种字节码增强的技术,它的实现库有很多,jdk自带的动态代理(这个只作用于接口),还有cglib(作用于类),还有Bytebuddy
等。我们常用的框架里面使用代理的地方很多,Mybatis
,spring
的aop和fegin等,这些都是利用的代理,因为动态生成的代理对象可以让你的整个逻辑很简洁,整个过程对使用者完全透明,大大减少了开发者编码量。而真正的实现转移到了代理里面。
在Configuration.getMapper
时首先拿到addMapper
时候放在mapper里面初始化(占位)用的mapperProxyFactory<T>
,然后创建了一个MapperProxy<T>
对象,这个代理对象里面会有一个Map<Method, MapperMethodInvoker> methodCache
方法缓存,因为一个类会有多个方法,每个方法会对应一个实现,在实际调用的时候会拿到methodCache
里面对应的MapperMethodInvoker
调用,如果没有会初始化放到缓存里。
当我们调用代理方法时,会先判断是否是object的方法,如果是直接执行,这些方法和mapper映射无关。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
否则会构建缓存的Invoker
:
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (!m.isDefault()) {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
}
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
});
、、、、、、、、、、、、、、、、、、、、省略
}
}
PlainMethodInvoker
我们先不看PlainMethodInvoker
的调用,先看下面的,下面会有一个DefaultMethodInvoker
,这个类是干嘛的呢?它是针对接口的默认方法实现做的适配,这里有jdk8和9的区别貌似。
上面实现会调用cachedInvoker
方法构建一个PlainMethodInvoker
对象。它是MapperMethodInvoker
的一个实现。而MapperMethodInvoker
是一个接口,实现如下,里面就一个方法:
interface MapperMethodInvoker {
Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
}
可以看到PlainMethodInvoker
和DefaultMethodInvoker
都是它的实现。这里主要是看PlainMethodInvoker
,它里面有一个MapperMethod
的变量:
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
最后实际在mapper里面调用具体方法的时候其实是执行的mapperMethod.execute(sqlSession, args);
方法,这个方法就是根据xml或者注解里面得到的sql去具体执行并组装数据返回了。
MapperMethod
MapperMethod
的构造函数主要有两块,一个是sqlCommand
,对应xml或者注解的sql部分,一个是MethodSignature
,对应接口的方法签名。
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
这里面的SqlCommand
和MethodSignature
是很重要的两个类,这个我们后面具体介绍。
Mapper代理调用过程梳理
首先在初始化配置的时候会生成一个MapperProxyFactory
(type)这样一个接口代理工厂放到MapperRegistry
的knownMappers
里,这个MapperProxyFactory
主要是占位用,为了生成真正的MapperProxy
。
MapperProxy
是XXXMapper
接口的代理类,我们用sqlsession.getMapper(XXX.class);
其实返回的是MapperProxy
, 我们实际调用的是MapperProxy
代理的方法,也就是MapperProxy.invoke
方法,为了更好的调用方法,mybatis
封装了一个MapperMethodInvoker
,它会缓存在map里面,就是methodCache
,如果下次重复getMapper
,直接拿缓存的,不会再生成。而MapperProxyFactory
里面的methodCache
是和MapperProxy
里面的methodCache
是同一个引用,所以MapperProxyFactory
里面的getMethodCache
就是得到所有的方法缓存列表。
然后用XXXMapper
具体调用crud方法时,经过一连串的代理调用,最后会执行MapperMethod.execute
方法,这个execute
方法就是连接mapper里面crud方法和xml里sql的纽带,最后执行操作。具体实现是sqlCommand
会在构建的时候设置一个type,主要是根据xml里面的标签或者注解,然后分别进行crud操作。而select的实现会比增删改复杂得多。有兴趣的可以自己看一下。
然后这里面还有一个Flush的type,这个主要是对批处理执行使用的,这个以前没用到过,看了源码之后才了解是干嘛的,你可以直接在mapper接口里面写一个方法,上面写一个@Flush注解,后面调用这个方法,该事务里面未提交的操作都是进行提交。
总结
Mybatis
的整个动态代理实现流程还是挺复杂的,里面为了不同情况各种封装嵌套,一开始看着可能会很懵,我一开始也是,直到我画出了整个流程图之后才整个清晰了起来,你可以按照我文章描述去debug具体的调用栈,然后画出一个调用链路图,这样能加深理解。