介绍
要了解动态代理,首先要知道什么是代理,在笔者看来,代理的功能概括来说就是:在不修改被代理类代码的情况下给这个类包含的方法增加某些功能。
**至于为什么要不修改这个被代理类?**可能是为了满足开闭原则,可能是想把这个被代理类给隐藏起来,等等 。原因可能有很多,但概括起来就是不修改代理类。
要增加什么功能呢?(后文提到的增强指的就是增加功能)
这个就看使用者怎么写了,这个是非常灵活的,常用的就是增加日志、统计方法耗时等
我熟知的动态代理通常有两种,即JDK动态代理与CGLIB动态代理。
笔者最近学习了JDK动态代理,所以编写这个博客输出一下,因为笔者也是初学,了解的不多,尽可能写的详细~
好处是,笔者知道初学者的学习的难点在哪,应该更能帮助初学者理解~
坏处嘛,嘿嘿,如果教错了,那就抱歉啦~
JDK动态代理
笔者前面前面提到过JDK动态代理是基于接口来实现的,具体怎么个实现法,参考下面这张图。
代理类实现了与被代理类相同的接口,那么相应的代理类要实现对应的接口方法。
既然是要增强被代理类某些方法的功能,那么思考一下,代理类实现接口方法时方法中应当包含那些内容?
答案显而易见:实现新增功能的代码 + 调用被代理类对应的方法
比如我想统计一个方法的执行时间,那么伪代码如下
void test (Object[] args){
1. 统计开始时间
2. 调用被代理类的test方法
3. 统计结束时间
4. 计算整体耗时
}
鲁迅曾经说过:光说理论不给代码,那就是在耍流氓(鲁迅:也许我没说过? )
客官稍安勿躁,细想一下要实现如上的功能我们需要解决的两个问题:
- 代理对象怎么创建?
- 增强功能肯定是要写代码的,这个代码写在哪?
代理对象怎么创建
非常非常简单,java.lang.reflect
中有一个类Proxy
, 这个类中有个静态方法Proxy.newProxyInstance()
,通过这个方法我们可以获取某个对象的代理对象。
啪一下就蹦出来个陌生的方法就, 问你怕不怕? 别怕,慢慢来~
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
这个静态方法有三个参数需要提供(这个静态方法还有一个重载方法,我们不需要关注)
ClassLoader loader
: 被代理对象的类加载器Class<?>[] interfaces
: 被代理对象实现的接口InvocationHandler h
:一个InvocationHandler接口的实现类
问:为什么需要被代理对象的类加载器?
答:所谓动态代理,动态的点就在于JVM会帮我们生成代理类的字节码文件并加载,然后实例化,这过程就需要类加载器ClassLoader
,为了和被代理对象保持一致,所以就使用被代理对象的类加载器进行加载。
问:为什么需要被代理对象的接口集合?
答:这个接口本质上就是告诉代理对象,哪些方法是我想要增强的。 也就是说,提供的所有接口中的所有方法,就是JVM帮我们创建的代理对象所能代理的方法,也就是我们可以对其进行功能增强的方法。
这也意味着,并非需要把被代理类实现的所有接口传递进去,仅仅需要将包含我们想要增强方法的接口传递进去即可。
如被代理类 Test实现了两个接口,分别是接口A,接口B, 现在我想增强接口A中的方法,这个时候,传递进去的接口集合中仅仅需要接口A。
问:这个InvocationHandler接口是个什么东东?
答:这个就涉及到我们前面提到的第二个问题:新增功能的代码我们要写在哪里?, 答案就是这个InvocationHandler
里 ,InvocationHandler
接口中只有一个方法需要我们实现,那就是invoke
方法,准确的说,我们要新增的代码功能,就是写在这个invoke
方法里的。
Object invoke(Object proxy, Method method, Object[] args)
客官先别急,这个方法具体怎么用我们后面会提到,现在我们只需要有一个认知:我们想要增强的功能是写在invoke这个方法里的,因此我们需要创建一个类实现InvocationHandler
接口并实现其中的invoke
方法,并将这个类当做参数传给newProxyInstance
方法
理清了这三个参数,成功调用Proxy.newProxyInstance
方法,我们就可以获取到代理类对象。
举例
举个具体的例子:
接口:
public interface JDKTest {
void hello();
String eat(String food);
}
被代理类:
public class JDKTestImpl implements JDKTest{
@Override
public void hello() {
System.out.println("hello");
}
@Override
public String eat(String food) {
return "吃了" + food;
}
}
InvocationHanlder
实现类:
package com.zp.myproject.demo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"实现了代理增强");
return method.invoke(target,args);
}
}
测试类:
import java.lang.reflect.Proxy;
public class TEST {
public static void main(String[] args) {
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
JDKTestImpl JDKTestImpl = new JDKTestImpl();
Object JDKProxy = Proxy.newProxyInstance(
JDKTestImpl.getClass().getClassLoader(),
JDKTestImpl.getClass().getInterfaces(),
new MyInvocationHandler(JDKTestImpl)
);
JDKTest jdkTest = (JDKTest) JDKProxy;
System.out.println(jdkTest.eat("Apple"));
jdkTest.hello();
}
}
这个例子中,我们的接口为JDKTest
,它有一个实现类JDKTestImpl
,也就是我们上面提到的被代理类,需要增强的类
同时我们实现了InvocationHandler
接口 MyInvocationHandler
,里面包含一个Object target
, 这就就是我们想要代理的实例,同时我们实现了invoke
方法
在Test
中我们实例化了JDKImpl
,并将其作为target传给MyInvocationHandler
Proxy.newInstance()
这个方法的返回值JDKProxy
就是JVM为我们创建的代理对象,它是一个Object类型,但是我们可以将其强转为JDKTest
类型,因为它实现了JDKTest
这个接口。
JVM为其生成了对应的字节码文件, System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
这行代码可以让JVM将生成的字节码文件保存到本地,这样我们就可以反编译出源码直接查看
运行一下,可以看到结果符合预期,两个方法都进行了增强
现在我们来好好看看这个代理对象的源码,看看是怎么个事儿
创建出的代理类源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package jdk.proxy1;
import com.zp.myproject.demo.JDKTest;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements JDKTest {
private static final Method m0;
private static final Method m1;
private static final Method m2;
private static final Method m3;
private static final Method m4;
public $Proxy0(InvocationHandler var1) {
super(var1);
}
public final int hashCode() {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final boolean equals(Object var1) {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String eat(String var1) {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void hello() {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
ClassLoader var0 = $Proxy0.class.getClassLoader();
try {
m0 = Class.forName("java.lang.Object", false, var0).getMethod("hashCode");
m1 = Class.forName("java.lang.Object", false, var0).getMethod("equals", Class.forName("java.lang.Object", false, var0));
m2 = Class.forName("java.lang.Object", false, var0).getMethod("toString");
m3 = Class.forName("com.zp.myproject.demo.JDKTest", false, var0).getMethod("eat", Class.forName("java.lang.String", false, var0));
m4 = Class.forName("com.zp.myproject.demo.JDKTest", false, var0).getMethod("hello");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {
if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {
return MethodHandles.lookup();
} else {
throw new IllegalAccessException(var0.toString());
}
}
}
源码分析
首先,可以看到这个代理类的名称叫做$Proxy0
,它实现了JDKTest
接口并继承了Proxy
类
其次,可以看到它有一个有参构造方法,传入了一个InvocationHandler
接口实现类, 并在方法内部调用了super(var1)
我们来看一下父类中怎么写的
父类中仅有两个操作,判断h非空,将这个h赋值给类的属性h,而这个InvocationHandler
对象,实际就是我们之前创建代理类时传入的MyInvocationHander
看回代理对象,它还有5个Method
类型的属性,命名m0 ~ m4
,从下图可以看出,在静态代码块中,利用了反射机制,对m0 ~ m4
进行赋值
- m0 :
hashCode()
- m1:
equals(Obeject o)
- m2:
toString()
- m4:
eat()
- m5:
hello()
发现了什么! 前三个是所有Java类都有的方法 hashCode()
equals(Object)
toString()
, 而后两个是JDKTest
接口中对应的方法。
那么这些个方法有什么用呢? ,我们再看下去。
接下来我们看看代理类中实现的接口方法里面到底都写了些什么
嗯哼? 有点怪? 发现了什么?代理类本身并没有进行功能增强,而是调用了InvocationHandler.invoke()
!所以我们前面说的,具体的增强功能是写在invoke方法里面,现在是否能理解了?
现在我们可以回过头来结合super.h.invoke(this, m4, (Object[]{var1})
,看看这个invoke
方法的三个参数分别代表了什么。
Object invoke(Object proxy, Method method, Object[] args)
Method method
: 这个代表了当前被代理的方法Object[] args
: 这个代表了当前被代理方法的参数Object proxy
: 这个代表了被代理类本身, 传入的是this
所以现在能看懂MyInvocationHandler
中invoke
的编写逻辑了吗,它遵照的就是我在开篇写的伪代码的逻辑,做了两件事
- 调用被代理对象的方法
- 实现增强功能的逻辑
因为要调用被代理对象的方法,所以,我们的MyInvocationHandler
中必须包含被代理对象, 这也就是target
字段的作用
OK?method和args我理解了,那么还有一个问题:这个proxy有什么用,为什么要把代理对象传递给invoke方法呢?
其实这个问题我也没有太清楚,但是能想到的一个原因就是:保证增强功能的时候调用其他方法时这个被调用的方法也能增强。
比如在增强方法A,在编写增强逻辑时,我想要调用方法B,这个时候我就有两个选择:
- 使用被代理对象,调用
target.B()
- 使用代理对象, 调用
proxy.B()
这取决我想要调用的是没有增强的B方法还是增强后的B方法, 如果我想调用增强的B方法,那么就调用prxoy.B()
,反之调用target.B()
如果我的增强逻辑是记录所有方法的调用日志,那么如果我在增强代码中调用B方法时选择target.B()
,那么这个方法时没有增强过的,也就是这次调用的日志是没有的,反之,如果我们调用proxy.B()
,那么这次调用的日志同样能被记录。 不知道这样你们是否能理解
用例子说明:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "实现了代理增强");
if (method.getName().equals("eat")) {
((JDKTest) proxy).hello();
}
return method.invoke(target,args);
}
如果这么写, 增强eat()
方法时调用了增强的hello()
,那么输出是这样的:
躺若我调用没有增强的hello()
,
那么结果是这样的
总结
JDK动态代理 通过反射的机制生成了代理类, 这个代理类实现了被代理类的接口,以及通过反射机制获取了被代理对象需要增强的方法, 当用户使用代理类的方法时,它会调用InvocationHandler.invoke()
,将信息转发给inovke()
, 并在invoke()
中完成增强逻辑以及调用被代理对象的方法。
所以大致是一下这个图,如果客官能看懂下面这个图,那么说明你的理解程度至少和我是一个水平了(骄傲.jpg)
笔者第一次写这么长的博客文章,如果对客官有帮助,可以点个赞吗~非常感谢,当然,我需要强调的是:
笔者也是现学现卖,说不上精通熟练,所以如果有讲的错误的地方,我非常抱歉,也欢迎发现问题的大佬能够及时指出问题,我尽快改正,避免祸害了其他的萌新