美文网首页
学习JAVA中的动态代理

学习JAVA中的动态代理

作者: 我只要喝点果粒橙 | 来源:发表于2021-08-19 16:10 被阅读0次

    Q:首先得知道什么是代理?

    A:代理模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象

    为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。
    在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司。

    代理模式

    代理模式的主要角色如下。

    1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
    2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
    3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。 <----外部接口主要调用对象
    代理模式.png

    从UML图中,可以看出代理类与真正实现的类都是继承了抽象的主题类,这样的好处在于代理类可以与实际的类有相同的方法,可以保证客户端使用的透明性。

    静态代理和动态代理

    代理模式可以有两种实现的方式,一种是静态代理类,另一种是各大框架都喜欢的动态代理。

    静态代理

    静态代理模式的特点,代理类接受一个实现了Subject接口的对象,由于任何实现该接口的对象都可以通过代理类进行代理,从而增加了通用性。但是也有缺点,每一个代理类都必须实现一遍委托类(也就是real subject)的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。

    interface Person{
        void say();
    }
    
    class CommonPerson implements Person{
        @Override
        public void say() {
            System.out.println("I am just a normal person");
        }
    }
    
    class Lawyer implements Person{
        private Person commonPerson;
        Lawyer(Person p){
            commonPerson = p;
        }
    
        @Override
        public void say() {
            System.out.println("I am Lawyer");
            commonPerson.say();
            System.out.println("end");
        }
    }
    
    class Speaker{
        public static void main(String[] args) {
            Lawyer lawyer = new Lawyer(new CommonPerson());
            lawyer.say();
        }
    }
    

    静态代理的缺点

    虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来(需要实现Subject主题接口的缺点)。

    1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

    • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
    • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

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

    动态代理

    动态代理有别于静态代理,是根据代理的对象,动态创建代理类。这样,就可以避免静态代理中代理类接口过多的问题。动态代理是实现方式,是通过反射来实现的,1. 可以借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。 2. 通过CGLli库来实现

    为什么类可以动态的生成?

    这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

    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中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。

    常见的字节码操作类库

    这里有一些介绍:java-source.net/open-source…

    • 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动态代理

    注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦。

    JDK动态代理

    JDK提供的动态代理实现步骤如下:

    1. 编写一个委托类的接口,即静态代理的(Subject接口)
    2. 实现一个真正的委托类,即静态代理的(RealSubject类)
    3. 创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法
    4. 在测试类中,生成动态代理的对象。
    interface Subject {
        void say();
    }
    
    class RealSubject implements Subject {
        @Override
        public void say() {
            System.out.println("I am just a normal person");
        }
    }
    
    class DynasticProxy implements InvocationHandler {
        private Object o;
    
        public DynasticProxy(Object o) {
            this.o = o;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object invoke = method.invoke(o, args);
            return invoke;
        }
    }
    
    public class Dynastic {
        public static void main(String[] args) {
            DynasticProxy dynastic_proxy = new DynasticProxy(new RealSubject());
            Subject p =(Subject)Proxy.newProxyInstance(Dynastic.class.getClassLoader(),
                    new Class[]{
                            Subject.class
                    }, dynastic_proxy);
            p.say();
        }
    }
    

    创建动态代理的对象,需要借助Proxy.newProxyInstance。该方法的三个参数分别是:

    • ClassLoader loader表示当前使用到的appClassloader。
    • Class<?>[] interfaces表示目标对象实现的一组接口。
    • InvocationHandler h表示当前的InvocationHandler实现实例对象。

    此外, 使用JDK实现的代理,还有一个问题:Proxy.newProxyInstance的参数中有一个是委托类的接口,也就是说,▲如果使用JDK实现动态代理,则必须有委托接口(本体必须实现接口),而无法针对没有接口的类。对此情况,CGlib可以解决(是JDK代理的补充,不要求本体实现接口)

    CGLib动态代理

    什么是CGLib

    CGLib是一个强大的、高性能的代码生成库,它可以在运行期扩展Java类与实现Java接口。Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。例如Spring AOP就是他们提供方法的interception(拦截)。

    实现步骤如下:

    1. 导入CGlib库
    2. 定义委托类
    3. 创建代理类:实现一个MethodInterceptor接口,方法调用会被转发到该类重写的intercept()方法中。
    4. 使用
    class User {
        public String say(String msg){
            System.out.println("早上好"+msg);
            return  msg;
        }
    }
    
    public class CGDynastic implements MethodInterceptor {
        @Override
         // Object为由CGLib动态生成的代理类实例, Method为上文中实体类所调用的被代理的方法引用, Object[]为参数值列表, MethodProxy为生成的代理类对方法的代理引用。 变量名可以修改
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("这里可以插入执行关键代码之前的逻辑");
            // 调用代理类实例上的methodProxy方法的父类方法(即实体类SomeService中对应的方法),然后返回目标方法的返回值result,然后实现增强的逻辑
            Object o1 = methodProxy.invokeSuper(o, objects);//关键代码:
            System.out.println("这里可以插入执行关键代码之后的逻辑");
            return o1;
        }
    
        public static void main(String[] args) {
            // 代理类class文件存入本地磁盘方便我们反编译查看源码
            // System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
            Enhancer enhancer = new Enhancer();  // 通过CGLIB动态代理获取代理对象的过程
            enhancer.setSuperclass(User.class);     // 设置enhancer对象的父类
            enhancer.setCallback(new CGDynastic());    // 设置enhancer的回调对象
            User user = (User) enhancer.create();   // 创建代理对象
            String world = user.say("world");  // 通过代理对象调用目标方法
            System.out.println(world);
        }
    }
    
    
    // 或者
     class CGDynasticInterceptor implements MethodInterceptor {
        private Object object;
        public CGDynasticInterceptor(Object object) {
            this.object = object;
        }
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("这里可以插入执行关键代码之前的逻辑");
            method.invoke(object, args);
            System.out.println("这里可以插入执行关键代码之后的逻辑");
            return null;
        }
    }
    
    class CGDynastic {
        public static void main(String[] args) {
            CGDynasticInterceptor cgDynasticInterceptor = new CGDynasticInterceptor(new User());
            User user = (User)Enhancer.create(User.class, cgDynasticInterceptor);
            user.say("good");
        }
    }
    

    CGLIB 创建动态代理类的模式是:

    1. 查找目标类上的所有非final 的public类型的方法定义;
    2. 将这些方法的定义转换成字节码;
    3. 将组成的字节码转换成相应的代理的class对象;
    4. 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求

    原理:

    CGLib采用底层的字节码技术ASM, 可以为一个类创建子类, 在子类中采用方法拦截的技术拦截所有父类方法的调用, 并织入横切逻辑。(代理对象执行目标方法时会调用回调接口的方法即CGDynasticInterceptor的intercept)

    image

    流程:我们通过CGLIBEnhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个代理对象所有final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象调用其具体方法。CGLIGMethodInterceptor的作用跟JDK动态代理代理中的InvocationHandler很类似,都是方法调用的中转站。

    总结

    我们学习了通过CGLIB实现动态增强,但是CGLIB也有其缺陷,那就是必须目标类必须是可以继承的,如果目标类不可继承,那么我们就无法使用CGLIB来增强该类(因为CGLib是通过继承来实现的),现在我们已经学习完了Spring AOP中两种AOP的实现机制,我们可以称JDK动态代理实现的AOP为面向接口的动态增强,将CGLIB实现的AOP称为面向子类的动态增强

    参考:

    相关文章

      网友评论

          本文标题:学习JAVA中的动态代理

          本文链接:https://www.haomeiwen.com/subject/yjkabltx.html