设计模式之代理模式(JDK动态代理、CGLIB动态代理)

代理模式

一、概述

由于某些原因需要给某对象提供一个代理以控制访问者对该对象的访问,这样,访问对象不能直接访问目标对象,代理对象作为访问对象和目标对象之间的中介,可以降低代码的耦合度并且保护了目标对象。

比如买房者不能直接访问售房者,售房中介充当代理。

代理模式的分类

  • 静态代理
    • 代理对象在编译期就生成
  • 动态代理
    • 代理对象在程序运行过程中动态的在内存中生成
    • 分为 JDK 动态代理和 CGLIB 动态代理两种

二、结构

代理模式(Proxy)分为三种角色:

  • 抽象主题类(接口): 通过接口或抽象类声明被代理类和代理类所要实现的业务方法
  • 被代理类: 实现了抽象主题类中的具体业务,是代理对象所代表的真实对象,是访问者最终要访问的对象
  • 代理类 : 实现了抽象主题类中的具体业务(与被代理类的业务相同),它可以访问、控制或扩展被代理类的功能

三、静态代理

静态代理的代理对象在编译期就生成

1. 案例说明

如果要买火车票的话,需要去火车站买票,但是火车站人流较大,显然比较麻烦。而火车站在多个地方都有代售点,去代售点买票就方便很多。这个火车站买票的例子就是典型的代理模式,火车站是被代理类,代售点是代理类。类图如下:

image-20210511155931224

其中 SellTickets 就是抽象主题类,代理类和被代理类都需要实现其具体方法

2. 代码实现

  • 抽象主题类

    public interface SellTickets {
    
        void sell();
    }
    
  • 被代理类(需要实现抽象主题类)

    public class TrainStation implements SellTickets {
    
        @Override
        public void sell() {
            System.out.println("火车站卖票");
        }
    }
    
  • 代理类(需要实现抽象主题类)

    public class ProxyPoint implements SellTickets {
    
        //声明火车站类对象
        private TrainStation trainStation  = new TrainStation();
    
        @Override
        public void sell() {
            System.out.println("代售点收取一些服务费用");
            
            //最终调用的是被代理类的方法
            trainStation.sell();
        }
    }
    
  • 进行测试

    public class Client {
        public static void main(String[] args) {
    
            //创建代理类对象
            ProxyPoint proxyPoint = new ProxyPoint();
    
            //调用方法进行卖票,访问的是代理类,但最终执行的是被代理类
            proxyPoint.sell();
            //代售点收取一些服务费用
            //火车站卖票
        }
    }
    
  • 从以上代码可以看出访问者访问的是代理类,也就是说代理类作为访问对象和目标对象的中介,同时也对被代理类的方法进行了增强(代理点收取一些服务费用)

四、JDK动态代理

  • 动态代理的代理对象在程序运行过程中动态的在内存中生成

  • Java 中提供了一个动态代理类 ProxyProxy 并不是上述所说的代理类,而是提供了一个创建代理对象的静态方法 newProxyInstance 来获取代理对象

1. 代码实现

