第 24 章 Java的静态代理模式和动态代理模式(随笔)

这是一篇详细且容易理解的代理模式讲解!

--------------------------------------------正文分割线-------------------------------------------------------------------

代理模式(Proxy Pattern)是程序设计中的一种设计模式,它的特征是代理类和委托类实现同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。总结为一句话:其他对象提供一个代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如:有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

代理模式要素:有共同接口、代理对象、目标对象。

代理模式的行为:由代理对象执行目标对象的方法、由代理对象扩展目标对象的方法。

代理模式的宏观特性:对客户端只暴露出接口,不暴露它以下的架构。

优点:

1)中间隔离了一层,更加符合开闭原则。

2)通过代理类处理委托类(目标类)的非核心工作,委托类可以专注做自己的事情,二者解耦,提高代码的可维护性。

3)无需干涉委托类的情况下,代理对委托类进行增强作用。

两种常用的代理模式:静态代理模式和动态代理模式。

1. 静态代理模式

代理对象与目标对象一起实现相同的接口或者继承相同父类,由程序员创建或特定工具自动生成源代码,即在编译时就已经确定了接口,目标类,代理类等,因此在程序运行之前,代理类的 .class 文件就已经生成。

要求:

1)代理类与委托类实现相同的接口

2)委托类必须要通过代理类来处理请求

来看代码例子:一个典型的案例就是老板通过秘书来处理日程安排的事情。

/**
 * 定义一个代理接口
 * @author hhfounder
 * @date 2023-11-16
 */
public interface BossSchedule {
    //会面
    void meeting(String name);
}

/**
 * 定义一个委托类Boss,实现了代理接口
 * @author hhfounder
 * @date 2023-11-16
 */
public class Boss implements BossSchedule{
    @Override
    public void meeting(String name) {
        System.out.println("5.请求发送给老板");
        System.out.println("6.老板收到秘书发来的信息,在"+LocalDate.now()+"日15点见"+name+"客户");
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

/**
 * 定义一个代理类Secretary,实现了代理接口
 * @author hhfounder
 * @date 2023-11-16
 */
public class Secretary implements BossSchedule{
    //代理类持有一个委托类的对象引用
    private Boss hhfounder;
    //代理类构造函数,对委托类对象(Boss类对象)进行初始化
    public Secretary(Boss hhfounder)
    {
        this.hhfounder=hhfounder;
    }
    @Override
    public void meeting(String name) {
        System.out.println("2.请求发送给秘书");
        System.out.println("3.秘书接待来访客户:"+name+",预约"+ LocalDate.now()+"日与老板会面");
        //鉴权
        if (name.equals("cory")){
            System.out.println("4.核实身份后,秘书将预约信息发送给老板,通知老板在" +LocalDate.now()+"日与"+name+"客户会面");
            //调用委托类方法
            hhfounder.meeting(name);
        } else {
            System.out.println("7.老板出差");
        }
        System.out.println("8.会面完成");
    }
}

/**
 * 定义main方法,程序入口
 * @author hhfounder
 * @date 2023-11-16
 */
public class MainTest {
    public static void main(String[] args){
        System.out.println("1.客户要见老板");
        //将委托类对象实例化传给代理类
        Secretary bossProxy=new Secretary(new Boss());
        //调用代理类的meeting方法
        bossProxy.meeting("cory");
    }
}

结果:
1.客户要见老板
2.请求发送给秘书
3.秘书接待来访客户:cory,预约2023-11-16日与老板会面
4.核实身份后,秘书将预约信息发送给老板,通知老板在2023-11-16日与cory客户会面
5.请求发送给老板
6.老板收到秘书发来的信息,在2023-11-16日15点见cory客户
8.会面完成

上面的代码用详细日志标出了调用流程,相信大家很容易看懂。用一个简单的流程图总结一下静态代理模式的调用关系:

这样做的优点显而易见,同一个代理类可以代理多个相似属性的委托类来处理业务请求,同时又兼有代理类的3大优点(见上文列出的代理模式优点)。

但是静态代理也有很明显的缺点,静态代理在编译时就已经确定了接口,委托类(目标类),代理类等,如果代理接口内的方法发生改变,所有接口相关的代理类和委托类的方法都要改变,代码的维护成本比较高,而且在实际的业务逻辑中,业务类的接口通常是不一样的,委托类也差异较大,可能很简单的需求因此需要写很多个不同的代理类和不同的接口,这样很容易造成代码和架构冗余,比如这样:

有没有更好的方法?可以让程序在运行时根据委托类(目标类)来动态生成代理类。

2. 动态代理模式

代理类在程序运行时才创建的代理方式被称为动态代理。在Java中有2种动态代理实现方式:JDK动态代理和CGLIB动态代理。例如Spring 中的AOP就是依靠动态代理来实现切面编程的。

2.1 JDK动态代理

JDK动态代理依靠反射来实现,代理类和委托类不需要实现同一个接口。委托类需要实现接口,否则无法创建动态代理,代理类在JVM运行时动态生成,而不是编译期就能确定。

如何实现JDK动态代理呢?我们需要JDK java.lang.reflect 包下的 Proxy 类 和 InvocationHandler接口,在调用委托类方法前通过调用代理类 InvokeHandler 的 invoke 方法来处理。

要点:

1)动态的生成代理类(.class文件),不需要手动写代理类,只需要维护委托类,接口以及代理处理类(InvocationHandler接口实现类,下文例子中的Secretary)的代码。

2)委托类(目标类)必须实现接口。

