动态代理的理解

1. 代理模式

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能,而无需更改原来的代码。

这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法

在代码中,一般代理模式会被理解为为原来的代码增强一些功能,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

举个例子来说明代理的作用:

假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子。

代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象。

参考文章:

实战:使用代理模式切换数据源
彻底搞懂动态代理

1.1. 代理模式的应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

前面分析了代理模式的结构与特点,现在来分析以下的应用场景。

远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。

虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。

安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。

智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。

延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。

1.2. 静态代理

静态代理在使用时,需要定义接口或者父类(一般是抽象类),被代理对象与代理对象一起实现相同的接口或者是继承相同父类。

演出活动的接口:

package com.ethan.proxy;

/**
 *
 * 演出节目的接口
 *
 */
public interface Performance {
    //表演节目
    void showTime();
}

目标对象:明星:

package com.ethan.proxy;

/**
 *
 * 目标对象:明星
 */
public class SuperStar implements Performance{

    public void showTime() {
        System.out.println("我在舞台上唱跳rap~");
    }
}

代理对象:经纪人:

package com.ethan.proxy;

/**
 *
 * 代理对象:明星的经纪人
 *
 */
public class Agent implements Performance{

    //接收并引用目标对象
    private Performance target;

    public Agent(Performance target) {
        this.target = target;
    }

    public void showTime() {
        signContracts();
        target.showTime();
        takeCash();
    }

    //签约演出合同
    public void signContracts(){
        System.out.println("签约了演出的合同!");
    }

    //收取演出费用
    public void takeCash(){
        System.out.println("收取演出的费用!");
    }
}

测试代码:

package com.ethan.proxy;

import org.junit.Test;

public class ProxyTest {
    @Test
    public void test01(){
        SuperStar superStar = new SuperStar();
        Agent proxyAgent = new Agent(superStar);
        proxyAgent.showTime();
    }
}

1.2.1. 静态代理的总结

优点:

  • 可以做到在不修改目标对象的功能前提下,对目标功能扩展.

缺点:

因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.

同时,一旦接口增加方法,目标对象与代理对象都要维护,变相的增加了系统的复杂性.

在前面介绍的代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点。

1. 真实主题与代理主题一一对应,增加真实主题也要增加代理。
2. 设计代理以前真实主题必须事先存在,不太灵活。

如何解决静态代理中的缺点呢?答案是可以使用动态代理方式

1.3. 动态代理

1.3.1. JDK关键类的简要解析

在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类(该类文件时缓存在java虚拟机中的,不是显式被我们看到的)和动态代理对象(该对象是动态生成出来的,可以被引用到)。

分析Proxy类:

利用Proxy类的newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法,可以帮我们动态的生成所需要的代理对象。

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


loader: 用哪个类加载器去加载代理对象,一般是目标对象的类加载器

interfaces:动态代理类需要实现的接口    代理对象所实现的所有接口

h:实现了InvocationHandler接口的对象,该类中要重写实现invoke()方法,当代理对象的方法在执行时,会调用h里面的invoke方法去执行。

分析和InvocationHandler接口

我们需要的代理类可以认为是实现了InvocationHandler接口的类,必须重写invoke方法,而且重点是其第二个参数method就是动态代理类正在执行的方法,具体见代码中的分析。

只要将Proxy类和一个InvocationHandler接口理解了,那么就不难理解动态代理的实现了。

1.3.2. 测试代码

先将测试的代码写出来,再具体分析:仍然以明星和经纪人为例。

首先有一个接口,供代理对象和目标对象实现。

package com.ethan.proxy;

/**
 * 表演的接口,唱歌跳舞说唱的方法。
 */
public interface Performance {

    void Sing(String singName);
    void Dance();
    void rap();
}


然后必须存在一个目标对象:明星

package com.ethan.proxy;

/**
 * 目标对象:明星
 * 实现了表演的接口并重写其方法
 */
public class SuperStar implements Performance {

    private String name;

    public SuperStar() {

    }

    public SuperStar(String name) {
        this.name = name;
    }

    @Override
    public void Sing(String singName) {
        System.out.println(singName+": 唱歌中...");
    }

    @Override
    public void Dance() {
        System.out.println("跳舞中...");
    }

    @Override
    public void rap() {
        System.out.println("说唱中...");
    }

}

然后为了能够动态的生成代理对象,我们需要一个实现了InvocationHandler接口的代理类,可以认为该类就是我们的代理类,而且该类是Proxy类用来生成代理对象的必要参数。

