首页 > 其他分享 >大厂面试必备系列:一文彻底搞懂 Cglib 代理

大厂面试必备系列:一文彻底搞懂 Cglib 代理

时间:2024-07-08 21:29:34浏览次数:18  
标签:var1 String cglib CglibDemoDTO Object 大厂 Cglib 搞懂 public

前言

大家在面试中经常被问到 Cglib 和 JDK动态代理有啥区别? 然后每次回答都是 Cglib 通过创建目标类的子类来实现代理。

这个回答当然是对的,但是太敷衍了,没得加分,今天我带大家深入了解下。

最佳实践

直接上案例

案例地址: https://github.com/zhuangjiaju/easytools/blob/main/easytools-test/src/test/java/com/github/zhuangjiaju/easytools/test/demo/cglib/CglibTest.java

最简单的cglib案例

Cglib 核心是生成了一个代理类,所以性能会比反射有优势。 我们先看一个列子,用反射和cglib分别设置一个值到一个对象中。

/**
 * 最简单的cglib案例
 * 设置一个值到一个对象中
 */
@Test
public void beanMap() throws Exception {
    // 我们需求是将 cglibDemo.name 设置成 "JiaJu Zhuang"

    // 用反射
    CglibDemoDTO cglibDemo = new CglibDemoDTO();
    Field nameField = CglibDemoDTO.class.getDeclaredField("name");
    // 设置私有字段可访问
    nameField.setAccessible(true);
    nameField.set(cglibDemo, "JiaJu Zhuang");
    log.info("设置结果:{}", cglibDemo.getName());
    Assertions.assertEquals("JiaJu Zhuang", cglibDemo.getName());

    // 反射的性能相对较差,所以spring用了cglib代理,性能会更好的方案

    // 用cglib
    // cglib给我们提供了一个BeanMap的工具类,可以将一个对象转换成一个map,并且可以设置值
    cglibDemo = new CglibDemoDTO();
    BeanMap beanMap = BeanMap.create(cglibDemo);
    beanMap.put("name", "JiaJu Zhuang");
    log.info("设置结果:{}", cglibDemo.getName());
    Assertions.assertEquals("JiaJu Zhuang", cglibDemo.getName());
}

是不是好好奇?BeanMap 的 put 方法这么神奇,他就是做了啥?

查看生成后的源码

我们通过 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path) 方法来让 Cglib 输出生成的源码。

    /**
 * 查看生成后的源码
 */
@Test
public void showClass() {
    // 我们只要设置输出一个路径,就可以看到生成的源码
    // 打印到 easytools/easytools-test/target/test-classes/cglib
    String path = this.getClass().getResource("/").getPath() + "cglib";
    log.info("输出目录是:{}", path);
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path);
    BeanMap.create(new CglibDemoDTO());
}

先看 BeanMap 的 put 方法,非常简单,直接调用了一个抽象方法的put 方法。

abstract public class BeanMap implements Map {
    @Override
    public Object put(Object key, Object value) {
        return put(bean, key, value);
    }

    abstract public Object put(Object bean, Object key, Object value);
}

生成的源码如下,他继承了BeanMap 所以直接看put方法:


package com.github.zhuangjiaju.easytools.test.demo.cglib;

import java.util.Set;

import org.springframework.cglib.beans.BeanMap;
import org.springframework.cglib.beans.FixedKeySet;

public class CglibDemoDTO$$BeanMapByCGLIB$$fe3c380f extends BeanMap {
    private static FixedKeySet keys;
    private static final Class CGLIB$load_class$java$2Elang$2EString;

    public CglibDemoDTO$$BeanMapByCGLIB$$fe3c380f() {
    }

    public BeanMap newInstance(Object var1) {
        return new CglibDemoDTO$$BeanMapByCGLIB$$fe3c380f(var1);
    }

    public CglibDemoDTO$$BeanMapByCGLIB$$fe3c380f(Object var1) {
        super(var1);
    }

    public Object get(Object var1, Object var2) {
        CglibDemoDTO var10000 = (CglibDemoDTO)var1;
        String var10001 = (String)var2;
        switch (((String)var2).hashCode()) {
            case 3373707:
                if (var10001.equals("name")) {
                    return var10000.getName();
                }
        }

        return null;
    }

