专注Java教育14年 全国咨询/投诉热线:400-8080-105
动力节点LOGO图
始于2009,口口相传的Java黄埔军校
首页 hot资讯 Java反射和动态代理

Java反射和动态代理

更新时间:2022-10-19 09:49:13 来源:动力节点 浏览501次

Java在运行时可以实时获取对象的类型信息:RTTI(RealTime Type Identification)。比如多态,父类引用子类对象,调用方法时可以准确调用子类的override方法。本文从RTTI讲反射,然后讲一个与反射-动态代理相关的主要应用。

类对象

getClass()
Class.forName(String)

反射:在运行时获取类信息

动态代理:反射的一大应用

调用处理程序

代理人

静态代理

动态代理

动态代理内部

动态代理应用

JDK 与 CGLib

类对象

Java 想知道一个对象的类型信息,并且必须将类型信息存储在某个地方:

.java该文件是类的源代码,运行时肯定不会用到;

.class该文件是源代码编译后的字节码,也是静态存储的,运行时不使用;

但是,当 jvm 加载一个 java 类(类文件)时,它会生成一个特殊的对象:Class 对象 。 该对象记录了类的相关信息,具体信息类型可以在java的Class.java中查看。 比如有一个类叫Father,加载后会生成一个Father的Class对象 Class。

与来自Father new的其他对象相比,Class对象是一个特殊的对象,对于每个类,Class对象都是一个单例。类new的每个对象都有一个指向该类的Class对象的指针,这样对于每个对象,通过找到它的Class对象,就可以得到该对象在运行时的结构:属性、方法、类信息等。

getClass()

每个对象都有一个指向该类的Class对象的指针,这体现在代码层面,即在Object对象中,有一个获取Class对象的方法:

public final native Class<?> getClass();

Class.forName(String)

Java中获取Class对象的另一种方法是使用Class类提供的静态方法:

public static Class<?> forName(String className) 抛出 ClassNotFoundException

只要给出完整的类名(类名加包名),就可以获得该类的Class对象。

反射:在运行时获取类信息

类对象记录类的信息。对于每一个对象,通过它的类对象可以得到什么信息?看Class类的内容就明白了。

getFields:获取属性;

getConstructors:获取构造方法;

getDeclaredMethods:获取常用方法;

getAnnotations:获取注解;

getClasses:获取类的所有类信息(如父类、接口等);

还有其他辅助方法。总之,通过类对象,可以知道这个类的类名、继承关系、定义的属性、方法、构造函数、注解等。基本上一个类的所有信息都被反汇编了。

Field、Method、Annotation、Constructor等类都是上述数据结构的实现。它们都是 java.lang.reflect.*反射包( )中的核心类。

动态代理:反射的一大应用

代理是一个很常见的概念。比如房产中介,可以简单理解为房东的代理人。租房者有租金要求,不是直接和房东打交道,而是和代理人沟通。至于代理人,是否直接解决租客的问题或与房东沟通。解决租客的问题后,即代表自己的内部事务。

静态代理

假设有一个接口Coder:

public interface Coder { 
    /** 
     * 完成一个需求。
     * @param demand demand 
     */ 
    void implementDemands(String demand); 
    /** 
     * 估计一个需求的时间。
     * @param demand demand 
     */ 
    void estimateTime(String demand); 
    /** 
     * 说点什么
     * @return 
     */ 
    String comment();}

这个接口是程序员要做的。现在有一个Java程序员:

import lombok.AllArgsConstructor;import org.apache.commons.lang3.RandomUtils;/** 
* @author liuhaibo on 2018/04/18 
*/@AllArgsConstructorpublic class JavaCoder implements Coder { 
    String name; 
    @Override 
    public void implementDemands(String demand) { 
        System.out.println(name + ": 我用Java完成" + demand); 
    } 
    @Override 
    public void estimateTime(String demand) { 
        System.out.println(name + ": 我将使用 " + (demand.length() + RandomUtils.nextInt(3, 9)) + " hours."); 
    } 
    @Override 
    public String comment() { 
        return "Java 是完美的!"; 
    }   
    公共无效 showAngry() {
        System.out.println(name + ": 我很生气!!!"); 
    }}

它实现了程序员接口指定的任务。

静态代理:

