都2024年了,还有人不懂动态代理么?

news2024/11/24 13:28:26

文章目录

  • 一、定义
  • 二、静态代理
  • 三、动态代理
    • 1. JDK代理
      • 1.1 JDK代理实现流程
      • 1.2 动态生成的类字节码
    • 2. Cglib代理
      • 2.1 Cglib实现流程
  • 四、总结

一、定义

静态代理和动态代理都反映了一个代理模式,代理模式是一种经典的设计模式,常用于为其他对象提供一种代理或占位符,以限制对它的访问,充当访问对象的中介
就好比平时我们租房,会和中介进行沟通,中介带我们去看房,中介又与房东保持联系,但房东不会带我们看房。


二、静态代理

静态代理是一种代理模式的实现,静态代理在编译时就确定了代理类和目标类之间的关系。
在静态代理中,代理类和目标类通常实现相同的接口,或者代理类继承目标类。
代理类作为目标类的包装,持有目标类的引用,并且调用目标对象的方法前后进行增强操作。
下面举个例子

public interface Landlord {

    /**
     * 出租房间
     */
    void rentingHouse();

}
public class ShenZhenLandlord implements  Landlord {
    @Override
    public void rentingHouse() {
        System.out.println("深圳房东出租房间");
    }
}
public class Intermediary implements Landlord{

    /**
     * 代理类
     */
    private final Landlord landlord;

    public Intermediary(Landlord landlord) {
        this.landlord = landlord;
    }

    @Override
    public void rentingHouse() {
        System.out.println("中介带租客看房");
        landlord.rentingHouse();
        System.out.println("中介收取出租非");
    }
}
public static void main(String[] args) {
    // 目标类
    Landlord landlord = new ShenZhenLandlord();
    //代理类
    Intermediary intermediary = new Intermediary(landlord);
    // 运行代理方法
    intermediary.rentingHouse();
}

image-20240623131158278

这是一个经典的代理模式的实现,可以在不修改原对象的情况下对目标类进行增强处理,但是若接口新增方法,所有代理类都都需要新增对应的实现,不好维护。


三、动态代理

动态代理是一个在运行期间动态创建代理对象的技术,它允许开发者为一个或多个接口创建一个代理对象,且无需事先知道具体实现类。

静态代理和动态代理的区别在于class文件是编译期间确定还是运行期间确定
静态代理在编写代码的时候就明确了代理类和目标类之间的关系,在编译阶段会生成一个Class对象。
动态代理则是在运行阶段动态生成代理对象,在运行的时候动态生成字节码,并加载到JVM中去


1. JDK代理

JDK代理技术是Java提供的一种动态代理技术,使用方式如下

public class ProxyFactory {
    /**
     * 目标类
     */
    private Object target;

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

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
            // 目标类加载器
            target.getClass().getClassLoader(),
            // 目标对象的接口类型
            target.getClass().getInterfaces(),
            // 事件处理
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("前置增强");
                    method.invoke(target, args);
                    System.out.println("后置增强");
                    return null;
                }
            }
        );
    }
}
public static void main(String[] args) {
    ShenZhenLandlord landlord = new ShenZhenLandlord();
    System.out.println(landlord.getClass());
    Landlord proxyInstance = (Landlord) new ProxyFactory(landlord).getProxyInstance();
    proxyInstance.rentingHouse();
    System.out.println(proxyInstance.getClass());
}

image-20240623131414977

代理类打印出的Class为com.sun.proxy.$Proxy0, 从ShenZhenLandlord到 com.sun.proxy.$Proxy0 ,其中经历了什么呢?


1.1 JDK代理实现流程

需要知道的是JVM虚拟机的类加载过程分为加载、验证、准备、解析、初始化这五个阶段, 在加载阶段需要进行下面这几个步骤:

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态结构转化为方法区的运行时数据结构
  3. 内存生成一个代表这类的Class对象,作为方法区这个类的各种数据访问入口

而获取类的二进制字节流,JVM提供了三种途径:

  1. 本地获取字节码,比如前面的ShenZhenLandlord类,编译后就属于本地的字节码
  2. 从网络中获取,可以使用URLClassLoader来加载类
  3. 运行时计算生成,在程序运行的过程中动态生成类字节码

而代理就是运行时动态生成字节码,然后交给JVM进行类加载过程使用。
所以经过JDK代理后,从ShenZhenLandlord变成了com.sun.proxy.$Proxy0,也就是程序在运行中通过计算,生成了$Proxy0代理类的类字节码。


1.2 动态生成的类字节码

