所谓就是代理是通过代理对象访问目标对象,用代理对象来代表目标对象的功能,也可以目标方法进行增强。Java代理分为静态代理和动态代理。在这里主要描述静态代理和JDK动态代理。
1.静态代理
所谓静态代理,就是程序在编译时就确定了代理类与被代理类的关系。
假设有一接口TakeBooks,接口内有一抽象方法takeBooksFromOffice。老师类Teacher实现了这一接口,现在有一代理类Student也实现了TakeBooks接口。
现在要让学生代理老师去办公室取书,也就是说我们要让学生(Student)类去代理老师(Teacher)类实现takeBooksFromOffice方法。
以下是静态代理的实现
- TakeBooks接口
public interface TakeBooks {
public void takeBooksFromOffice();
}
- 目标类 Teacher类
public class Teacher implements TakeBooks{
@Override
public void takeBooksFromOffice() {
System.out.println( "去办公室取书" );
}
}
- 代理类 Student类
public class Student implements TakeBooks{
//持有目标对象
private Teacher teacher;
public Student(Teacher teacher){
this.teacher = teacher;
}
@Override
public void takeBooksFromOffice() {
//实现目标对象的方法
teacher.takeBooksFromOffice();
//这里也可以写其他内容进行对目标方法的增强
}
}
- 测试类Test
public class Test {
public static void main(String[] args) {
//目标类Teacher
Teacher teacher = new Teacher();
//代理类Student
Student proxy = new Student(teacher);
//学生代理老师去办公室取书
proxy.takeBooksFromOffice();
}
}
//Output:
//去办公室取书
从上述的例子我们可以看出,实现对一个目标类的静态代理
我们只需要:
1.创建一个代理类也实现目标类的接口;
public class Student implements TakeBooks
2.使代理类持有目标类的引用;
private Teacher teacher;
3.代理类接口方法实现为 持有的目标类引用.接口方法 或者再加上其他的增强方法;
teacher.takeBooksFromOffice();
观察上述,我们也能发现静态代理也存在不可避免的缺陷。假设在上面的例子中,我今天让学生代理去取书,我明天让保洁阿姨代理老师去取书,后天让校门警卫代理老师去取书,那么我们就要在代码中再添加多个代理类。假如老师去学校要司机代理开车,去食堂吃饭要食堂阿姨代理打饭,这样一个目标类的不同方法还需要定义不同的代理类去实现。可见,静态代理的重用性比较差
2.JDK动态代理
JDK动态代理是java.lang.reflect.*包提供的方式,这种方法必须要借助一个接口才能产生一个代理对象。我们仍然使用上述老师取书的例子,代理类与目标类的共用接口TakeBooks和目标类Teacher没有变。
- TakeBooks接口
public interface TakeBooks {
public void takeBooksFromOffice();
}
- 目标类 Teacher类
public class Teacher implements TakeBooks{
@Override
public void takeBooksFromOffice() {
System.out.println( "去办公室取书" );
}
}
- 现在我们创建能够建立代理对象和目标对象的联系的JDK代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKproxy implements InvocationHandler {
//JDK代理类持有目标类的引用
private Object target;
//建立代理类与目标类的联系
public Object bind(Object target){
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
//InvocationHandler接口里抽象方法的具体实现,就是代理逻辑方法的实现
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这行代码相当于调度了目标对象的方法
//只不过静态代理我们直接使用this.teacher.takeBooksFromOffice()调用
//而动态代理运用了反射实现调用而已
Object object = method.invoke(target, args);
return object;
}
}
- 测试类Test
public class Test {
public static void main(String[] args) {
//创建JDK代理类实例
JDKproxy jdKproxy = new JDKproxy();
//目标类Teacher
Teacher teacher = new Teacher();
//建立接口引用的代理类,由于bind方法返回值是Object类型,我们需要强制向下转型
//bind传入参数为目标类,返回一个代理类
TakeBooks proxy = (TakeBooks)jdKproxy.bind(teacher);
//代理者帮老师去取书
proxy.takeBooksFromOffice();
}
}//Output:
//去办公室取书
从上述的例子我们可以看出,实现对一个目标类的动态代理
我们只需要:
1.创建一个实现InvocationHandler接口的JDK代理类;
2.在JDK代理类中持有一个目标类的引用,并创建一个方法为这个引用赋值,以此来建立目标对象和代理对象的联系(这里用了bind方法,其实为JDK代理类创建一个有参构造方法也可以实现);
3.在invoke方法中实现代理逻辑方法,invoke方法是JDK代理类所实现的InvocationHandler接口中的抽象方法。
在JDK动态代理中我们发现JDK代理类中持有的目标类和方法返回的代理类均为Object类型,这意味着不仅学生可以给老师取书,甚至还能帮保洁阿姨倒水,帮校园门卫站岗,这取决于我们在main方法中做出怎样的向下转型(前提是合法的向下转型)。
不过相信大家浏览完后可能会和我提出一样的问题:
- InvocationHandler接口是什么;
- Proxy.newProxyInstance方法参数什么,返回了什么;
- invoke方法传入参数是什么;
- main函数里没有显式地调用invoke方法,InvocationHandler中的invoke()方法是怎么自动实现的呢;
1)InvocationHandler接口中invoke方法包含3个参数
public Object invoke(Object proxy, Method method, Object[] args)
参数 | 含义 |
---|---|
Object proxy | proxy对象即为newProxyInstance方法中生成的对象 |
Method method | method 用来获取当前调度的方法信息 |
Object[] args | args就是当前调度方法的参数列表 |
2)Proxy中的静态方法newProxyInstance包含三个参数
参数 | 含义 |
---|---|
ClassLoader loader | 类加载器,这里我采用了target的类加载器 |
Class<?>[] interfaces | 表示生成的动态代理对象处在哪个接口之下,我们直接获取target的接口实现列表然后传入方法中 |
InvocationHandler h | 表示实现了代理方法逻辑的JDK代理类(也就是实现了InvocationHandler接口的那个类),我们是在JDK代理类中调用的newProxyInstance方法,所以要将自身传入该方法中,所以此处传入this。 |
newProxyInstance方法的返回值就是JDK动态生成的代理类。
实际上newProxyInstance也可以在main函数中调用,在main函数调用时我们应该这样声明:
TakeBooks proxy = Proxy.newProxyInstance(teacher.getClass().getClassLoader(), teacher.getClass().getInterfaces(), jdkproxy);
这样的话我们就不需要bind方法了,可以将bind方法改成JDK代理类的有参构造器。
3)既然我们想知道代理类是如何实现目标类方法的,那么我们要首先了解代理类中的内容。我们现在在invoke方法里面再加一个语句:
@Override
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//添加一句可以输出newProxyInstance方法帮助我们自动生成的代理类的类名的语句
//proxy.getClass().getName()中的proxy就是上面invoke方法的第一个参数里面的proxy
System.out.println( proxy.getClass().getName() );
Object object = method.invoke(target, args);
return object;
}
这样输出的结果就变成了:
com.sun.proxy.$Proxy0
去办公室取书
Process finished with exit code 0
很明显 $ Proxy0类就是编译器为我们生成的代理类的类名,我们可以让编译器生成$ Proxy0的.class文件再经过反编译获得其类型信息,具体步骤为:
1)在main方法的首部添加
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
注意,此处的是"true"被双引号包围的
2)按照控制台输出的路径对应手动创建文件夹
我的控制台输出是:com.sun.proxy.$Proxy0
我的项目名是jdkproxy,那么我创建的文件夹应该为
C:\Users\14583\IdeaProjects\jdkProxy\com\sun\proxy
也就是我们在项目的根目录下,根据控制台输出的路径创建文件夹
这样当我们运行了main方法时,在我们新建的这个文件目录下就会生成$ Proxy0.class文件了,我使用的JD-GUI这个软件反编译的.class文件,得到了$ Proxy0类的源代码,下面是其一部分:
//代理类是Proxy的子类并实现了我们定义的TakeBooks接口
public final class $Proxy0
extends Proxy
implements TakeBooks
{
//......源代码相当的长,我只选取了一个部分
private static Method m3;
//......
public final void takeBooksFromOffice()
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
//.......
static
{
try
{
//......
m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);
//......
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
通过对源代码的阅读,我明白了我在main方法中调用proxy.takeBooksFromOffice(); 这一语句时,我操作的proxy引用所指向的其实是一个$ Proxy0类的实例;
我调用的这个takeBooksFromOffice本质上是$ Proxy0源代码中的final void takeBooksFromOffice 这一方法,它进而调用this.h.invoke(this, m3, null); 方法,这里的 this.h,其实是代理类父类Proxy中的一个保护变量,查看Proxy类的源代码后,我发现其中有这样一个信息:
protected InvocationHandler h;
但是这个h究竟是什么值呢?
在$ Proxy0类中有这样的构造方法:
public $Proxy0(InvocationHandler paramInvocationHandler)
{
super(paramInvocationHandler);
}
$ Proxy0的构造器内部引用了父类Proxy的构造方法,我们再查找父类Proxy的源代码发现:
protected Proxy(InvocationHandler h) {
//这句是用来判断传入的h是否为空的
Objects.requireNonNull(h);
this.h = h;
}
而我们定义的JDKproxy类恰好是InvocationHandler的实现类。在建立目标类与代理类联系的时候,我们向生成代理类的newProxyInstance方法传入了三个参数,分别为:1.目标类加载器,2.目标类所实现的接口,3.JDKproxy实例本身,那么这里的"this.h"的值很可能就是在main方法中定义的jdkProxy.
我们再看$ Proxy0的源代码,我们发现这样的一部分静态代码块:
static
{
try
{
//......
m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);
//......
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
其中有这样的一条语句:
m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);
再看main方法中的
JDKproxy jdKproxy = new JDKproxy();
Teacher teacher = new Teacher();
TakeBooks proxy = (TakeBooks)jdKproxy.bind(teacher);
proxy.takeBooksFromOffice();
也就是说当我们将目标类的真实类型传入bind方法并进行对其结果进行强制向下转型的时候,代理类就已经加载了目标类的真实类型,JDK代理类型的实例jdkProxy以及目标类所实现的接口类型,我们调用proxy.takeBooksFromOffice() 时编译器会执行this.h.invoke,也就是jdkProxy.invoke方法,而通过反射实现的调度真实对象的方法就封装在jdkProxy.invoke中了,即Object object = method.invoke(target, args);
InvocationHandler中的invoke()方法是怎么自动实现的呢?代理类的实例又是怎样知道目标类有takeBooksFromOffice方法呢?
当我们将目标类的类加载器和接口列表传给newProxyInstance时,代理类$ Proxy0就已经通过静态代码块完成了对需要代理的类(即目标类)类型和方法的加载;当我们再将实现了InvocationHandler接口的JDK代理类实例jdkProxy传入时,由于代理类$ Proxy0持有由InvocationHandler类型的变量声明,它就可以通过接口变量调度被JDKproxy实现的接口方法invoke。而当接口变量调用了被类实现的接口中方法时,也是在通知相应的对象调用接口的方法,这种层间协作的过程也被称为接口的回调。