【设计模式】代理模式

2023-11-08

Java 的代理模式是一种设计模式,它可以让一个对象(代理对象)代替另一个对象(目标对象)去执行一些操作,并且可以在执行前后添加一些额外的功能。代理模式可以实现对目标对象的功能扩展和保护。

Java 的代理模式有两种实现方式:静态代理和动态代理。静态代理是在编译时就生成了代理类的字节码文件,而动态代理是在运行时动态生成代理类并加载到 JVM 中。

静态代理

静态代理的实现步骤如下:

  • 定义一个接口及其实现类;
  • 创建一个代理类同样实现这个接口;
  • 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法;
  • 通过代理类来访问目标对象的方法,并且可以在方法执行前后添加自定义的操作。

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活。需要对每个目标类都单独写一个代理类,而且接口一旦新增加方法,目标对象和代理对象都要进行修改。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。从 JVM 层面来说,静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

动态代理

动态代理的实现方式有多种,比如 JDK 动态代理、CGLIB 动态代理等。JDK 动态代理是利用 Java 内部的反射机制生成一个实现了目标接口的匿名类,然后调用目标方法;CGLIB 动态代理是利用 ASM 开源包,对目标类的字节码文件进行修改,生成子类来处理目标方法。CGLIB 动态代理这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

JDK 动态代理

JDK动态代理是面向接口的,必须要实现了接口的业务类才生成代理对象。

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;

  2. 通过 Proxy.newProxyInstance() 创建实例,它需要3个参数:

    • 使用的 ClassLoader,通常就是接口实现类的 ClassLoader
    • 需要实现的接口数组,至少需要传入一个接口进去;
    • 用来处理接口方法调用的 InvocationHandler 实例。
  3. 将返回的 Object 强制转型为接口。

示例实现代码:

public interface RequestInterface {
    void request();
}

public class RequestInstance implements RequestInterface {
    @Override
    public void request() {
        System.out.println("print request.");
    }
}

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

public class JdkProxyHandler implements InvocationHandler {

    private Object instance;

    public JdkProxyHandler(Object instance) {
        this.instance = instance;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before instance execution");
        System.out.println("Method:" + method);
        method.invoke(instance, args);
        System.out.println("after instance execution");
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        RequestInstance instance = new RequestInstance();
        JdkProxyHandler proxyHandler = new JdkProxyHandler(instance);
        ClassLoader classLoader = instance.getClass().getClassLoader();
        RequestInterface requestInst = (RequestInterface) Proxy.newProxyInstance(
                classLoader, new Class[]{RequestInterface.class}, proxyHandler);
        requestInst.request();
    }
}

运行上述代码之后,控制台打印出:

before instance execution
Method:public abstract void jdk.instance.RequestInterface.request()
print request.
after instance execution

可以看出,通过代理的方式,已经成功增强了 RequestInstancerequest() 方法。

如果想通过 JDK 动态代理实现一个通用的代理,那么可以在代理处理器中实现一个创建代理的静态方法,通过传入不同的实现类来实现对同一接口、不同实现的通用代理。假设实现一个统计接口耗时的代理:

public class JdkProxyRuntimeHandler implements InvocationHandler {

    private Object target;