由于动态生成的类字节码是动态计算出来并加载到JVM内存中去,因此无法通过查看编译后的文件去查看这个代理类的 Class文件。
不过,可以通过arthas工具来查看动态生成的类字节码。
下载官网:https://arthas.aliyun.com/doc/download.html#%E7%94%A8-arthas-boot-%E5%90%AF%E5%8A%A8
输入命令java -jar arthas-boot.jar 运行arthas

image-20240623131648385

proxy.jdk.Main 就是我们要查看的目标类,按3进入监控界面,如下图所示就代表进入成功了

img

输入命令jad com.sun.proxy.$Proxy0命令,就能解析出$Proxy0的源码了

img

整理一下代码,如下:

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

    // 构造参数为InvocationHandler
    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m3 = Class.forName("proxy.static_proxy.Landlord").getMethod("rentingHouse", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    // Landlord接口的`rentingHouse`方法
    public final void rentingHouse() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

看一个类,首先先看构造方法,KaTeX parse error: Expected 'EOF', got '#' at position 70: …就是`ProxyFactory#̲getProxyInstanc…Proxy0`的构造方法传进去的,也就是是同一个!!

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
            // 目标类加载器
            target.getClass().getClassLoader(),
            // 目标对象的接口类型
            target.getClass().getInterfaces(),
            // 事件处理
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("前置增强");
                    method.invoke(target, args);
                    System.out.println("后置增强");
                    return null;
                }
            }
        );
    }

我们稍微看一下Proxy的源码

private static final Class<?>[] constructorParams =
    { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
throws IllegalArgumentException
{
    final Class<?>[] intfs = interfaces.clone();
    /*
     * 获取动态生成的字节码
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * 获取这个代理类的构造参数为InvocationHandler的构造方法
     */
    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;
            }
        });
    }
    // 将h作为构造参数创建代理类
    return cons.newInstance(new Object[]{h});
}

那么在$Proxy0#rentingHouse方法调用了构造参数传进来的InvocationHandlerinvoke方法,也就是调用了下面这个方法(增强后的方法)

new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强");
        method.invoke(target, args);
        System.out.println("后置增强");
        return null;
    }
}

通过这,就可以得出下面的结论:

  1. 代理类实际上就是实现了Landlord接口,重写了rentingHouse方法
  2. 当外界调用代理类的rentingHouse方法的时候,实际上就是调用了new InvocationHandler的invoke方法

2. Cglib代理

Cglib(Code Generation Library)是一个强大的高性能代码生成库,用于在运行时扩展Java类和实现接口。它通过字节码技术动态生成新的类,从而实现对目标类的代理。Cglib代理是AOP(面向切面编程)中常用的一种代理方式,尤其是在需要代理那些没有实现接口的类时。
举个例子:
目标类如下

public class UserServiceImpl {
    public String selectList(){
        System.out.println("正在查询");
        return "小明, 小红, 小黄";
    }
}
public class UserLogProxy implements MethodInterceptor {

    /**
     * 生成 CGLIB 动态代理类方法
     *
     * @param target
     * @return
     */
    public Object getLogProxy(Object target) {
        // 增强器类,用来创建动态代理类
        Enhancer enhancer = new Enhancer();

        // 设置代理类的父类字节码对象
        enhancer.setSuperclass(target.getClass());

        // 设置回调
        enhancer.setCallback(this);

        // 创建动态代理对象并返回
        return enhancer.create();

    }


    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始查询");
        // 执行原始方法
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("查询完毕");
        return result;
    }
}

public class MethodProxy{

   public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        // 目标对象
        UserServiceImpl userService = new UserServiceImpl();
        System.out.println(userService.getClass());

        // 代理对象
        UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
        System.out.println(proxy.getClass());

        String userList = proxy.selectList();
        System.out.println(userList);

        while (true) {

        }
    }
}

image.png


2.1 Cglib实现流程

使用arthas工具,将proxy.cglib.UserServiceImpl$$EnhancerByCGLIB$$b96d19a3类的代码获取出来。

public class UserServiceImpl$$EnhancerByCGLIB$$b96d19a3
extends UserServiceImpl
implements Factory {

    private MethodInterceptor CGLIB$CALLBACK_0;

    public final String selectList() {
        // 是否设置了回调
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl$$EnhancerByCGLIB$$b96d19a3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        // 设置回调,需要调用 intercept 方法
        if (methodInterceptor != null) {
            return (String)methodInterceptor.intercept(this, CGLIB$selectList$0$Method, CGLIB$emptyArgs, CGLIB$selectList$0$Proxy);
        }
        // 无回调,调用父类的 findUserList 即可
        return super.selectList();
    }

}