    public Object put(Object var1, Object var2, Object var3) {
        CglibDemoDTO var10000 = (CglibDemoDTO)var1;
        String var10001 = (String)var2;
        // 这里用 switch 核心是加速,如果属性特别多,不用一个个equals,equals的性能不太好
        switch (((String)var2).hashCode()) {
            case 3373707:
                // 判断是否是 name 属性
                if (var10001.equals("name")) {
                    String var10002 = var10000.getName();
                    // 调用原始类的 setName 方法
                    var10000.setName((String)var3);
                    return var10002;
                }
        }

        return null;
    }

    static {
        CGLIB$STATICHOOK1();
        keys = new FixedKeySet(new String[] {"name"});
    }

    static void CGLIB$STATICHOOK1() {
        CGLIB$load_class$java$2Elang$2EString = Class.forName("java.lang.String");
    }

    public Set keySet() {
        return keys;
    }

    public Class getPropertyType(String var1) {
        switch (var1.hashCode()) {
            case 3373707:
                if (var1.equals("name")) {
                    return CGLIB$load_class$java$2Elang$2EString;
                }
        }

        return null;
    }
}

是不是很简单?他就是直接调用了原始类的 setName 方法,没有任何技术含量。

我们自己来实现下代理

Spring 使用的 Cglib 动态代理,是不是和下面的差不多。 我们对用 setName 方法做一个增强,都改成 name+" 你真棒!!!"

/**
 * 我们自己来实现下代理
 * 调用setName 都改成 name+" 你真棒!!!"
 */
@Test
public void proxy() {
    // 我们只要设置输出一个路径,就可以看到生成的源码
    // 打印到 easytools/easytools-test/target/test-classes/cglib
    String path = this.getClass().getResource("/").getPath() + "cglib";
    log.info("输出目录是:{}", path);
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path);

    // 新建增强器
    Enhancer enhancer = new Enhancer();
    // 父类是CglibDemoDTO
    enhancer.setSuperclass(CglibDemoDTO.class);
    enhancer.setCallback((MethodInterceptor)(obj, method, args, proxy) -> {
        // 判断setName方法且是String
        if (method.getName().equals("setName") && args[0] != null && args[0] instanceof String) {
            String name = (String)args[0];
            // 修改参数
            args[0] = name + " 你真棒!!!";
        }
        // 继续调用父类
        return proxy.invokeSuper(obj, args);
    });
    CglibDemoDTO cglibDemo = (CglibDemoDTO)enhancer.create();
    cglibDemo.setName("JiaJu Zhuang");
    log.info("输出结果:{},{}", cglibDemo.getName(), cglibDemo.getClass());
    Assertions.assertEquals("JiaJu Zhuang 你真棒!!!", cglibDemo.getName());
}

输出结果:

20:40:12.644 [main] INFO com.github.zhuangjiaju.easytools.test.demo.cglib.CglibTest -- 输出结果:JiaJu Zhuang 你真棒!!!,class com.github.zhuangjiaju.easytools.test.demo.cglib.CglibDemoDTO$$EnhancerByCGLIB$$6be31ec5

可以看到已经能输出 JiaJu Zhuang 你真棒!!! 了,而不是原来的 JiaJu Zhuang

我们直接看下生成的代理类 com.github.zhuangjiaju.easytools.test.demo.cglib.CglibDemoDTO$$EnhancerByCGLIB$$6be31ec5 他继承了
CglibDemoDTO类, 然后重写了 setName 方法,并用final修饰。

public class CglibDemoDTO$$EnhancerByCGLIB$$6be31ec5 extends CglibDemoDTO implements Factory {

    // 省略了很多方法

    public final void setName(String var1) {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        // 判断有没有MethodInterceptor 有的话直接调用
        if (var10000 != null) {
            // 直接调用我们的 匿名内部类了
            var10000.intercept(this, CGLIB$setName$1$Method, new Object[] {var1}, CGLIB$setName$1$Proxy);
        } else {
            super.setName(var1);
        }
    }

    // 省略了很多方法
}

是不是也特别简单,就是一个简单回调。

补充问题:Cglib 能不能代理 final 类?能不能代理 final 方法?

这个就不用我多说了吧,我们看到了生成的代理类,他继承了我们的原始类,原始类是 final 的话,会导致无法代理。

代理方法也一样,如果是 final 方法,子类无法重写父类的 final 方法,所以也无法代理。

这里有个有趣的现象,被代理的方法也是 final 的,所以生成的代理也无法被再次代理了。

总结