/** 
* @author liuhaibo on 2018/04/18 
*/@ NoArgsConstructor@AllArgsConstructorpublic class StaticProxy implements Coder { 
    @Setter 
    private Coder coder; 
    @Override 
    public void implementDemands(String demand) { 
        // 代理要阻塞一些不合理的需求,给程序员最纯粹的任务
        if ("illegal".equals(demand)) { 
            System.out.println("No! ILLEGAL demand! "); 
            返回; 
        } 
        System.out.println("好的,我会找个程序员来做的。"); 
        coder.implementDemands(需求); 
    } 
    @Override
    公共无效估计时间(字符串需求){
        coder.estimateTime(需求); 
    } 
    @Override 
    public String comment() { 
        return coder.comment(); 
    }}

这个静态代理和程序员的接口是一样的。当有外部需求提供时,它会处理,但实际上它只是调用Java程序员做事。这是一个假程序员,只是一个代理。当然,这个代理还做了一些额外的事情。比如当外部需求“非法”时,代理直接拒绝该需求,这样可以避免程序员的一些琐碎事情:

        编码器 coder = new JavaCoder("Tom"); 
        编码器 staticProxy = new StaticProxy(coder); 
        staticProxy.estimateTime("你好,世界"); 
        staticProxy.implementDemands("发送广告"); 
        staticProxy.implementDemands("非法");

输出:

汤姆:我会用 19 个小时。好的,我会找个程序员来做的。汤姆:我用 Java 来完成 发送广告不!非法需求!

动态代理

静态代理不错,就是太麻烦了。如果要构建代理,则必须编写代理类来实现每个方法。如果有一个统一的要求:在程序执行每个方法之前和之后输出当前时间,那么写一个代理就太烦人了,看起来是重复的,毫无意义。

动态代理是jvm自动生成的代理,而不是类似上面开发者写的静态代理。此时,开发人员只需要指定代理的行为,剩下的交给 Java 来处理。

Java中创建动态代理的过程:

开发者指定接口;

JVM根据指定的接口自行动态生成一个类来实现接口;

接口中的每个方法都是这样实现的:对方法的每次调用都由开发人员指定的处理程序完成;

该处理程序的返回值将作为调用动态代理对象方法后的返回值。

所以理解 Java 动态代理的关键是区分 Java 会做什么和开发人员会做什么 。 开发者自己做就好了,剩下的就按照约定由Java来做。

所以开发者其实只需要担心两件事:

为哪个接口创建动态代理;

动态代理调用处理程序时要做什么;

第一步简单,第二步是核心。

调用处理程序

我们先来看第二步。

InvocationHandler 是 Java 和开发者之间的核心契约:

对于java动态生成的接口的实现类,对它的每一个方法调用都会转换为对指定InvocationHandler的调用 。

而handler的返回值就是动态代理方法被调用后的返回值 。

例如,下面的 InvocationHandler 实现会输出方法调用前后的当前时间:

import lombok.AllArgsConstructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.time.LocalDateTime;/** 
* 负责代理对象方法调用前后的bibi。
* 
* @author liuhaibo on 2018/04/19 
*/@AllArgsConstructorpublic class BibiAroundInvocationHandler implements InvocationHandler { 
    /** 
     * 被代理的对象。之所以使用它,是因为毕竟调用了原始方法。
     * 如果不需要代理对象,则不需要放入
     */ 
    private Coder coder; 
    /** 
     * 方法调用时将代理对象交给handler(回调handle的invoke方法)
     * 
     * @param proxy jvm 生成的代理对象,如果与生成的代理对象无关,该参数可以省略
     * @param method 要调用的代理对象的
     方法 * @param args 代理对象方法的参数被调用.method + args 可以判断调用哪个方法
     * @return 如果被调用的方法会返回一个值,一般返回值会原封不动的返回,当然也可以更改返回。
     * @throws Throwable 
     */ 
    @Override 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        System.out.println(">>> Before: "+ LocalDateTime.now());
        // 通过反射通过函数名、对象和函数参数调用这个函数。
        对象结果 = method.invoke(coder, args); 
        System.out.println("<<< 之后:"+ LocalDateTime.now()); 
        返回“(代理)”+结果;
    }}

实现一个InvocationHandler其实就是实现他的invoke接口。当调用动态类的方法时,Java会调用接口的invoke方法并传入三个参数:

proxy:Java生成的动态代理类生成的对象;

method,args:方法名和方法参数,这两部分可以结合起来确定类中的一个方法;

具体到上面的实现:

首先,我们不关心实际的动态代理对象,所以不使用proxy参数;

由于我们要在被代理对象的方法调用前后进行bibi,所以必须分为三个步骤:

在调用方法之前,强制它;

调用代理对象的方法。因此,在创建 BibiAroundInvocationHandler 时,会传入一个代理对象 Coder 来调用其方法。当调用被反射时使用: Method#invoke(object, args);