在JVM编译阶段,Enhancer会根据目标类信息去动态生成代理类并设置回调。
当用户通过Cglib动态代理执行selectList方法的时候,会直接调用methodInterceptor.intercept方法,在intercept方法中通过invikeSuper调用父类的selectList方法。
如果没有设置回调,则直接调用父类的selectList方法。


四、总结

JDK代理与Cglib代理对比

  1. Cglib实现的动态代理是基于ASM字节码生成框架实现的,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高一点。但是需要注意,Cglib代理技术不可对final类进行类或方法的代理,因为Cglib的原理是动态生成代理类的子类实现的。
  2. 在JDK1.6以后的版本已经开始对JDK代理进行优化(具体优化了什么 以后有机会再写篇文章讲),在调用次数较少的情况,JDK代理的效率要比Cglib代理效率要高,只有进行大量调用的时候,Cglib的优势才会出现。而到了JDK1.8,JDK代理的效率已经高于Cglib代理,因此在有接口的情况下推荐优先使用JDK代理,没接口优先推荐使用Cglib代理技术。

动态代理与静态代理的比较

  1. 动态代理的最大优势就是在于可以把接口中声明的所有方法转移到调用处理器的一个集中的方法进行处理(Invocation.invoke),在接口数量比较多的情况下,可以进行灵活处理。
  2. 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1852932.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

字节跳动:从梦想之芽到参天大树

字节跳动掌舵人&#xff1a;张一鸣 2012年&#xff1a;梦想的起点&#xff1a;在一个阳光明媚的早晨&#xff0c;北京的一座普通公寓里&#xff0c;一位名叫张一鸣的年轻人坐在电脑前&#xff0c;眼中闪烁着坚定的光芒。他的心中有一个梦想——通过技术改变世界&#xff0c;让…

AcWing 1801:蹄子剪刀布 ← 模拟题

【题目来源】https://www.acwing.com/problem/content/1803/【题目描述】 你可能听说过“石头剪刀布”的游戏。 这个游戏在牛当中同样流行&#xff0c;它们称之为“蹄子剪刀布”。 游戏的规则非常简单&#xff0c;两头牛相互对抗&#xff0c;数到三之后各出一个表示蹄子&#x…

32 - 判断三角形(高频 SQL 50 题基础版)

32 - 判断三角形 select *,if(xy>z and xz>y and zy > x,Yes,No) triangle fromTriangle;

webpack处理js资源10--webpack入门学习

处理 js 资源 有人可能会问&#xff0c;js 资源 Webpack 不能已经处理了吗&#xff0c;为什么我们还要处理呢&#xff1f; 原因是 Webpack 对 js 处理是有限的&#xff0c;只能编译 js 中 ES 模块化语法&#xff0c;不能编译其他语法&#xff0c;导致 js 不能在 IE 等浏览器运…

QtCreator/VS中制作带有界面的动态库

1、首先创建动态库项目 class UNTITLED25_EXPORT Untitled25 {public:Untitled25(); };2、直接右键创建同名窗口类进行覆盖 3、引入global头文件并添加到处宏</

[机器学习算法]支持向量机

支持向量机&#xff08;SVM&#xff09;是一种用于分类和回归分析的监督学习模型。SVM通过找到一个超平面来将数据点分开&#xff0c;从而实现分类。 1. 理解基本概念和理论&#xff1a; 超平面&#xff08;Hyperplane&#xff09;&#xff1a;在高维空间中&#xff0c;将数据…

Git学习2 -- VSCode中的Git

看了下&#xff0c;主要的插件有3个。自带的Source Control。第1个是Gitlens&#xff0c;第2个是Git Graph。第三个还有个git history。 首先是Source Control。界面大概是这样的。 还是挺直观的。在第一栏source control&#xff0c;可以进行基本的git操作。主要的git操作都是…

qt开发-11_Dialog 仿苹果支付界面

QDialog 是 Qt 框架中用于创建对话框的一个基类。对话框是一种特殊类型的窗口&#xff0c;通常用于短暂的交互和信息交换&#xff0c;如接收用户输入、显示消息、询问用户决定等。QDialog 提供了一种方便的方式来实现这些功能&#xff0c;并能够控制用户与其他窗口的交互性&…

Android模拟器linux内核的下载,编译,运行,驱动开发测试

Android模拟器linux内核的下载&#xff0c;编译&#xff0c;运行&#xff0c;内核模块开发 1.下载适合Android模拟器的内核 git clone https://aosp.tuna.tsinghua.edu.cn/android/kernel/goldfish.git git branch -a git checkout android-goldfish-4.14-gchips 新建一个目录…

