代理模式详解和JDK动态代理、CGLIB动态代理

代理模式是最常用的设计模式之一,在Java的主流框架如:Spring、Mabits都有用到,因此学会代理模式,对于阅读框架源码也会有很大的帮助。本文主要介绍了代理模式、静态代理、JDK动态代理、CGLIB动态代理。阅读完本文,保证你对代理模式和Java动态代理如数家珍。看完之后如果真的还不懂,麻烦带上平底锅,顺着网线摸过来拍我[旺柴]。

一、代理模式概述

1. 代理模式定义

代理模式(Proxy Pattern),为其他对象提供一种代理(Proxy)以控制对这个对象的访问,是构造型的设计模式之一。

2. 代理模式UML类图

角色和职责分析:

  • Subject(抽象主题角色、公共接口、抽象类): 它为 RealSubject 和 Proxy 提供了公共接口,因为实现了和 RealSubject 一样的接口,Proxy 可以在 RealSubject 出现的任何地方取代它;
  • RealSubject(真实主题角色、被代理类、目标类): 是真正做事的对象,它是被 Proxy 代表和控制访问的对象;
  • Proxy(代理主题角色、代理类): 持有 RealSubject 的引用,某些例子中,Proxy 还会负责创建和销毁 RealSubject

二、 静态代理

1. 静态代理的概念

需要为公共接口编写相应的代理类代码。

2. 静态代理示例 ------ 海外代购

根据代理模式标准UML类图修改的海外代购类图如下:

随着经济发展,人们的生活水平逐渐提高,对于物质的需求也变得多样化,在本国产品无法满足的情况下,海外购物是一个很好的选择。这里的海外购物是抽象的购物主题。具体的购物主题分别包括"韩国购物"、"日本购物"等。这些都是"自己"去完成主题,但是有时候出于各种考虑,海外代购是更为划算的选择。作为海外代购,为了保证用户的权益,需要进行辨别真伪,另外出于法律规定海关安检也是必不可少的。因此,代理除了完成海外购物本身,还需要额外完成辨别真伪和海关安检。具体的代码实现如下:

java 复制代码
/**
 * 抽象主题
 */
public interface OverseaShopping {
    void buy();
}

/**
 * 具体的购物主题,韩国购物
 */
class KoreaShopping implements OverseaShopping {
    public KoreaShopping() {
    }

    public void buy() {
        System.out.println("购买了韩国的物品");
    }
}

/**
 * 具体的购物主题,日本购物
 */
class JapanShopping implements OverseaShopping {
    public JapanShopping() {
    }

    public void buy() {
        System.out.println("购买了日本的物品");
    }
}

class OverseaShoppingProxy implements OverseaShopping {
    private OverseaShopping overseaShopping;

    public OverseaShoppingProxy(OverseaShopping overseaShopping) {
        this.overseaShopping = overseaShopping;
    }

    public void buy() {
        discern();
        overseaShopping.buy();
        securityCheck();
    }

    void discern() {
        System.out.println("购买前辨别商品真伪");
    }

    void securityCheck() {
        System.out.println("过海关进行安检");
    }
}
java 复制代码
public void test01() {
    // 日本海外代购
    OverseaShopping japanShopping = new JapanShopping();
    OverseaShopping japanProxy = new OverseaShoppingProxy(japanShopping);

    japanProxy.buy();
    System.out.println("======================================");
    // 韩国海外代购
    OverseaShopping koreaShopping = new KoreaShopping();
    OverseaShopping koreaProxy = new OverseaShoppingProxy(koreaShopping);

    japanProxy.buy();
}

3. 静态代理存在的问题

  • 不利于代码的拓展,当公共接口新增方法时,所有实现类都需要一一实现新增的方法;
  • 需要为每一个公共接口编写相应的代理类代码,静态类文件数量过多,不利于项目管理,例如当我的增强逻辑需要修改时,每一个代理类都需要进行修改。以上述海外代购为例,当除了辨别真伪和海关安检,还需要购买运输保险,这个时候每个代理类的代码都需要进行修改。

静态代理虽然简单直观,但是在面对复杂系统和需求变化时存在明显的缺点,这个时候就需要依靠动态代理了。

三、动态代理

1. 动态代理的概念

在程序运行时,直接创建代理对象,运行目标方法和实现功能增强。这里有2个关键点,第一个是程序运行时,这也是动态的核心要义所在;第二个是直接创建代理对象,而并不像静态代理那样需要先编写代理类的代码源文件,然后再创建代理对象。动态代理是直接在程序运行时,利用动态字节码技术生成代理类的字节码并加载对应的Class对象和生成代理对象,Java种主要的动态代理有JDK动态代理CGLIB动态代理

2. JDK 动态代理

2.1 概述

JDK 动态代理开发主要用到 类java.lang.reflect.Proxy 和 接口 java.lang.reflect.InvocationHandler

Proxy 类

在以上介绍的代理模式的介绍中,我们已经知道,代理模式主要涉及3个角色,分别是公共接口、被代理类和代理类。应用代理模式实现功能的增强,公共接口和被代理类是给定的,而我们需要做的就是通过编码代理类来实现对被代理类的增强。Proxy 类就是负责生产代理类实例的。Proxy 类中最重要的就是提供给了一个静态方法 newProxyInstance,该方法声明如下:

java 复制代码
```
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
```

该类接受3个参数,并返回一个Object类型的对象,该返回值就是生成的代理对象,3个入参含义如下:

  • ClassLoader loader:类加载器,用于①加载类对应类的字节码文件,②创建类的Class对象,进而③创建该类的实例对象(加载字节码+创建对象)
  • Class<?>[] interfaces:被代理类所实现的接口,代理设计模式中第一个主题角色即代理类和被代理类所实现的公共接口
  • InvocationHandler h: 见如下 InvocationHandler 接口详解

InvocationHandler 接口

InvocationHandler 是用于编写增强逻辑代码的,该接口定义了一个 invoke 方法方法,该方法在代理对象被调用时执行。 invoke 方法声明如下:

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;

该方法有一个返回值,即被代理方法(代理类中需要增强的方法)的返回值 该方法接受3个参数:

  • Object proxy:代理对象
  • Method method:需要增强功能的被代理方法
  • Object[] args:被代理方法的参数

2.2 JDK 动态代理开发步骤

  1. 创建被代理类的实例
  2. 实现 InvocationHandler 的 invoke 方法,编写增强逻辑和执行被代理方法
  3. 生成代理对象
  4. 使用代理对象调用需要增强的方法的同名方法

2.3 JDK动态代理案例

在开发中,我们通常要分析接口的性能,这就要求记录下接口的执行耗时。现在有一个查询某个商品的详细信息接口,我们需要分析下该接口的耗时。案例代码如下:

java 复制代码
public interface GoodsService {
    void queryGoods();
}
java 复制代码
public class GoodsServiceImpl implements GoodsService {

    @Override
    public void queryGoods() {
        System.out.println("查询商品信息");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
java 复制代码
public class JdkProxyTest {
    public static void main(String[] args) {
        // 0. 被代理类的实例
        GoodsService goodsService = new GoodsServiceImpl();
        // 1. 实现增强逻辑
        InvocationHandler handler = new RecordTimeHandler(goodsService);

        // 2. 生成代理对象
        GoodsService goodsServiceProxy = (GoodsService) Proxy.newProxyInstance(JdkProxyTest.class.getClassLoader(), goodsService.getClass().getInterfaces(), handler);
        // 3. 使用代理对象调用需要增强方法的同名方法
        goodsServiceProxy.queryGoods();



    }
}

class RecordTimeHandler implements InvocationHandler {
    private Object realSubject;

    public RecordTimeHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object ret = method.invoke(realSubject, args);
        long end = System.currentTimeMillis();
        System.out.printf("查询商品信息耗时:%s ms\n", (end - start));
        return ret;
    }
}

前面我们说过,静态代理需要为每一个公共接口编写代理类代码,不利于项目管理,那么JDK动态代理呢?从以上案例可以看到,我们并没有去编写代理类的代码,只需要实现 InvocationHandler 的 invoke 方法,从而实现增强逻辑,假设现在我们有一个查询订单的接口,也需要记录耗时,这个时候,只需要使用新的被代理类对象创建一个handler,生成对应的代理类即可。同样,我们如果想改变记录耗时的方法,则只需要修改一个invoke方法实现即可,这大大降低了代码维护的难度。

记录订单查询接口耗时案例代码如下:

Java 复制代码
public interface OrderService {
    void queryOder();
}
Java 复制代码
public class OrderServiceImpl implements OrderService{
    @Override
    public void queryOder() {
        System.out.println("查询订单信息");
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
java 复制代码
public class JdkProxyTest {
    public static void main(String[] args) {
        // 0. 被代理类的实例
        GoodsService goodsService = new GoodsServiceImpl();
        // 1. 实现增强逻辑
        InvocationHandler handler = new RecordTimeHandler(goodsService);

        // 2. 生成代理对象
        GoodsService goodsServiceProxy = (GoodsService) Proxy.newProxyInstance(JdkProxyTest.class.getClassLoader(), goodsService.getClass().getInterfaces(), handler);
        // 3. 使用代理对象调用需要增强方法的同名方法
        goodsServiceProxy.queryGoods();

        // 记录查询订单接口耗时
        OrderService orderService = new OrderServiceImpl();
        InvocationHandler orderHandler = new RecordTimeHandler(orderService);
        OrderService orderServiceProxy = ((OrderService) Proxy.newProxyInstance(JdkProxyTest.class.getClassLoader(), orderService.getClass().getInterfaces(), orderHandler));
        orderServiceProxy.queryOder();

    }
}

class RecordTimeHandler implements InvocationHandler {
    private Object realSubject;

    public RecordTimeHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object ret = method.invoke(realSubject, args);
        long end = System.currentTimeMillis();
        System.out.printf("查询商品信息耗时:%s ms\n", (end - start));
        return ret;
    }

3. CGLIB 动态代理

CGLIB(Code Generation Library)是一个开源的第三方库,用于在Java运行时生成字节码并创建代理类。与JDK动态代理(基于接口)不同,CGLIB可以为普通类生成代理对象,即使它们没有实现任何接口。CGLIB使用ASM库来生成字节码,并通过继承的方式创建代理类,因此也被称为子类代理。

3.1 CGLIB 动态代理概述

CGLIB 动态代理主要涉及类 net.sf.cglib.proxy.Enhancer 和 接口net.sf.cglib.proxy.MethodInterceptor

Enhancer 类

学习CGLIB动态代理,可以与JDK动态代理对比学习,JDK动态代理通过调用Proxy类的静态方法newProxyInstance创建代理对象的,该方法主要的三个参数:ClassLoader loader , Class<?>[] interfaces , InvocationHandler h。CGLIB的是使用Enhancer类来创建代理对象的,它也需要三个关键的信息:类加载器、父类、回调函数。

  • 类加载器:与JDK动态代理的ClassLoader loader相同,用来加载字节码和创建对象
  • 父类类型:因为CGLIB是通过继承被代理类来实现代理的,因此需要指定被代理类的类型
  • 回调函数:Callback,这是一个回调接口,一边拿使用其子接口 MethodInterceptor,当代理对象被调用时会转到该接口的intercept方法

MethodInterceptor 接口 MethodInterceptorInvocationHandler 功能类似,这个接口只有一个方法 intercept,函数声明如下:

java 复制代码
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;

该方法同样有一个返回值即被代理方法的返回值 该方法接收4个参数:

  • Object obj:被代理对象
  • Method method:需要增强功能的被代理方法
  • Object[] args:被代理方法的参数
  • MethodProxy proxy:用于调用父类方法。

3.2 CGLIB 动态代理开发步骤

  1. 引入CGLIB依赖
  2. 创建被代理对象
  3. 实现MethodInterceptor 接口,编写增强逻辑代码和调用原始方法
  4. 创建Enhancer对象,并设置相关信息
  5. 调用Enhancer create方法,创建代理对象
  6. 调用代理对象方法

3.3 CGLIB 动态代理案例

如下案例主要是模拟一个登录接口,我们在登录前后分别进行登录校验和登录成功后输出成功信息。

java 复制代码
public interface UserService {
    void login();
}
java 复制代码
public class UserServiceImpl implements UserService{
    @Override
    public void login() {
        System.out.println("用户登录。。。");
    }
}
java 复制代码
public class CglibProxyTest {
    public static void main(final String[] args) {
        final UserService userService = new UserServiceImpl();


        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(CglibProxyTest.class.getClassLoader());
        enhancer.setSuperclass(UserServiceImpl.class);

        MethodInterceptor interceptor = new MethodInterceptor() {
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("登录前校验");
                Object ret = method.invoke(userService, args);
                System.out.println("用户登录成功");
                return ret;
            }
        };
        enhancer.setCallback(interceptor);
        UserService userServiceProxy = (UserService) enhancer.create();

        userServiceProxy.login();
    }
}

注:

执行以上代码可能会出现如下报错信息: java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @722c41f4

这是由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了,为了兼容原先的版本,需要在运行项目时添加 --add-opens java.base/java.lang=ALL-UNNAMED 选项来开启这种默认不被允许的行为

4. JDK 动态代理和 CGLIB 动态代理比较