3)调用目标类的方法是通过JDK内部反射的方式调用的。

继续用上文中老板通过秘书来处理日程安排的例子,将上述例子改写为JDK动态代理模式,具体代码如下:

接口:接口代码不变。

/**
 * 代理接口
 * @author hhfounder
 * @date 2023-11-16
 */
public interface BossSchedule {
    //会面
    void meeting(String name);

}

委托类:委托类代码不变

/**
 * 委托类,实现了代理接口
 * @author hhfounder
 * @date 2023-11-16
 */
public class Boss implements BossSchedule{
    @Override
    public void meeting(String name) {
        System.out.println("5.请求发送给老板");
        System.out.println("6.老板收到秘书发来的信息,在"+LocalDate.now()+"日15点见"+name+"客户");
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

代理处理类:需要实现JDK动态代理InvocationHandler接口,重写invoke()方法,并通过method.invoke反射的方式调用委托类的方法,代码如下。

/**
 * 代理处理类,实现了JDK动态代理的InvocationHandler接口
 * @author hhfounder
 * @date 2023-11-16
 */
public class Secretary implements InvocationHandler {
    //代理对象持有一个委托类的对象引用,这里也可以将其类型定义为Object类型,因为InvocationHandler类型为Object
    private Boss hhfounder;
    //private Object hhfounder;

    //构造函数,对委托类对象(Boss类对象)进行初始化
    public Secretary(Boss hhfounder) {
        this.hhfounder=hhfounder;
    }

    //重写InvocationHandler类中的invoke()方法,在invoke()方法中可以写代理类代码的处理逻辑
    //通过method.invoke()反射的方式调用委托类的处理方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("2.请求发送给秘书");
        System.out.println("3.秘书接待来访客户:" +args[0]+",预约"+LocalDate.now()+"日与老板会面");
        //鉴权
        if (args[0].equals("cory")){
            System.out.println("4.核实身份后,秘书将预约信息发送给老板,通知老板在" +LocalDate.now()+"日与"+"客户会面");
            //通过method.invoke反射调用委托类方法并传入参数
           Object result=method.invoke(hhfounder,args);
            System.out.println("8.会面完成");
           return result;
        } else {
            System.out.println("7.老板出差");
            return null;
        }
    }
}

主程序请求入口:先获取委托类的代理对象,将代理对象作为参数传给JDK动态代理,创建一个动态代理对象。

public class MainTest {
    public static void main(String[] args){
        //将生成的代理文件存放到项目的根目录下,注意这是JDK1.8版本之后的设置
        System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

        System.out.println("1.客户要见老板");
        //将委托类对象实例化传给代理类
        Secretary bossProxy=new Secretary(new Boss());

        //调用JDK动态代理Proxy.newProxyInstance,生成JDK动态代理对象
        /**
         * 调用JDK动态代理,需要传入3个参数
         * @getClassLoader getClassLoader() 代理类加载器
         * @getInterfaces getInterfaces() 代理的接口(即委托类实现的接口)
         * @目标对象 bossProxy,即代理处理对象
         */
        BossSchedule bossDynamicProxy=(BossSchedule)Proxy.newProxyInstance(Boss.class.getClassLoader(),
                Boss.class.getInterfaces(),
                bossProxy);

        //JDK动态代理对象调用meeting方法
        bossDynamicProxy.meeting("cory");
    }
}

调用结果:

1.客户要见老板
2.请求发送给秘书
3.秘书接待来访客户:cory,预约2023-11-16日与老板会面
4.核实身份后,秘书将预约信息发送给老板,通知老板在2023-11-16日与客户会面
5.请求发送给老板
6.老板收到秘书发来的信息,在2023-11-16日15点见cory客户
8.会面完成

根据上例总结下JDK动态代理的调用逻辑,客户端的请求会先发给JDK动态代理Proxy,由JDKProxy动态生成代理类.class文件,然后调用InvocationHandler的实现类的对象,再通过InvocationHandler实现类中method.invoke方法反射调用委托类的方法。

可以看下JDK动态代理生成的文件$Proxy0.class,动态代理文件$Proxy0继承了Proxy并实现了目标类的接口BossSchedule,因此在客户端调用meeting方法时,会先调用JDK动态代理文件$Proxy0.class中的meeting()方法,然后再去调用InvocationHandler接口中的invoke()方法,如下代码:

super.h.invoke(this, m3, new Object[]{var1});

再通过反射调用目标类真实的meeting()方法。 


package jdk.proxy1;
import Proxy.BossSchedule;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

//动态代理文件$Proxy0继承了Proxy并实现了目标类的接口BossSchedule

public final class $Proxy0 extends Proxy implements BossSchedule {
    private static final Method m0;
    private static final Method m1;
    private static final Method m2;
    private static final Method m3;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    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);
        }
    }

    public final boolean equals(Object var1) {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } 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 void meeting(String var1) {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        ClassLoader var0 = $Proxy0.class.getClassLoader();

        try {
            m0 = Class.forName("java.lang.Object", false, var0).getMethod("hashCode");
            m1 = Class.forName("java.lang.Object", false, var0).getMethod("equals", Class.forName("java.lang.Object", false, var0));
            m2 = Class.forName("java.lang.Object", false, var0).getMethod("toString");
            m3 = Class.forName("Proxy.BossSchedule", false, var0).getMethod("meeting", Class.forName("java.lang.String", false, var0));
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {
        if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {
            return MethodHandles.lookup();
        } else {
            throw new IllegalAccessException(var0.toString());
        }
    }
}

用一个简单的流程图总结一下JDK动态代理模式的处理逻辑:

虽然JDK动态代理解决了静态代理中代码何结构冗余的问题,但JDK 动态代理是基于接口设计实现的,而对于没有接口的场景,JDK动态代理的方式就无法实现了,而且通过反射的方式调用委托类(目标类)可能会有性能较慢的问题(JDK1.8版本之后对反射调用做了优化,性能基本上能达到CGLIB动态代理调用目标类方法的性能,在某些场景下甚至超过CGLIB代理方式)。

有没有不需要接口就能实现动态代理的方式?还真有,下面一起来学习下cglib动态代理。

2.2 CGLIB动态代理

CGLIB(Code Generation Library)是一个强大的,高性能,高质量的Code生成类库,它主要利用 asm 开源包,将目标对象类的 class 文件加载进来,然后修改其字节码生成新的子类来进行扩展处理。因此,在使用cglib代理前需要先引入cglib包。

将上面的例子改写成CGLIB动态代理方式,需要注意的是,CGLIB动态代理方式采用继承委托类(目标类)的方式,因此不需要委托类(目标类)实现接口,代码例子如下:

委托类:不需要实现接口(区别于JDK动态代理)

/**
 * 委托类
 * @author hhfounder
 * @date 2023-11-16
 */
public class Boss {
    public void meeting() {
        System.out.println("4.请求发送给老板");
        System.out.println("5.老板收到秘书发来的信息,在"+LocalDate.now()+"日15点见客户");
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

代理类:实现MethodInterceptor接口,通过MethodInterceptor接口实现对委托类(目标)方法的拦截,CGLIB会首先调用拦截器的intercept方法,然后在该方法中调用目标方法,在目标方法调用前后,我们可以实现对目标方法的增强逻辑。

CGLIB代理类参数说明:

MethodInterceptor:拦截器接口,用于实现拦截逻辑。
Callback:是CGLIB的回调接口
MethodProxy:为实现对委托类方法的代理和拦截而设计,CGLIB会为每个方法生成对应的MethodProxy对象,用于代理委托类中的方法。

/**
 * 代理类,实现了CGLIB动态代理的MethodInterceptor接口
 * @author hhfounder
 * @date 2023-11-16
 */
public class Secretary implements MethodInterceptor {

    //代理类持有一个委托类的对象引用
    private Object hhfounder;
    //代理类构造函数,对委托类对象(Boss类对象)进行初始化
    public Secretary(Object hhfounder)
    {
        this.hhfounder=hhfounder;
    }

    /**
     * @param obj 表示要进行增强的对象
     * @param method 表示拦截的方法
     * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型
     * @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
     * @return 执行结果
     * @throws Throwable 异常
     */
    
    public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("2.请求发送给秘书");
        System.out.println("3.核实身份后,秘书将预约信息发送给老板,通知老板在" + LocalDate.now() + "日与客户会面");
        //methodProxy.invokeSuper调用的是obj(代理类,即Secretary类)的父类对象(即Boss类)
        Object object = methodProxy.invokeSuper(obj, objects);
        System.out.println("6.会面完成");
        return object;
    }
}

主程序:其执行流程参考上文CGLIB动态代理主要流程。

/** 定义main方法,程序入口
 * @Enhancer 是CGLIB的核心类之一,它可以动态地生成一个子类,并且拦截父类的所有方法。
 * @setSuperclass 设置父类,即需要增强的目标类
 * @setCallback 设置回调函数
 * @author hhfounder
 * @date 2023-11-16
 */
public class MainTest {
    public static void main(String[] args){
        System.out.println("1.客户要见老板");
        // 通过CGLIB动态代理获取代理对象的过程
        Enhancer enhancer = new Enhancer();
        // 设置enhancer对象的父类
        enhancer.setSuperclass(Boss.class);
        // 设置enhancer的回调对象
        enhancer.setCallback(new Secretary(Boss.class));
        // 调用create方法正式创建CGLIB动态代理对象
        Boss bossDynamicProxy= (Boss)enhancer.create();
        //通过CGLIB动态代理对象调用委托类方法
        bossDynamicProxy.meeting();
    }
}

执行结果:

1.客户要见老板
2.请求发送给秘书
3.核实身份后,秘书将预约信息发送给老板,通知老板在2023-11-16日与客户会面
4.请求发送给老板
5.老板收到秘书发来的信息,在2023-11-16日15点见客户
6.会面完成

梳理下CGLIB动态代理主要流程:

(1)客户端请求进来后,在实例化阶段会查找目标类上的所有非final的public类型的方法定义,通过Enhancer类创建一个代理对象,该代理对象是目标类的子类,因此可以继承目标类的所有方法和属性;
(2)将符合条件的方法定义转换成字节码;
(3)将组成的字节码转换成相应的代理的class对象;
(4)实现MethodInterceptor接口,通过MethodInterceptor接口实现对委托类(目标)方法的拦截,在拦截器中,客户端可以实现对目标类方法的增强逻辑,用来处理对代理类上所有方法的请求;

(5)当代理对象调用目标方法时,CGLIB会首先调用拦截器的intercept方法,然后在该方法中调用目标方法,在目标方法调用前后,我们可以实现对目标方法的增强逻辑。最后,CGLIB会将增强后的方法返回给代理对象,从而实现对目标方法的动态代理。

上面的流程用流程图表示:

可能有同学会有疑问,methodProxy.invokeSuper()是如何调用到目标类的方法呢?

具体过程为:fastclass文件会为目标类的每一个方法(上例中的Boss类的meeting()方法)创建一个索引值,通过索引值可以快速定位到这个方法(meeting()方法),然后回到CGLIB生成的代理类.class文件(Secretary***.class),调用代理类文件中的子类(meeting)方法,再调用其父类方法(meeting()方法),用一个简单流程图说明下:

3. 总结

最后,再总结下各自优缺点。

1. 静态代理虽然逻辑简单且具备代理模式的基本优点,但很容易造成代码和架构冗余。

2. JDK的动态代理是基于接口实现的,无接口场景下不能使用,而CGLIB的动态代理是基于继承实现的,有没有接口都能使用。
3. JDK动态代理类和CGLIB动态代理都是在运行期生成字节码,但JDK动态代理字节码生成快于CGLIB。
3. JDK动态代理执行方法时是基于反射调用,而CGLIB动态代理将为方法生成代理对象MethodProxy,JDK1.8版本之前JDK动态代理效率更低,CGLIB执行效率更高,而JDK1.8之后二者执行效率无明显差异。

码字不易,希望大家都能点个赞哈,谢谢啦~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值