对上述的火车站售票案例进行改进

  • 抽象主题类

    public interface SellTickets {
    
        void sell();
    }
    
  • 被代理类(需要实现抽象主题类)

    public class TrainStation implements SellTickets {
    
        @Override
        public void sell() {
            System.out.println("火车站卖票");
        }
    }
    
  • 生产代理对象的工厂(通过Proxy生成代理对象)

    public class ProxyFactory {
    
        //JDK动态生成的代理类也实现了抽象主题类,但是不用手动实现
    
        //声明被代理对象
        private final TrainStation station = new TrainStation();
    
        /**
         * 获取代理对象,调用Proxy.newProxyInstance获取,需要传递三个参数
         * @return 由于生成的代理类也实现了抽象主题类,所以返回类型为抽象主题类
         */
        public SellTickets getProxyObject() {
    
            /**
             * 参数说明:
             * ClassLoader loader : 类加载器,用于加载代理类,可以通过被代理对象获取类加载器
             * Class<?>[] interfaces : 代理类实现的接口(sellTickets)的字节码对象,被代理对象也实现了此接口,可以通过被代理对象获取父接口
             * InvocationHandler h : 代理对象的调用处理程序,是一个接口
             */
            SellTickets proxyObject = (SellTickets)Proxy.newProxyInstance(
    
                station.getClass().getClassLoader(), //第一个参数
    
                station.getClass().getInterfaces(), //第二个参数,getInterfaces方法获取这个对象所实现的父接口
    
                //第三个参数,需要重写invoke方法
                new InvocationHandler() {
    
                    //通过代理对象调用抽象主题类中的方法,实际上调用的是如下的invoke方法
    
                    /**
                     * invoke方法的参数说明:
                     * @param proxy 代理对象,即上述的proxyObject,在invoke方法中基本不用代理对象
                     * @param method 对抽象主题类中的方法进行封装的对象
                     * @param args 代理对象调用抽象主题类中的方法时传递的实际参数
                     * @return 代理对象调用抽象主题类中的方法的返回值
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                        System.out.println("invoke方法执行了");
    
                        //此时仅仅是执行了invoke方法,还没有执行被代理对象的方法
                        System.out.println("接下来执行被代理对象的方法");
    
                        //执行被代理对象的方法,最终执行的还是火车站售票的方法
                        Object obj = method.invoke(station, args); //反射机制
                        return obj;
                    }
                }
            );
    
            return proxyObject; //getProxyObject()方法的返回值
        }
    }
    
  • 进行测试

    public class Client {
        public static void main(String[] args) {
    
            // 获取代理对象
            SellTickets proxyObject = new ProxyFactory().getProxyObject();
    
            // 调用代理对象卖票的方法,代理对象是抽象主题类的子类,所以有sell方法
            proxyObject.sell();
            //invoke方法执行了
            //接下来执行被代理对象的方法
            //火车站卖票
        }
    }
    
  • 通过上述代码得出如下结论

    • 自定义的 ProxyFactory 和 JDK 提供的 Proxy 都不是代理类,代理类是程序在运行过程中动态的在内存中生成的类

    • 真正的代理类如下所示

      //运行如下代码
      //获取代理对象
      SellTickets proxyObject = new ProxyFactory().getProxyObject();
      
      //得到代理类
      System.out.println(proxyObject.getClass());
      //class com.sun.proxy.$Proxy0
      

2. 底层原理

对生成的代理类 $Proxy0 进行反编译得到如下内容:

//省略hashCode、toString、equals等在此处无用的方法

public final class $Proxy0 extends Proxy implements SellTickets {

    private static Method m3; //封装抽象主题类中的方法

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

    //将抽象主题类中的方法封装至m3
    static {
        m3 = Class.forName("com.qizegao.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
    }

    //代理类的sell方法,h表示的是父类Proxy中InvocationHandler的子类对象
    //所以代理对象调用抽象主题类中的方法时实际调用的是InvocationHandler重写的invoke方法
    public final void sell() {
        this.h.invoke(this, m3, null);
    }
}

//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {

    //仅展示与本例相关的源码

    protected InvocationHandler h;

    //将在代理对象生产工厂中创建的InvocationHandler赋值给Proxy的InvocationHandler
    protected Proxy(InvocationHandler h) {
        this.h = h;
    }
}

观察上述源码得到如下结论:

  • 代理类继承自 Proxy 并且实现了抽象主题类,印证了之前所说的 “JDK动态生成的代理类也实现了抽象主题类”
  • 自动生成Method对象,加载代理类时通过静态代码块会封装抽象主题类中的方法
  • 代理类中的 sell()方法又调用了 InvocationHandler 接口子实现类对象重写的 invoke() 方法
  • 最终 invoke() 方法通过反射调用了被代理对象的 sell() 方法

五、CGLIB动态代理

  • CGLIB全称 Code Generation Library
  • 动态代理的代理对象在程序运行过程中动态的在内存中生成
  • 如果没有定义抽象主题类,只定义了被代理类,显然 JDK 动态代理无法使用,因为 JDK 动态代理要求必须定义抽象主题类,使得代理类和被代理类可以实现其方法,这时就需要使用 CGLIB 动态代理
  • CGLIB 是一个功能强大,高性能的代码生成包,它可以为没有实现接口的类提供代理,为 JDK 动态代理提供了很好的补充
    • CGLIB 是第三方提供的包,所以需要引入jar包
    • 使用 CGLIB 生成的代理类是被代理类的子类
      • JDK 动态代理生成的代理类是抽象主题类的子类

1. 代码实现

对上述的火车站售票案例进行改进

  • pom.xml 中引入 CGLIB 的坐标

    <dependency>    
    	<groupId>cglib</groupId>    
    	<artifactId>cglib</artifactId>    
    	<version>2.2.2</version>
    </dependency>
    
  • 被代理类(没有实现抽象主题类)

    public class TrainStation {    
    
    	public void sell() {       
    		System.out.println("火车站卖票");    
    	}
    }
    
  • 生产代理对象的工厂(通过CGLIB生成代理对象)

//实现方法拦截接口MethodInterceptor,重写intercept方法,用于方法的拦截回调
public class ProxyFactory implements MethodInterceptor {

    //使用CGLIB生成的代理类是被代理类的子类

    //声明被代理类对象
    private final TrainStation station = new TrainStation();

    /**
     * 获取代理对象,通过CGLIB获取
     * @return 由于生成的代理类是被代理类的子类,所以返回类型为被代理类
     */
    public TrainStation getProxyObject() {

        //创建Enhancer对象,类似JDK动态代理中的Proxy类
        Enhancer enhancer = new Enhancer();

        //定义父类的字节码对象,即被代理类
        enhancer.setSuperclass(TrainStation.class);

        //定义回调函数,回调函数需要传递CallBack类对象
        //interface MethodInterceptor extends Callback,所以本类实现MethodInterceptor接口
        enhancer.setCallback(this);

        //创建代理对象
        return (TrainStation) enhancer.create();
    }

    //实现接口MethodInterceptor需要重写的intercept方法
    
    /**
     * 通过生成的代理对象调用sell方法时会自动的执行重写的MethodInterceptor中的intercept方法
     * 代理类是被代理类的子类,所以拥有sell方法
     * 前三个参数与JDK动态代理中InvocationHandler重写的invoke方法类似,不再赘述
     * 第四个参数MethodProxy表示代理类封装的sell方法的对象
     * @return 调用被代理类的方法时的返回值
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println("intercept方法执行了");

        //此时仅仅是执行了intercept方法,还没有执行被代理对象的方法
        System.out.println("接下来执行被代理类中的方法");

        //调用被代理类的方法,不是通过反射机制!!!
        //参数1表示代理对象,参数2表示方法的参数
        Object obj = methodProxy.invokeSuper(o, objects);
        return obj;
    }
}
  • 进行测试
public class Client {
    public static void main(String[] args) {

        //获取代理对象
        TrainStation proxyObject = new ProxyFactory().getProxyObject();

        //调用代理对象中的sell方法卖票
        proxyObject.sell();
        //intercept方法执行了
        //接下来执行被代理类中的方法
        //火车站卖票
    }
}

2. 底层原理

将动态代理生成的字节码文件保存在本地:

public class Client {
    public static void main(String[] args) {

        //将动态代理生成的字节码文件保存到本地指定目录
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:/Users/GAOqize/Desktop");

        //获取代理对象
        TrainStation proxyObject = new ProxyFactory().getProxyObject();
        proxyObject.sell(); //必须执行此方法才可以演示源码

        //获取代理类
        System.out.println(proxyObject.getClass());
        //com.qizegao.pattern.proxy.cglib_proxy.TrainStation$$EnhancerByCGLIB$$a33fd3be
    }
}

发现生成了三个字节码文件:

image-20210512221119395

通过 debug 可以发现,代理类的class文件是调用 Enhancer.create 时生成的,而两个 FastClass 文件是第一次调用 MethodProxy.invokeSuper 时才生成。

反编译代理类的class文件,部分内容如下:

//父类是被代理类
public class TrainStation$$EnhancerByCGLIB$$a33fd3be extends TrainStation implements Factory {
    private MethodInterceptor CGLIB$CALLBACK_0; //传入的方法拦截接口
    private static final Method CGLIB$sell$0$Method; //被代理类的sell方法

    private static final MethodProxy CGLIB$sell$0$Proxy; //代理类的sell方法

    static void CGLIB$STATICHOOK1() {
        //省略其余内容...
        //随着代理类的加载为两个sell方法的对象赋值
        CGLIB$sell$0$Method = ReflectUtils.findMethods(new String[] { "sell", "()V" }, (clazz2 = Class.forName("com.qizegao.pattern.proxy.cglib_proxy.TrainStation")).getDeclaredMethods())[0];
        CGLIB$sell$0$Proxy = MethodProxy.create(clazz2, clazz1, "()V", "sell", "CGLIB$sell$0");
    }

    //这个方法直接调用父类(被代理类的)sell方法
    final void CGLIB$sell$0() {
        super.sell();
    }

    //代理类中生成sell方法
    public final void sell() {
        if (this.CGLIB$CALLBACK_0 == null)
            CGLIB$BIND_CALLBACKS(this); 
        if (this.CGLIB$CALLBACK_0 != null)
            //此处会调用重写的MethodInterceptor对象的intercept方法
            return; 
        super.sell(); 
    }
}

通过分析代理类的 FastClass 文件,可以得出,通过它,直接调用到代理类的 CGLIB$sell$0 方法,相当于可以直接调用被代理类的 sell 方法,而不需要像 JDK 动态代理一样通过反射的方式调用,极大提高了执行效率。

六、三种代理模式的对比

1. JDK代理和CGLIB代理

  • CGLIB 底层采用ASM字节码生成框架,使用字节码技术生成代理类,在 JDK1.6 之前比使用 Java 反射效率要高,需要注意的是,CGLIB不能对声明为 final 的类或者方法进行代理,因为CGLIB原理是动态生成被代理类的子类
    • 如果被代理类或方法声明为 final ,其无法生成子类,无法代理
  • 在 JDK1.6、JDK1.7、JDK1.8 逐步对 JDK 动态代理优化之后,在调用次数较少的情况下,JDK 代理效率高于CGLIB,只有当进行大量调用的时候,JDK1.6 和 JDK1.7 比CGLIB代理效率低一点,但是到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理
    • 所以如果有抽象主题类(接口)则使用 JDK 动态代理,如果没有接口则使用 CGLIB 动态代理

2. 动态代理和静态代理

动态代理与静态代理相比,最大的好处是接口中声明的所有方法都被转移到一个集中的方法中处理,比如 JDK 最终调用的是 invoke() 方法,CGLIB最终调用的是 intercpt() 方法。这样,在接口方法数量比较多的时候,可以进行灵活处理。

对于静态代理,如果接口增加了一个方法,静态代理除了所有被代理类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度,而动态代理不会出现该问题(集中在一起通过反射调用)。

七、代理模式使用场景

  • 远程代理

    本地服务通过网络请求远程服务,为了保证良好的代码设计和可维护性,将远程网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心远程通信部分的细节(RPC的思想)

  • 防火墙代理

    将浏览器配置成使用代理功能时,防火墙就将浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把响应转给浏览器(VPN的思想)

  • 保护代理

    控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nice2cu_Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值