  • JDK 动态代理是通过实现接口来创建代理对象的,因此使用JDK创建代理对象,被代理类一定要实现接口。
  • CGLIB 动态代理则是通过继承被代理类来创建代理对象的,那么这要求被代理类一定不能是final的。
  • 从性能上来说,CGLIB比JDK动态代理更快。

5. 拓展

以上讲解,都还只是在api应用层面,并没有深入到动态代理实现的底层原理,而对于底层的东西,比如字节码是如何生成的、如何加载的、对象是如何创建的、MethodInterceptor.intercept的第四个参数的作用是什么、为什么说CGLIB比JDK动态代理更快,本文并不打算详细介绍这些,旨在抛砖引玉,对这些感兴趣的同学可以探索。

总结

本文先介绍了代理模式的基本内容,包括定义、UML类图、各个角色的职责;接着分别介绍静态代理和动态代理,着重介绍了Java中实现代理模式的2种方式,每种方式的开发步骤、案例代码展示、应用条件等,学习本文,还应去体会到动态代理与静态代理相比的优势,这样才能更好地理解代理设计模式和更熟练地应用动态代理,同时在最后也指出更多值得探索的东西。

相关推荐
我明天再来学Web渗透10 分钟前
【hot100-java】【二叉树的层序遍历】
java·开发语言·数据库·sql·算法·排序算法
结衣结衣.24 分钟前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
原野心存27 分钟前
java基础进阶知识点汇总(1)
java·开发语言
无理 Java1 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
gobeyye1 小时前
spring loC&DI 详解
java·spring·rpc
鱼跃鹰飞1 小时前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
我是浮夸2 小时前
MyBatisPlus——学习笔记
java·spring boot·mybatis
TANGLONG2222 小时前
【C语言】数据在内存中的存储(万字解析)
java·c语言·c++·python·考研·面试·蓝桥杯
杨荧2 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源
Leighteen2 小时前
ThreadLocal内存泄漏分析
java