喜报!极限科技新获得一项国家发明专利授权:“搜索数据库的正排索引处理方法、装置、介质和设备”

近日&#xff0c;极限数据&#xff08;北京&#xff09;科技有限公司&#xff08;简称&#xff1a;极限科技&#xff09;新获得一项国家发明专利授权&#xff0c;专利名为 “搜索数据库的正排索引处理方法、装置、介质和设备”&#xff0c;专利号&#xff1a;ZL 2024 1 0479400…

谈谈跳台阶算法的记忆法和编程理念|青蛙跳台阶|递归|动态规划|算法|程序员面试|Java

简介 为什么会写这篇文章&#xff1f; 因为鄙人刷过此题多次&#xff0c;每次觉得自己会了&#xff0c;可下次还是不能一下子写出题解&#xff0c;故记录下我是如何记忆此题的&#xff0c;并且探索一些编程理念。 题目 一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级…

俄语打招呼和问候的12种表达方式,柯桥俄语培训

- Как дела ? 近况如何&#xff1f; -Нормально, а ты как? 还行吧&#xff0c;你呢&#xff1f; Vol.2 -Как себя чувствуете? 你感觉如何&#xff1f; -Все замечательно! 一切都非常棒。 Vol.3 -Ка…

详解 Macvlan 创建不同容器独立跑仿真(持续更新中)

一、概念介绍 1.1 什么是macvlan macvlan是一种网卡虚拟化技术&#xff0c;能够将一张网卡&#xff08;Network Interface Card, NIC&#xff09;虚拟出多张网卡&#xff0c;这意味着每个虚拟网卡都能拥有独立的MAC地址和IP地址&#xff0c;从而在系统层面表现为完全独立的网络…

颠覆传统编程:用ChatGPT十倍提升生产力

我们即将见证一个新的时代&#xff01;这是最好的时代&#xff0c;也是最坏的时代&#xff01; 需求背景 背景&#xff1a; 平时会编写博客&#xff0c;并且会把这个博客上传到github上&#xff0c;然后自己买一个域名挂到github上。 我平时编写的博客会有一些图片来辅助说明的…

拦截器Interceptor

概念&#xff1a;是一种动态拦截方法调用的机制&#xff0c;类似于过滤器。Spring框架中提供的&#xff0c;用来动态拦截方法的执行。 作用&#xff1a;拦截请求&#xff0c;在指定的方法调用前后&#xff0c;根据业务需要执行预先设定的代码。

nvdiadocker相关配置S3Gaussian

https://download.csdn.net/download/sinat_21699465/89458214 dockerfile文件参考&#xff1a; https://download.csdn.net/download/sinat_21699465/89458214 prework&#xff1a; 显卡驱动决定了cuda版本支持的上限。例如nvdia535驱动最高支持cuda12.2所以显卡驱动版本选…

如何快速绘制logistic回归预测模型的ROC曲线?

临床预测模型&#xff0c;也是临床统计分析的一个大类&#xff0c;除了前期构建模型&#xff0c;还要对模型的预测能力、区分度、校准度、临床获益等方面展开评价&#xff0c;确保模型是有效的&#xff01; 其中评价模型的好坏主要方面还是要看区分度和校准度&#xff0c;而区分…

2024全网最全面及最新且最为详细的网络安全技巧四 之 lsql注入以及mysql绕过技巧 (1)———— 作者:LJS

目录 4. SQL注入基础之联合查询 什么是SQL注入漏洞 SQL注入原理 SQL注入带来的危害 注入按照注入技术&#xff08;执行效果&#xff09;分类 简单联合查询注入语句 4.1 [网鼎杯 2018]Comment二次注入 正好总结一下绕过addslashes的方式 4.2 ciscn2019web5CyberPunk 复现平台 解…

im即时通讯软件系统,私有化部署国产化信创适配安全可控

私有化部署IM即时通讯软件系统是许多企业为了确保数据安全、控制隐私保护、提升灵活性而考虑的重要选择之一。信创适配安全可控是企业在私有化部署IM即时通讯软件系统时需要关注的关键点。本文将探讨私有化部署IM即时通讯软件系统的意义、信创适配的重要性&#xff0c;以及如何…

张宇1000题太难?这么刷只要30天就能吃透!

1000题真的难&#xff0c;一刷正确率不高是正常的&#xff01; 我不建议再继续去刷880题&#xff0c;因为继续开始做新题并没有太大的意义&#xff0c;老问题不解决&#xff0c;做新题的效果其实并不好。 如果一刷1000题正确率不高&#xff0c;我们应该反思为什么会这样&…