代理模式
起源
阅读mybatis
源码时需要到动态代理技术,所以查阅许多文章,深入学习代理技术。
本文的内容并非原创,摘录自网络各处。此文想表达的主要是对代理模式做一个自我总结。
代理分类
- 静态代理
- 动态代理
- jdk原生动态代理
- cglib动态代理
术语列表
- 代理对象
- 被代理对象
其实代理就是一种委托,被代理对象委托代理对象完成自己的职责。代理对象作为独立个体,可以在完成被代理对象职责的同时添加一些自己的职责。
静态代理
静态代理类(对象)持有被代理对象的引用,同时实现被代理对象实现的接口。
代码示例
// 接口
interface Hello{
String sayHello(String str);
}
// 实现
class HelloImp implements Hello{
@Override
public String sayHello(String str) {
return "HelloImp: " + str;
}
}
下面对HelloImpl
类的对象进行代理
// 静态代理方式
class StaticProxiedHello implements Hello{
//持有被代理对象引用且实现接口
private Hello hello = new HelloImp();
@Override
public String sayHello(String str) {
logger.info("You said: " + str);
//职责委托给被代理对象
return hello.sayHello(str);
}
}
动态代理
动态代理就是不手动写上面那个代理对象类,但是代理模式要完成的职能是不变的,即对被代理对象的方法进行增强。
由此可以想到,想要对被代理对象方法进行增强,必然要在调用这个方法的时候进行拦截。
方法拦截技术其实就是动态代理的核心。
对于方法拦截位置,学过AOP
肯定都知道有好多拦截时机,比如方法调用前,方法调用后,方法调用前后等位置。
至于如何拦截方法调用,如果使用静态代理类,自然可以手动拦截。使用动态代理时,这些工具一般由外部代码来完成。
原生动态代理
上面知道,动态代理技术的核心是方法拦截。原生动态代理技术中,完成此职责就是InvocationHandler
。
当借助java.lang.reflect.Proxy#newProxyInstance
方法生成代理对象时,必须传入一个InvocationHandler
类型的对象。此后针对java.lang.reflect.Proxy#newProxyInstance
方法生成的代理对象进行方法调用时,方法会被转发到java.lang.reflect.InvocationHandler#invoke
中。
口说无凭,还是直接看代码最好:
class LogInvocationHandler implements InvocationHandler{
//可以看到,此处还是得委托实际对象来完成职责。
//代理对象只是额外添加一些职责
private Hello hello;
public LogInvocationHandler(Hello hello) {
this.hello = hello;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("sayHello".equals(method.getName())) {
logger.info("You said: " + Arrays.toString(args));
}
return method.invoke(hello, args);
}
}
Hello proxyInstance = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, invocationHandler);
public final class $Proxy0 extends Proxy implements Hello
{
...
public $Proxy0(InvocationHandler invocationhandler)
{
super(invocationhandler);
}
...
@Override
public final String sayHello(String str){
//方法调用被转发到invoke方法
return super.h.invoke(this, m3, new Object[] {str});// 将方法调用转发给invocationhandler
}
}
代理对象的所有接口方法调用都会转发到InvocationHandler.invoke()
方法,在invoke()
方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;之后我们通过某种方式执行真正的方法体,示例中通过反射调用了Hello对象的相应方法,还可以通过RPC调用远程方法。
注意:对于从Object中继承的方法,JDK Proxy会把
hashCode()
、equals()
、toString()
这三个非接口方法转发给InvocationHandler
,其余的Object方法则不会转发。详见JDK Proxy官方文档。
CGLib动态代理
上面已经讲过,核心在于方法拦截。所以对于使用此这种代理模式,需要实现MethodInterceptor
接口,用于方法拦截。
class MyMethodInterceptor implements MethodInterceptor{
...
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
logger.info("You said: " + Arrays.toString(args));
//转发给被代理对象,即父类。
return proxy.invokeSuper(obj, args);
}
}
Enhancer enhancer = new Enhancer();
//此处仍需委托给被代理对象完成职责
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());
HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
上述代码中,我们通过CGLIB的Enhancer
来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()
方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()
方法,在intercept()
方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()
方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete
的具体方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler
很类似,都是方法调用的中转站。
注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如
hashCode()
、equals()
、toString()
等,但是getClass()
、wait()
等方法不会,因为它是final方法,CGLIB无法代理。
// CGLIB代理类具体实现
public class HelloConcrete$$EnhancerByCGLIB$$e3734e52
extends HelloConcrete
implements Factory
{
...
private MethodInterceptor CGLIB$CALLBACK_0;
public final String sayHello(String paramString)
{
...
MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;
//如果拦截器不为空,就转发
if (tmp17_14 != null) {
// 将请求转发给MethodInterceptor.intercept()方法。
return (String)tmp17_14.intercept(this,
CGLIB$sayHello$0$Method,
new Object[] { paramString },
CGLIB$sayHello$0$Proxy);
}
//为空就不转发
return super.sayHello(paramString);
}
...
}
mybatis中的动态代理
其实在mybatis
中没有被代理对象,稍微有点特殊。没有动态代理对象,这一点一直让我很疑惑,今晚写完文章后去翻了下源码,发现的确有点特殊:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
//最终并没有委托给被代理对象。而是委托给mapperMethod对象。
return mapperMethod.execute(this.sqlSession, args);
}
可以看到此处的逻辑是如果非数据库相关方法,则不去查询数据库。
其实按照正常的逻辑,应该是传入一个Mapper
接口的实现类,然后委托给这个实现类来完成查询,可实际并非如此。
总结
对于普通的动态代理,把握住方法拦截、被代理对象即可。
对于mybatis
,并没有被代理对象,而是委托给mapperMethod
来完成职责,这个其实稍微有点不同,应该属于变种的代理模式,毕竟纯代理模式最终还是得依靠被代理对象来完成职责。
对于这种拓展的代理模式,实际职责完成时,可以委托给任意对象来完成。这样一来,也解决我了对mybatis
中代理模式的困惑,毕竟我们只有Mapper
接口类。
其实如果想深刻理解代理模式,比较好的方法就是实际反编译看下生成的代理类。
至于如何把生成的代理类保存到硬盘上,可见文章末尾。
我翻了字典,查阅了代理的含义,发现英文其实等同于delegation
,即委托的含义。所以也呼应文章开头所说,代理的本质就是被代理对象将自己的职责委托给代理对象。