Java代理详解

1,747 阅读6分钟

Java代理可以分为静态代理和动态代理:

如果根据字节码的创建时机来分类,可为分为静态代理和动态代理:

  • 所谓的静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实的主题角色的关系在运行前就确定了。
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件,不存在.java文件到.class文件的转换过程(代理类)。

静态代理的优缺点:

​ 优点:通过静态代理,达到了功能增强的目的,而且没有入侵源代码

​ 缺点:

​ 1. 当需要代理多个类的时候,由于代理对象需要时间和目标对象一致的接口:有两种方式:

​ 只维护一个代理类,由于这个代理类实现多个接口,但是这样就导致代理类过于庞大

​ 新建多个代理类,每个目标对象对于一个代理类,但是这样会产生过多的代理类

​ 2. 当接口需要增加,删除,修改方法的时候,目标对象和代理类都需要同时修改,不易维护

如何改进?当然是让代理类动态的生成,也就是动态代理

为什么类可以动态的生成?涉及java虚拟机的类加载机制。

Java虚拟机类加载过程主要分为五个阶段:加载,验证,准备,解析,初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制流
  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

  • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础

  • 从网络中获取,典型的应用是 Applet

  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流

  • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类

  • 从数据库中获取等等

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。

常见的字节码操作类库

Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。

ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。

CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。

Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。

...

实现动态代理的思考方向

为了让生成的代理类与目标对象(真实主题角色)保持一致性,下面介绍两种最常见的方式:

  1. 通过实现接口的方式,JDK动态代理
  2. 通过继承类的方式,CGLIB动态代理

JDK动态代理

动态代理也要有目标实现类,就是我们的委托类,实现目标接口

中介类:中介类必须实现InvocationHandler接口,作为调用处理器拦截对代理类方法的调用。

public class DynamicProxy implements InvocationHandler { 
    //obj为委托类对象; 
    private Object obj; 
 
    public DynamicProxy(Object obj) {
        this.obj = obj;
    } 
 
    @Override 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        System.out.println("before"); 
        Object result = method.invoke(obj, args); 
        System.out.println("after"); 
        return result; 
    }
} 

从上面的方法我们可以看到,中介类持有一个委托类对象引用,在invoke方法中调用了委托类对象的相应方法。

通过聚合方式持有委托类对象引用,把外部对invoke的调用最终都转为委托类对象的调用。其实和静态代理的实现方式差不多,其实动态代理关系由两组静态代理组成,这就是动态代理的原理

jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口

CGLIB动态代理

ASM

Retrofit中动态代理的目标接口实现类是什么

对比动态代理和静态代理,我怎么感觉动态代理还是会有静态代理的那两个缺点。

其实,动态代理的使用得看使用场景,比如retrofit,它不会因为你的接口变多,然后它就变得更加庞大复杂,为什么,因为我们动态代理他能实现的功能是,比如你所有的接口,他的这个调用过程或者流程是类似的,那么这种的话用代理就很好啦,retrofit就是这么用。

public class TestClass {

    public static void main(String[] args) {

        // 1. 动态代理,如果要是实现InvocationHandler接口,然后有具体的实现内容,
        //    那就调用动态代理类直接调用方法
        // 2. 如果类似下面的用匿名内部类的实现方式,我们可以在匿名内部类中,根据方法名称,
        //    然后,在api.test这样调用方法的时候,我们在invoke里面可以拿到方法名做个判断,
        //    然后invoke方法方法具体的对象 Student student = api.test(); api对象就类似一个壳
       ApiService2 api = (ApiService2) Proxy.newProxyInstance(ApiService2.class.getClassLoader(), new Class<?>[]{ApiService2.class}
                , new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                        //Object impl = new ApiServie2Impl(); //目标接口的实现类
//                        Object result = method.invoke(this,args);
                        System.out.println(method.getName());
                        if (method.getName().equals("test")){
                            return new Student(30);
                        }
                        if (method.getName().equals("testA")){
                            return new Student(20);
                        }

                        return null;
                    }
                });
    Student student = api.test();
    System.out.println(student.age);

    System.out.println("===================");

    Student student1 = api.testA();
    System.out.println(student1.age);

//      输出内容
//        test
//        30
//        ===================
//        testA
//        20

    }
}

参考文章:

juejin.cn/post/684490…

juejin.cn/post/684490…