通过这篇文章大家是不是对吹的神乎其技的 Cglib 有了新的理解,实际上他比大家想象的容易非常多,只是大家没有时间去实践一下,大家去动手试试吧,试过才是自己的。

写在最后

给大家推荐一个非常完整的Java项目搭建的最佳实践,也是本文的源码出处,由大厂程序员&EasyExcel作者维护,地址:https://github.com/zhuangjiaju/easytools

标签:var1,String,cglib,CglibDemoDTO,Object,大厂,Cglib,搞懂,public
From: https://blog.csdn.net/fish7790714/article/details/140279233

相关文章

  • 一文彻底带你搞懂什么是适配器模式!!
    一文彻底带你搞懂什么是适配器模式!!什么是适配器模式?适配器的两种实现方式适用情况代码示例背景类适配器对象适配器IO流中的实际应用应用扩展总结什么是适配器模式?适配器模式(AdapterPattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,......
  • 保姆级阿里云接入http接口加密数据教程,一看就会!大厂经验分享,接口数据落表
    前言:有关接口数据的接入,源端会给予我们一份接口说明文件,接着我们需依据对方提供的接口文档进行代码编写,以实现数据落表。接入步骤大致可分为以下几步:1.依据对方提供的接口文档,明确接口地址、请求方式、传参信息以及参数格式等。2.借助编写代码,获取接口数据内容。3.按照......
  • 大厂面试高频题——二分查找
    35.搜索插入位置给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为O(logn)的算法。思考二分模板题classSolution:defsearchInsert(self,nums:List[int],target:in......
  • 一文带你彻底搞懂什么是代理模式!!
    一文带你彻底搞懂什么是代理模式!!什么是代理模式?静态代理静态代理的问题动态代理JDK动态代理JDK动态代理的问题CGLIB动态代理JDK动态代理和CGLIB动态代理对比静态代理和动态代理的对比总结什么是代理模式?代理模式是一种常见的设计模式,它为其他对象提供了一种......
  • 从jvm层面搞懂java的i++
    >本博客将从java字节码的层面解剖为什么i=0;i=i++;仍然会等于0字节码解析:iconst_x:代表将常量x放到操作数栈中istore_x:其中x必须是局部变量表中的一个合法下标,然后我们会从操作数栈中弹出对应的栈尾的元素(需要是int)以之来进行设置iload_x:则是加载对应的局部变量表的x......
  • 通信协议 | 一文搞懂SPI通信协议
    SPI的英文全称为SerialPeripheralInterface,顾名思义为串行外设接口。SPI是一种同步串行通信接口规范,主要应用于嵌入式系统中的短距离通信。该接口由摩托罗拉在20世纪80年代中期开发,后发展成了行业规范。SPI是一种高速的、全双工的、同步的通信总线,并且至多仅需使用......
  • 30个Linux运维面试题,面试一线大厂必备!
    在本文中,我们将讨论30个Linux系统管理员面试问题以及经验丰富的专业人士的答案。(1)为什么需要LVM?LVM(Logicalvolumemanagement)推荐使用LVM管理linux服务器上的磁盘或存储,可以在线调整LVM分区的大小,而不用停止服务器。(2)如何检查内存和CPU统计信息?使......
  • 一文搞懂到底什么是 AQS
    前言日常开发中,我们经常使用锁或者其他同步器来控制并发,那么它们的基础框架是什么呢?如何实现的同步功能呢?本文将详细讲解构建锁和同步器的基础框架--AQS,并根据源码分析其原理。一、什么是AQS?1.AQS简介AQS(AbstractQueuedSynchronizer),抽象队列同步器,它是用来构建锁或其他......
  • 一文搞懂Nginx配置:轻松驾驭多域名管理的实战
    1.nginx.conf如何配置多个域名在Nginx中配置多个域名,可以通过创建单独的配置文件来实现,这样可以让配置更加清晰且易于管理。以下是配置多个域名的一个基本步骤指南:1.1.步骤1:创建域名配置文件首先,在Nginx的配置目录下创建一个用于存放各域名配置文件的目录(如果还没有......
  • C++ 彻底搞懂指针(3)
    1.数组指针、二维数组指针、字符串指针1.1定义一个数组指针前面说过,指针变量存放的是地址,它可以存放普通变量的地址,可以存放另一个指针变量的地址,当然也可以存放数组、结构体、函数的地址。如果一个指针指向了数组,就称它为数组指针,比如下面的代码就定义了一个指针p指向......