我的学习笔记——Java静态代理和动态代理

静态代理与JDK动态代理

  所谓就是代理是通过代理对象访问目标对象,用代理对象来代表目标对象的功能,也可以目标方法进行增强。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 proxyproxy对象即为newProxyInstance方法中生成的对象
Method methodmethod 用来获取当前调度的方法信息
Object[] argsargs就是当前调度方法的参数列表

  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。而当接口变量调用了被类实现的接口中方法时,也是在通知相应的对象调用接口的方法,这种层间协作的过程也被称为接口的回调。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值