    public JdkProxyRuntimeHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long starTime = System.nanoTime();
        Object result = method.invoke(this.target, args);//@1
        long endTime = System.nanoTime();
        System.out.println(this.target.getClass() + " 方法耗时(纳秒): " + (endTime - starTime));
        return result;
    }

    /**
     * 用来创建targetInterface接口的代理对象
     *
     * @param target          被代理的对象
     * @param targetInterface 被代理的接口
     * @param <T> 实现类类名
     * @return 代理对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(Object target, Class<T> targetInterface) {
        if (!targetInterface.isInterface()) {
            throw new IllegalStateException("targetInterface 不属于接口类型!");
        } else if (!targetInterface.isAssignableFrom(target.getClass())) {
            throw new IllegalStateException("target 不是 targetInterface 接口的实现类!");
        }

        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new JdkProxyRuntimeHandler(target));
    }
}

public class Main {
    public static void main(String[] args) {
        RequestInterface proxyInstance = JdkProxyRuntimeHandler.createProxy(new RequestInstance(), RequestInterface.class);
        proxyInstance.request();
    }
}

上面当我们创建一个新的接口的时候,不需要再去新建一个代理类了,只需要使用 JdkProxyRuntimeHandler.createProxy 创建一个新的代理对象就可以了,方便了很多。

使用 JDK 动态代理需要注意几点:

  • JDK 中的 Proxy 只能为接口生成代理类,如果想给某个类创建代理类,那么需要用到 CGLIB 了
  • 当调用通过 Proxy 创建代理对象的任意方法时候,会被 InvocationHandler 接口中的 invoke 方法进行处理

CGLIB 动态代理

JDK 的动态代理只能为接口创建代理,在应用上具有局限性。在实际的场景中,我们的类不一定有接口,如果我们想为普通的类也实现代理功能,我们就需要用到CGLIB 来实现了。

CGLIB 是一个强大、高性能的字节码生成库,底层使用了 ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。它用于在运行时扩展 Java 类和实现接口,本质上它是通过动态生成一个子类去覆盖所要代理的类。Enhancer 可能是 CGLIB 中最常用的一个类,和 JDK 中的 Proxy 不同的是,Enhancer既能够代理一般的类,也能够代理接口。Enhancer 创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的 toStringhashCode 方法)。Enhancer 不能够拦截final方法,例如 Object.getClass() 方法,这是由 Java final 方法语义决定的。基于同样的道理,Enhancer 也不能对 final 类进行代理操作。

由于 Spring 已将第三方 CGLIB Jar 包中所有的类集成到 Spring 自己的 Jar 包中,因此可直接使用接口 MethodInterceptor 来实现代理。

CGLIB动态代理的基本实现代码:

public class RawRequestInstance {
    public String request() {
        System.out.println("print request.");
        return "print raw request.";
    }
}

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

public class CglibProxy {

    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 声明生成的代理类是某一个父类的子类
        enhancer.setSuperclass(this.target.getClass());
        // 指定回调方法,即代理中需要加强的内容
        enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
            System.out.println("before instance execution");
            System.out.println("Method:" + method);
            Object result = methodProxy.invokeSuper(o, objects);
            System.out.println("after instance execution");
            return result;
        });
        return enhancer.create();
    }
}

public class Main {
    public static void main(String[] args) {
        CglibProxy proxy = new CglibProxy();
        RawRequestInstance proxyInst = (RawRequestInstance) proxy.getInstance(new RawRequestInstance());
        System.out.println(proxyInst.request());
    }
}

运行上述代码之后,控制台打印出:

before instance execution
Method:public void instance.RawRequestInstance.request()
print request.
after instance execution
print raw request.

实际上,CGLIB 代理类在方法被调用时,里面的所有的方法都是会被拦截处理的,下面我们修改一下实现类 RequestInstance 的成员方法,然后再执行一次以上的代码:

public class RawRequestInstance {
    public String request() {
        System.out.println("print request.");
        return "print raw request." + request2();
    }

    public String request2() {
        System.out.println("print request2.");
        return "print raw request 2.";
    }
}

运行上述代码之后,控制台打印出:

before instance execution
Method:public java.lang.String instance.RawRequestInstance.request()
print request.
before instance execution
Method:public java.lang.String instance.RawRequestInstance.request2()
print request2.
after instance execution
after instance execution
print raw request.print raw request 2.

可见,即便 request2() 是在 request1() 中被调用,但它仍然被代理拦截进行处理。实际上这就是 Spring 中的 @Configuration 注解的实现方式,使用 CGLIB 代理拦截 @Bean 注解的方法,从而确保注入的被依赖 Bean 都是同一个。

同样地,我们实现一个通用的、用于统计接口耗时的代理:

public class CglibProxyRuntime implements MethodInterceptor {

    private Object target;

    public CglibProxyRuntime(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long starTime = System.nanoTime();
        Object result = method.invoke(target, objects);
        long endTime = System.nanoTime();
        System.out.println(method + " 方法耗时(纳秒): " + (endTime - starTime));
        return result;
    }

    /**
     * 创建任意类的代理对象
     *
     * @param target 被代理的对象
     * @param <T> 实现类类名
     * @return 代理对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target) {
        CglibProxyRuntime costTimeProxy = new CglibProxyRuntime(target);
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(costTimeProxy);
        enhancer.setSuperclass(target.getClass());
        return (T) enhancer.create();
    }
}

public class Main {
    public static void main(String[] args) {
        RawRequestInstance proxy = CglibProxyRuntime.createProxy(new RawRequestInstance());
        System.out.println(proxy.request());
    }
}

输出结果为:

print request.
print request2.
public java.lang.String instance.RawRequestInstance.request() 方法耗时(纳秒): 403800
print raw request.print raw request 2.

从上面的示例代码可知,CGLIB 代理拦截方法后的处理逻辑,主要是通过自定义一个 MethodInterceptor 接口并重写其方法,将该接口赋值给 enhancer 作为 callback 回调方法。这里的 MethodInterceptor 其实也是继承了 Callback 接口,因此我们还可以定义其他继承了 Callback 的接口,从而增强代理类的功能。

FixedValue 返回固定值
enhancer.setCallback(new FixedValue() {
    @Override
    public Object loadObject() throws Exception {
        return "**** Fixed Value ****";
    }
});

在上面的代码中直接修改 setCallback() 方法的赋值,最终输出结果为:

**** Fixed Value ****
NoOp.INSTANCE 拦截无操作

直接上代码:

enhancer.setCallback(NoOp.INSTANCE);

最终输出结果为:

print request.
print request2.
print raw request.print raw request 2.
CallbackFilter 不同方法实现不同拦截策略

定义一个新的实现类 MultiMethodInstance

public class MultiMethodInstance {

    public String runtime() {
        System.out.println("print runtime.");
        return "print runtime.";
    }

    public String fixedValue() {
        System.out.println("fixed value");
        return "fixed value";
    }

    public String doNothing() {
        System.out.println("----------- do nothing -----------");
        return "----------- do nothing -----------";
    }
}

然后定义一个新的代理拦截类:

import org.springframework.cglib.proxy.*;
import java.lang.reflect.Method;

public class CglibProxyFilter {

    private Object target;

    public Object getInstance(Object target) {
        this.target = target;

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());

        //创建多个Callback
        Callback[] callbacks = {
                // 拦截所有方法名含有 run 的方法
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        long starTime = System.nanoTime();
                        Object result = methodProxy.invokeSuper(o, objects);
                        long endTime = System.nanoTime();
                        System.out.println(method + ",耗时(纳秒):" + (endTime - starTime));
                        return result;
                    }
                },
                // 拦截所有方法名含有 fixed 的方法,返回固定值的
                new FixedValue() {
                    @Override
                    public Object loadObject() throws Exception {
                        return "**** Fixed Value ****";
                    }
                },
                // 拦截后无操作
                NoOp.INSTANCE
        };
        enhancer.setCallbacks(callbacks);

        // 设置过滤器 CallbackFilter,判断调用方法时使用哪个 Callback 来处理,返回的是 callbacks 数组的下标
        enhancer.setCallbackFilter(new CallbackFilter() {
            @Override
            public int accept(Method method) {
                String methodName = method.getName();
                return methodName.contains("run") ? 0
                        : (methodName.contains("fixed") ? 1 : 2);
            }
        });

        return enhancer.create();
    }
}

通过定义多个 Callback 接口的实现,以及提供判断每个方法调用时所选用的回调策略(拦截处理),来完成对同一个实现类中不同方法进行不同的代理拦截策略。具体执行哪个 Callback,会通过 CallbackFilter 中的 accept 方法来判断,这个方法返回 callbacks 数组的索引。上述代码的输出结果如下:

print runtime.
public java.lang.String instance.MultiMethodInstance.runtime(),耗时(纳秒):29824600
print runtime.
**** Fixed Value ****
----------- do nothing -----------
----------- do nothing -----------

多策略拦截代理还能进一步优化,使用 CallbackHelper 替代 CallbackFilter 能使代码结构更加清晰,扩展性更强。具体实现代码如下:

import org.springframework.cglib.proxy.*;
import java.lang.reflect.Method;

public class CglibProxyCallbackHelper {

    private Object target;

    public Object getInstance(Object target) {
        this.target = target;

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());

        // 定义所有可用的 Callback 接口方法
        Callback costTimeCallback = (MethodInterceptor) (Object o, Method method, Object[] objects, MethodProxy methodProxy) -> {
            long starTime = System.nanoTime();
            Object result = methodProxy.invokeSuper(o, objects);
            long endTime = System.nanoTime();
            System.out.println(method + ",耗时(纳秒):" + (endTime - starTime));
            return result;
        };
        Callback fixedValueCallback = (FixedValue) () -> "**** Fixed Value ****";
        Callback noOpCallback = NoOp.INSTANCE;

        // 定义拦截策略的判断逻辑
        CallbackHelper callbackHelper = new CallbackHelper(target.getClass(), null) {
            @Override
            protected Object getCallback(Method method) {
                String methodName = method.getName();
                return methodName.contains("run") ? costTimeCallback
                        : (methodName.contains("fixed") ? fixedValueCallback : noOpCallback);
            }
        };

        enhancer.setCallbacks(callbackHelper.getCallbacks());
        enhancer.setCallbackFilter(callbackHelper);

        return enhancer.create();
    }
}

优化后的输出结果不变,这里就不再展示了。

小结

Java 标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例。动态代理是通过 Proxy 创建代理对象,然后将接口方法“代理”给 InvocationHandler 完成的。动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程

JDK 动态代理和 CGLIB 动态代理的区别在于:

  • JDK 动态代理是面向接口的,必须要实现了接口的业务类才生成代理对象;而 CGLIB 动态代理则是通过生成业务类的子类作为代理类,无需实现接口,达到代理类无侵入,只操作关心的类,而不必为其他相关类增加工作量。
  • Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
  • Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB 使用 ASM 框架直接对字节码进行操作,在类的执行过程中比较高效。

在 SpringBoot 中,有很多地方应用了代理模式,比如 AOP、事务管理、缓存等。例如,当我们使用 @Transactional 注解来开启事务时,SpringBoot 会根据目标类是否实现了接口来选择使用 JDK 动态代理还是 CGLIB 动态代理来创建一个带有事务功能的代理对象。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【设计模式】代理模式 的相关文章

随机推荐

  • 自动化测试 - 如何自动提取手机短信验证码

    在自动化测试中 除了之前博客介绍的各种图形验证码 以及滑块验证外 经常会碰到当遇到有手机短信验证的问题 可能有人会想到 通常验证码有效期都会在一定的时间内 当再次测试时 可以把手机收到的验证码写在代码里 显然这方法好像可行 但却在整个测试中
  • Hadoop:HDFS--分布式文件存储系统

    目录 HDFS的基础架构 VMware虚拟机部署HDFS集群 HDFS集群启停命令 HDFS Shell操作 hadoop 命令体系 创建文件夹 mkdir 查看目录内容 ls 上传文件到hdfs put 查看HDFS文件内容 cat 下载
  • Python使用XPath解析HTML:从入门到精通

    引言 XPath是一种用于选择XML文档中节点的语言 它可以通过路径表达式来定位节点 由于HTML文档的结构与XML文档类似 XPath也可以用于解析HTML文档 Python是一种非常流行的编程语言 它提供了许多库用于解析HTML文档 本
  • VScode常见用法

    1 VScode中通过快捷键ctrl shift p 打开配置文件 2 VScode可以通过shift alt F进行代码格式化 3 自动保存 在设置按钮弹出的菜单中 选择 Settings 选项 此处是整个vscode的设置入口 打开 s
  • 数据结构学习系列之两个单向链表的合并

    两个单向链表的合并 创建两个单向链表p1和p2 合并p1和p2即可 代码如下 示例代码 int merge 2 link list node t p1 node t p2 if NULL p1 NULL p2 NULL p2 printf
  • OpenCV:模型训练与验证

    一 过拟合 欠拟合 1 概念 过拟合是指所选模型的复杂度比真模型更高 学习时选择的模型所包含的参数过多 对已经数据预测得很好 但是对未知数据预测得很差得现象 欠拟合是指所选模型得复杂度比真模型更低 学习时选择的模型所包含的参数过少 2 如何
  • 目标检测中PR曲线和mAP

    目标检测中的PR曲线绘制与mAP 1 基本概念 1 交并比 Intersection Over Union IOU 2 TP FP FN TN 3 查准率 查全率 4 AP mAP 2 PR曲线的绘制与mAP的计算 原文链接 目标检测中的m
  • 分治法基本思想(汉诺塔问题 Tower of Hanoi)

    文章目录 前言 基本思想 适用的问题 求解步骤 分治法要点 时间复杂性分析 举例 汉罗塔问题 Tower of Hanoi 问题描述 解决步骤 java代码 前言 分治法来源于孙子兵法谋攻篇中写道 十则围之 五则攻之 倍则战之 敌则能分支
  • 6款真正好用的播放器推荐

    GOM player GOM player 是一款本身装有视频播放所需的解码 及占用系统资源少 并且能以最优秀的画质来观看多种格式影片的播放程序 可以支持播放大多数当前流行的视频格式 如 MP4 AVI WMV MKV MOV FLV 等
  • 上帝模式下的shellcode

    github https github com Wker666 讲解视频 https www bilibili com video BV1oY411E7hX p 1 share medium iphone share plat ios sh
  • Mysql主从同步的坑,主库没问题、从库卡死

    Mysql主从同步的坑 主库没问题 从库卡死 mysql主从同步原理 什么是binlog日志 binlog的格式 问题 解决办法 mysql主从同步原理 mysql自带的主从同步机制 是主库定期给从库传输binlog日志 从库通过binlo
  • Java实现多人聊天室

    多人聊天室原理图 源码 工具类 该类用于关闭各种流 public class CloseUtil public static void CloseAll Closeable closeable for Closeable c closeab
  • linux中查几个看文件的命令[cat、more、less、tail、head]

    一 cat 显示文件连接文件内容的工具 cat 是一个文本文件 查看 和 连接 工具 通常与more搭配使用 与more不同的是cat可以合并文件 查看一个文件的内容 用cat比较简单 就是cat后面直接接文件名 如 cat etc pas
  • 杭州攻壳不维护服务器,[OL][公告] 《三国杀OL》合服热点问题FAQ(11月28日23点30更新)...

    自11月28日16点 三国杀OL 合服开服以来 我们陆陆续续收到来自社区 来自客服的玩家反馈 针对大家提出的问题我们在此统一回复下处理进度 1 游戏创建房间失败 提示 创建桌子错误 错误返回值为13 答 已修复 请刷新重登录游戏尝试 因频繁
  • 奥菲斯办公

    下载 运行 移除旧组件 移除之后重启电脑 部署 如图设置 下载 1 5GB 点击上方的开始部署 部署完成后 安装许可证 安装成功后才能安装许可证 找一个 KMS 主机输入 主机列表 点击上方激活 有效期179天
  • 笔记本电脑无法设置移动热点

    1 右击 此电脑 选择 属性 2 在弹出来的页面中 选择 设备管理器 3 点击 网络适配器 4 在 网络适配器 的下拉列表里 选择含有 WIFI 的一项 5 打开之后 点击 高级 选项 将属性 2 4GHz channel bandwidt
  • Java基础系列-《流程控制--循环结构》

    前言 张浩Java考试成绩未达到自己的目标 为了表明自己勤奋学习的决心 他决定写一百遍 好好学习 天天向上 根据前面学习的内容我们知道System out println 好好学习 天天向上 可以将这句话打印出来 但是现在是要打印100次
  • 攻防世界NaNNaNNaNNaN-Batman

    打开压缩包观察web100的内容 看起来好像代码是吧 改后缀html浏览器打开 这个东西没有用 继续 查教程 发现有大佬说因为前面有script 基本可以藕断这是js代码 然后大佬又告诉我说把乱码最后的那个 eval改为alert在用浏览器
  • 文件管理服务器数据库,会博通系统的海量数据库管理策略

    在会博通的用户中 有些用户的数据量已达100GB 200GB 甚至数TB以上 并且 数据量还在迅速增长之中 为了满组客户海量数据管理的需要 会博通从多数据库管理和数据库本身的存储管理两方面提出策略与建议 多数据库管理 会博通的企业版支持多数
  • 【设计模式】代理模式

    Java 的代理模式是一种设计模式 它可以让一个对象 代理对象 代替另一个对象 目标对象 去执行一些操作 并且可以在执行前后添加一些额外的功能 代理模式可以实现对目标对象的功能扩展和保护 Java 的代理模式有两种实现方式 静态代理和动态代