package com.ethan.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ProxyHandler<T> implements InvocationHandler {
    //目标对象
    T target;

    public ProxyHandler(T target) {
        this.target = target;
    }

    /**
     * proxy:代表动态代理对象
     * method:代表正在执行的方法
     * args:代表调用目标方法时传入的实参
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        beforeShow();
        //疑问1:method.invoke的返回值是什么?返回值是目标对象所执行的方法的返回值。
        Object result = method.invoke(target, args);
        afterShow();

        return result;
    }

    void beforeShow(){
        System.out.println("beforeShow:演出之前需要签合同~");
    }

    void afterShow(){
        System.out.println("afterShow:演出之后需要收取费用~");
    }
}

测试:

package com.ethan.proxy;

import java.lang.reflect.Proxy;

public class Client {

    public static void main(String[] args) {
        SuperStar superStar = new SuperStar("刘德华");
        ProxyHandler<Performance> agentHandler = new ProxyHandler<>(superStar);
        //注意这里,是强转为接口类型,而不是具体的某个代理实现类,因为动态代理是动态生成代理类的,所以不存在具体的代理类。
        Performance proxyAgent = (Performance)Proxy.newProxyInstance(SuperStar.class.getClassLoader(), SuperStar.class.getInterfaces(), agentHandler);
        /**
         * 当调用动态代理对象proxyAgent的方法时,本质是调用了上述ProxyHandler类中的invoke方法。
         * 而invoke方法中第二个参数method代表的就是目标对象superStar此时执行的方法。
         * 所以invoke方法内在第二个参数method.invoke执行前后添加的语句,就是代理对象增强的功能。
         * 正如下所示,proxyAgent.Sing("忘情水");语句其实就是执行了ProxyHandler类中的invoke方法。
         */     
        proxyAgent.Sing("忘情水");
        proxyAgent.Dance();
    }
}


//输出结果:

beforeShow:演出之前需要签合同~
忘情水: 唱歌中...
afterShow:演出之后需要收取费用~
beforeShow:演出之前需要签合同~
跳舞中...
afterShow:演出之后需要收取费用~

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。

动态代理的过程,代理对象和目标对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中目标对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。

关于这一点,看下面的源码分析:对java动态代理的源码进行简要的分析,弄清楚其中缘由。

1.3.3. 动态代理源码的解析

对Proxy类的关键方法newProxyInstance的解析:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

上述代码中,关注Class<?> cl = getProxyClass0(loader, intfs);该语句生成了我们的代理类,后面代码中的构造器也是通过这里产生的类来获得,可以看出,这个类的产生就是整个动态代理的关键。这个类从头到尾都没有显式的出现在我们的面前,就是因为利用了反射的概念将该类存在了虚拟机中。

根据相关文章,对该类进行反编译,该类就可以显式的以代码的形式展现在我们的面前,引用来自其他博客:

动态代理的源码深入解析

jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会依次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。

代理类对象调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行目标对象的方法。

生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。

上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,对方法耗时统计。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。

1.4. Cglib代理

“代理”的目的是构造一个和被代理的对象有同样行为的对象,一个对象的行为是在类中定义的,对象只是类的实例。

所以构造代理,不一定非得通过持有、包装对象这一种方式。

通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时对这些方法增强,这就是cglib的思想。

上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理

Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.

JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用Cglib实现.

Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)

Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.

Cglib子类代理实现方法:

1.需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入pring-core-3.2.5.jar即可.

2.引入功能包后,就可以在内存中动态构建子类

3.代理的类不能为final,否则报错

4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.

示例代码:

明星类:含有一个final修饰的方法。

package com.ethan.cglib;

import com.ethan.proxy.Performance;

/**
 * 目标对象:明星
 * 
 */
public class Star {

    private String name;

    public Star() {

    }

    public Star(String name) {
        this.name = name;
    }


    public void Sing(String singName) {
        System.out.println(singName+": 唱歌中...");
    }


    public void Dance() {
        System.out.println("跳舞中...");
    }


    final public void rap() {
        System.out.println("说唱中...");
    }




}

一个实现了MethodInterceptor接口的类,重写intercept方法。

我们看一下intercept方法入参:

1)o表示增强的对象,即实现这个MethodInterceptor接口类的一个对象;

2)method表示要被拦截的方法;目标对象的方法

3)args表示要被拦截方法的参数;目标对象的方法的参数

4)proxy表示要触发父类的方法对象;
package com.ethan.cglib;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxyFactory<T> implements MethodInterceptor {
    //目标对象
    private T target;

    public CglibProxyFactory(T target) {
        this.target = target;
    }

    public Object getProxyInstance(){
        //工具类
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(target.getClass());
        //// 设置enhancer的回调对象,实现了MethodInterceptor接口的类的对象
        enhancer.setCallback(this);
        //创建子类代理对象
        return enhancer.create();

    }


    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("表演前签合同1~");
        Object obj  = methodProxy.invokeSuper(o,objects);
        System.out.println("表演后收取费用2~");
        return obj;
    }
}

测试代码:

package com.ethan.cglib;

public class Client {
    public static void main(String[] args) {
        Star star = new Star("刘德华");
        CglibProxyFactory<Star> proxyFactory = new CglibProxyFactory<Star>(star);
        Star proxyInstance = (Star)proxyFactory.getProxyInstance();
        proxyInstance.Dance();
        proxyInstance.Sing("忘情水");
        proxyInstance.rap();
    }
}

//输出结果

表演前签合同1~
跳舞中...
表演后收取费用2~
表演前签合同1~
忘情水: 唱歌中...
表演后收取费用2~
说唱中...

我们执行一下会发现rap因为加final修饰并没有被代理。

最后我们总结一下JDK动态代理和Gglib动态代理的区别:

1.JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。

2.JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。

3.JDK调用代理方法,是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。

posted @ 2021-07-15 16:49  ethanSung  阅读(220)  评论(0编辑  收藏  举报