方法调用后,强制执行;

设置调用函数的返回值。首先要知道BibiAroundInvocationHandler#invoke的返回值会被Java作为动态代理类的方法调用后的返回值。这里我们只是想在方法调用前后bibi,无意中改变了代理对象方法的返回值,所以对于代理对象,我们应该把它的返回值作为BibiAroundInvocationHandler#invoke的返回值。但是在返回之前,我给返回值加了一个“(Proxy)”前缀。作为demo,我们可以更清楚的看到动态代理对象的返回值被我们故意改变了,而且比代理好对象的返回值。返回值有一个额外的前缀。

代理人

解决了InvocationHandler实现的核心问题,还有一个简单的问题没有解决:为哪个接口生成动态代理类。

Java的Proxy类提供了创建动态代理类的方法。有两个方法比较重要:

public static Class<?> getProxyClass(ClassLoader loader, Class<?>...interfaces):

用于生成给定接口的动态代理类。需要传入接口和接口的ClassLoader,ClassLoader一般可以直接传入接口的ClassLoader;

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException:

用于为给定借口生成动态代理对象。其实这个方法包括前面的方法。毕竟要生成对象,必须先生成一个代理类,然后再生成一个新的对象。所以该方法大致分为三个步骤:

生成动态代理类 Class<?> cl = getProxyClass0(loader, intfs):;

获取动态代理类构造函数:

private static final Class<?>[] constructorParams = { InvocationHandler.class }; final Constructor<?> cons = cl.getConstructor(constructorParams);

生成动态代理对象 cons.newInstance(new Object[]{h}):;

值得关注的是生成对象的步骤。 首先,使用反射获取生成的动态代理类的构造函数。构造函数的参数是InvocationHandler。 然后将开发者定义的 InvocationHandler 实例传入构造函数,生成动态代理对象。 所以我们可以推测, Java在生成动态代理类时,除了要实现接口中给出的方法外,还必须添加一个参数,即 InvocationHandler 的构造方法 。

从动态代理方法只能传入接口参数可知,Java的动态代理 只能为接口生成代理,不能为具体的类生成代理 。

生成动态代理类并执行方法代码:

    InvocationHandler handler = new BibiAroundInvocationHandler(coder); 
   // 直接生成代理对象实例(其实分两步:先在jvm中生成代理类Class对象,然后使用Class对象生成代理对象)
     编码器代理 = (编码器) Proxy.newProxyInstance(coder.getClass().getClassLoader(), coder.getClass().getInterfaces(), handler);     
     proxy.estimateTime("你好,世界"); 
     proxy.implementDemands("发送广告"); 
     System.out.println(proxy.comment());

输出结果:

>>> 之前:2020-08-02T22:44:18.720Tom:我将使用 20 小时。<<< 之后:2020-08-02T22:44:18.721>>> 之前:2020-08-02T22:44: 18.721Tom:我使用 Java 完成发送广告<<< 之后:2020-08-02T22:44:18.721>>> 之前:2020-08-02T22:44:18.721<<< 之后:2020-08-02T22: 44:18.721(代理)Java 是完美的!

前两个方法输出调用前后的时间。第三种方法也是输出时间,然后输出方法的返回值:一个以“(Proxy)”为前缀的原始返回值。

动态代理内部

以上分析了Java动态代理架构设计,Java与开发者之间的契约。现在我们来看看Java生成的代理类长什么样。

首先,使用java提供的ProxyGenerator类生成一个带有Coder接口的动态代理类,叫做DynamicCoder。然后将这个类的字节码写入一个文件:

/** 
* @author liuhaibo on 2018/04/19 
*/public class ProxyUtil { 
    public static void main(String[] args) throws IOException { 
        byte[] classFile = ProxyGenerator.generateProxyClass("DynamicCoder", JavaCoder.class.getInterfaces ()); 
        文件 file = new File("/tmp/DynamicCoder.class"); 
        FileOutputStream fos = new FileOutputStream(file); 
        fos.write(classFile); 
        fos.flush(); 
        fos.close(); 
    }}

然后使用IDE反编译字节码,大致看看生成的动态代理类的代码是什么样子的:

import example.proxy.Coder;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class DynamicCoder extends Proxy implements编码器{
    私有静态方法m1;
    私有静态方法 m5;
    私有静态方法 m4;
    私有静态方法 m3;
    私有静态方法 m2;
    私有静态方法 m0;
    公共 DynamicCoder(InvocationHandler var1) { 
        super(var1); 
    } 
    public final boolean equals(Object var1) { 
        try { 
            return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) { 
            throw var3; 
        } catch (Throwable var4) { 
            throw new UndeclaredThrowableException(var4); 
        } 
    } 
    public final void estimateTime(String var1) { 
        try { 
            super.h.invoke(this, m5, new Object[]{var1}); 
        } catch (RuntimeException | Error var3) { 
            throw var3; 
        } catch (Throwable var4) { 
            throw new UndeclaredThrowableException(var4); 
        } 
    } 
    public final void implementDemands(String var1) { 
        try { 
            super.h.invoke(this, m4, new Object[]{var1});
        } catch (RuntimeException | Error var3) { 
            throw var3; 
        } catch (Throwable var4) { 
            throw new UndeclaredThrowableException(var4); 
        } 
    } 
    public final String comment() { 
        try { 
            return (String) super.h.invoke(this, m3, (Object[]) null); 
        } 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 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); 
        } 
    }
    静态 {
        尝试 { 
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m5 = Class.forName("example.proxy.Coder").getMethod("estimateTime", Class.forName("java.lang.String")); 
            m4 = Class.forName("example.proxy.Coder").getMethod("implementDemands", Class.forName("java.lang.String")); 
            m3 = Class.forName("example.proxy.Coder").getMethod("comment"); 
            m2 = Class.forName("java.lang.Object").getMethod("toString"); 
            m0 = Class.forName("java.lang.Object").getMethod("hashCode"); 
        } catch (NoSuchMethodException var2) { 
            throw new NoSuchMethodError(var2.getMessage()); 
        } catch (ClassNotFoundException var3) { 
            throw new NoClassDefFoundError(var3.getMessage()); 
        } 
    }

首先,动态生成的代理类实现了Coder接口中的方法;

动态代理类确实有一个以InvocationHandler为参数的构造方法;

对于每个方法,动态代理都由 InvocationHandler 处理。在调用InvocationHandler的invoke时,按照合约约定,传入三个参数:动态代理对象本身、方法名、方法参数。

最后,每个方法的返回值就是调用invoke后的返回值。

由此,我们亲眼看到,当jvm按照约定运行时,Java确实为我们生成了一个动态代理类。

至此,我们可以得出一个结论:Java的动态代理并不神秘。其实它就像一个静态代理。创建一个类,将所有方法一一实现。这个实现可以像简单的静态代理类StaticProxy那样直接由代理对象来完成,也可以像Java生成的DynamicCoder那样由InvocationHandler来完成。

它们的本质是一样的:反正必须有这样一个代理类来生成代理对象。

它们的创建时间与创建者不同:静态代理类是开发者在编译前编写的;动态代理类由 jvm 在运行时生成。

动态代理应用

动态代理应用最广泛的应该是Spring中实现的面向切面编程(AOP,Aspect Oriented Programming)。

在上面的例子中,通过动态代理,我们简单的输出了所有方法调用前后的时间。这是一个“横切逻辑”的场景。

Java中的继承是一个“垂直”的系统,方法前后的输出时间像狗皮膏药一样贴在每个方法的前后,是继承无法解决的。 如果把狗皮膏药和业务逻辑写在一起(就像一个静态代理一样),显然是杂乱无章的,重复性很高(每条业务逻辑前后都要贴两块相同的膏药)。 如果将方法一分为二,狗皮贴一分为二,提供一种机制,在调用业务逻辑代码前后自动调用狗皮贴, 不仅实现了想要的功能,而且还将业务代码和“狗皮膏药代码”分开 (性能监控、访问控制)、事务管理、日志记录等)。 这是一种“水平提取”的思想 ,即AOP。 动态代理显然可以很好地做到这一点。

当然,Spring的AOP不仅有Java动态代理模式的实现,还有使用CGLib实现动态代理的。

JDK 与 CGLib

Java 的动态代理 只能为接口生成代理,不能为特定的类生成代理 。 而CGLib可以为具体的类生成动态代理,其思路大致如下:

动态生成一个类的子类;

这个子类可以拦截所有的父方法调用,并且在intercept方法中,完成父方法前后贴狗皮膏药的逻辑,就像JDK的InvocationHandler的invoke方法一样。

CGLib是底层字节码技术创建的子类,这个子类是代理类的代理。jdk就是为接口生成一个具体的实现类,用这个实现类代理另一个实现类。

另外CGLib创建动态代理比jdk慢,但是创建代理的效率比jdk创建的动态代理要高,所以如果要充当单例,最好使用CGLib。不过jdk动态代理的另一个好处是不需要引入额外的依赖,jdk就够了。CGLib技术显然需要引入第三方CGLib依赖。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>