“喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗”。声音是从电话那头传来的

“是的,你好”。小帅暗喜,大厂终于找上我了。

“下面我们来进行一下电话面试吧,请先自我介绍一下吧”

“balabalabla...”小帅把之前的经历大概描述了一下

“嗯,经历很丰富呀,接下来咱们来聊聊技术吧,请问cglib和jdk动态代理的区别是什么呢?”

“额(⊙o⊙)…”,张小帅蒙了,场面一度尴尬。

......

面试的事情就发生在刚才,由于第一题就栽了,后面面试官的问题,小帅基本啥信心都没了,此时小帅心情久久不能平静,“这就是大厂的面试么?”,小帅喃喃自语,他万万没想到的是第一题就栽了。

屏幕前的小伙伴是否心中有数呢?接下来,就跟着老猫好好温习一下吧。


聊起动态代理,咱们还是从代理模式先着手来看看吧。

代理模式

关于代理模式,查阅比较专业的资料是这么定义的:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

上述概念看起来比较模糊,举些日常的例子,比方说火车票是个目标对象,咱们要去买,那么我们不一定非得去火车站才能买到,其实在很多代理点也能买到。再比如猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。

静态代理模式

代码演示

下面咱们通过买火车票的案例演示一下代理模式,具体代码如下:

抽象接口:

/**
* @Author: 老猫
* @Description: 票
*/
public interface Ticket {
void getTicket();
}

火车站实现抽象接口具备买票功能

/**
* @Author: 老猫
* @Description: 火车站
*/
public class RailwayStation implements Ticket { @Override
public void getTicket() {
System.out.println("买了张火车票");
}
}

火车站代理类实现抽象接口具备买票功能

/**
* @Author: 老猫
* @Description: 代理类
* @Date: 2021/12/22 5:35 下午
*/
public class RailwayAgencyProxy implements Ticket{
private RailwayStation railwayStation; public RailwayAgencyProxy(RailwayStation railwayStation) {
this.railwayStation = railwayStation;
} @Override
public void getTicket() {
railwayStation.getTicket();
}
}

上述其实就是静态代理模式。

优点:

  1. 可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
  2. 职责非常清晰,一目了然。

缺点:

  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
  3. 代码层面来看,如果接口发生改变,代理类也会发生变更。
动态代理

有了上面的基础,咱们正式聊聊动态代理。

上面的例子其实我们不难发现,每个代理类只能够实现一个接口服务。那么如果当我们的软件工程中出现多个适用代理模式的业务类型时那么咱们就会创建多个代理类,那么我们如何去解决这个问题呢?其实我们的动态代理就应运而生了。

很显然动态代理类的字节码在程序运行时由Java反射机制动态生成,无需我们手工编写它的源代码。

那咱们基于上述的案例来先看看JDK动态代理类

JDK动态代理

直接看一下JDK动态代理的使用,如下代码块

public class JDKDynamicProxy implements InvocationHandler {

    //被代理的对象
private Object object; public JDKDynamicProxy(Object object) {
this.object = object;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(object,args);
return result;
} //生成代理类
public Object createProxyObj(){
return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
}
}

静态代理,动态代理调用如下:

public class TestProxy {
public static void main(String[] args) {
//静态代理测试
RailwayStation railwayStation = new RailwayStation();
railwayStation.getTicket(); RailwayAgencyProxy railwayAgencyProxy = new RailwayAgencyProxy(railwayStation);
railwayAgencyProxy.getTicket(); //动态代理测试
Ticket ticket = new RailwayStation();
JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(ticket);
Ticket proxyBuyTicket = (Ticket) jdkDynamicProxy.createProxyObj();
proxyBuyTicket.getTicket();
}
}

观察上面动态代理以及静态代理测试,动态代理的优势就显而易见了。如果我们再演示另外猪八戒和高翠兰代理场景的时候,是不是就不用再去创建GaocuiLanProxy了,我们只需要通过JDKDynamicProxy的方式去创建代理类即可。

注意Proxy.newProxyInstance()方法接受三个参数:

  1. ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的。
  2. Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
  3. InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法

通过上面的例子以及上述参数,我们不难发现JDK动态代理有这样一个特性:

JDK动态代理是面向接口的代理模式,如果要用JDK代理的话,首先得有个接口,例如上面例子中的Ticket接口

cglib动态代理

咱们再来看一下cglib动态代理。先了解一下cglib是什么。关于cglib,其实其官方解释是比较少的,但是其本身是十分强大的,这也是很多人所诟病的。CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP为他们提供方法的interception(拦截)。CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

接下来我们看一下用法,由于cglib并不是jdk自带的,所以如果是maven项目的话,咱们首先需要的是引入cglib相关的pom依赖,如下:

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

由于cglib代理的对象是类,这个是和JDK动态代理不一样的地方,这个地方标红加重点。

这样的话,咱们同样的代理类的话则应该如下定义,

public class Ticket {
public void getTicket(){
System.out.println("买了张火车票");
} final public void refundTicket(){
System.out.println("退了张火车票");
}
}

显然,上面的类定义了两个方法,一个是买火车票另外的话退火车票。

public class CglibDynamicProxy implements MethodInterceptor {    

    /**
* @param o cglib生成的代理对象
* @param method 被代理对象的方法
* @param objects 传入方法的参数
* @param methodProxy 代理的方法
* @return
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("execute pre");
Object obj = methodProxy.invokeSuper(o,objects);
System.out.println("execute after");
return obj;
}
}

调用测试入口方法调用

public class TestCglibProxy {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/kdaddy/project/log");
// 通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
// 设置enhancer对象的父类
enhancer.setSuperclass(Ticket.class);
// 设置enhancer的回调对象
enhancer.setCallback(new CglibDynamicProxy());
// 创建代理对象
Ticket ticket = (Ticket) enhancer.create();
// 通过代理对象调用目标方法
ticket.getTicket();
// 通过代理尝试调用final对象调用目标方法
ticket.refundTicket();
}
}

运行之后我们得到的结果如下:

execute pre
买了张火车票
execute after
取消了张火车票

根据日志的打印情况,我们很容易发现相关打印“取消了张火车票”并没有被代理,所以我们由此可以得到一个结论cglib动态代理无法代理被final修饰的方法

上述源码中,我们提及将代理类class写到了相关的磁盘中,打开对应的目录,我们会发现下面三个文件

Ticket$$EnhancerByCGLIB$$4e79a04a类为cglib生成的代理类,该类即成了Ticket。

咱们来慢慢看看相关的源码:

public class Ticket$$EnhancerByCGLIB$$4e79a04a extends Ticket implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
//拦截器
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
//被代理方法
private static final Method CGLIB$getTicket$0$Method;
//代理方法
private static final MethodProxy CGLIB$getTicket$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$equals$1$Method;
private static final MethodProxy CGLIB$equals$1$Proxy;
private static final Method CGLIB$toString$2$Method;
private static final MethodProxy CGLIB$toString$2$Proxy;
private static final Method CGLIB$hashCode$3$Method;
private static final MethodProxy CGLIB$hashCode$3$Proxy;
private static final Method CGLIB$clone$4$Method;
private static final MethodProxy CGLIB$clone$4$Proxy; static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("kdaddy.com.cglibDynamic.Ticket$$EnhancerByCGLIB$$4e79a04a");
Class var1;
CGLIB$getTicket$0$Method = ReflectUtils.findMethods(new String[]{"getTicket", "()V"}, (var1 = Class.forName("kdaddy.com.cglibDynamic.Ticket")).getDeclaredMethods())[0];
CGLIB$getTicket$0$Proxy = MethodProxy.create(var1, var0, "()V", "getTicket", "CGLIB$getTicket$0");
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$1$Method = var10000[0];
CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = var10000[1];
CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = var10000[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = var10000[3];
CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
}
}

我们通过代理类的源码可以看到,代理类会获得所有在父类继承来的方法,并且会有MethodProxy与之对应,当然被final修饰的方法除外,上述源码中我们也确实没有看到之前的refundTicket方法。接下来往下看。

咱们看其中一个方法的调用。

//代理方法(methodProxy.invokeSuper会调用)
final void CGLIB$getTicket$0() {
super.getTicket();
} //被代理方法(methodProxy.invoke会调用,这就是为什么在拦截器中调用methodProxy.invoke会死循环,一直在调用拦截器)
public final void getTicket() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
} if (var10000 != null) {
//调用拦截器
var10000.intercept(this, CGLIB$getTicket$0$Method, CGLIB$emptyArgs, CGLIB$getTicket$0$Proxy);
} else {
super.getTicket();
}
}

通过上述,我们看下getTicket整个的调用链路:

调用getTicket()方法->调用拦截器->methodProxy.invokeSuper->CGLIB$getTicket$0->被代理的getTicket方法。

接下来,咱们再来看一下比较核心的MethodProxy,咱们直接看下核心:methodProxy.invokeSuper,具体源码如下:

 public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
this.init();
MethodProxy.FastClassInfo fci = this.fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException var4) {
throw var4.getTargetException();
}
} //进一步看一下FastClassInfo
private static class FastClassInfo {
FastClass f1;//被代理类FastClass
FastClass f2;//代理类FastClass
int i1; //被代理类的方法签名(index)
int i2;//代理类的方法签名 private FastClassInfo() {
}
}

上面代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。还记得之前生成三个class文件吗?Ticket$$EnhancerByCGLIB$$4e79a04a$$FastClassByCGLIB$$f000183.class就是代理类的FastClass,Ticket$$FastClassByCGLIB$$a79cabb2.class就是被代理类的FastClass。

关于FastClass

Cglib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机制,它的原理简单来说就是:为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。

这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。下面我们反编译一个FastClass看看:

public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch(var10000.hashCode()) {
case -80792013:
if (var10000.equals("getTicket()V")) {
return 0;
}
break;
case 189620111:
if (var10000.equals("cancelTicket()V")) {
return 1;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return 2;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return 3;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return 4;
}
} return -1;
}
...此处省略部分代码...
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
Ticket var10000 = (Ticket)var2;
int var10001 = var1; try {
switch(var10001) {
case 0:
var10000.getTicket();
return null;
case 1:
var10000.cancelTicket();
return null;
case 2:
return new Boolean(var10000.equals(var3[0]));
case 3:
return var10000.toString();
case 4:
return new Integer(var10000.hashCode());
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
} throw new IllegalArgumentException("Cannot find matching method/constructor");
}

FastClass并不是跟代理类一块生成的,而是在第一次执行MethodProxy invoke/invokeSuper时生成的并放在了缓存中。

//MethodProxy invoke/invokeSuper都调用了init()
private void init() {
if(this.fastClassInfo == null) {
Object var1 = this.initLock;
synchronized(this.initLock) {
if(this.fastClassInfo == null) {
MethodProxy.CreateInfo ci = this.createInfo;
MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
fci.f1 = helper(ci, ci.c1);//如果缓存中就取出,没有就生成新的FastClass,此处感兴趣的小伙伴可以仔细看一下底层源码,老猫此处提及一下。
fci.f2 = helper(ci, ci.c2);
fci.i1 = fci.f1.getIndex(this.sig1);//获取方法的index
fci.i2 = fci.f2.getIndex(this.sig2);
this.fastClassInfo = fci;
this.createInfo = null;
}
}
} }

总结

如果屏幕前的你也像张小帅那样,该如何应对呢?其实大部分的答案都在上面了。总结一下

(1)JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。

(2)JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。

(3)JDK调用代理方法,是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。

我是老猫,更多内容,欢迎大家搜索关注老猫的公众号“程序员老猫”。

面试造火箭系列,栽在了cglib和jdk动态代理的更多相关文章

  1. 学习CGLIB与JDK动态代理的区别

    动态代理 代理模式是Java中常见的一种模式.代理又分为静态代理和动态代理.静态代理就是显式指定的代理,静态代理的优点是由程序员自行指定代理类并进行编译和运行,缺点是一个代理类只能对一个接口的实现类进 ...

  2. Cglib 与 JDK动态代理的运行性能比较

    都说 Cglib 创建的动态代理的运行性能比 JDK 动态代理能高出大概 10 倍,今日抱着怀疑精神验证了一下,发现情况有所不同,遂贴出实验结果,以供参考和讨论. 代码很简单,首先,定义一个 Test ...

  3. Cglib 与 JDK动态代理

    作者:xiaolyuh 时间:2019/09/20 09:58 AOP 代理的两种实现: jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到: cglib是子类,private的方法照 ...

  4. 输出cglib以及jdk动态代理产生的class文件

      --该设置用于输出jdk动态代理产生的类 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles&q ...

  5. CGlib和JDK动态代理

    一.CGlib动态代理     JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了.CGLib采用了非常底层的1:字节码技术,其原理是通过字节 ...

  6. spring cglib 与 jdk 动态代理

    1. 概述 JDK动态代理是利用java反射机制 生成一个实现接口的匿名类, 在调用具体方法前调用InvocationHandler来处理 Cglib动态代理是 利用asm开源包 把被代理类的clas ...

  7. AOP面试造火箭始末

    本文已整理致我的github地址,欢迎大家 star 支持一下 这是一个困扰我司由来已久的难题,Dubbo 了解过吧,对外提供的服务可能有多个方法,一般我们为了不给调用方埋坑,会在每个方法里把所有异常 ...

  8. 面试必问系列之JDK动态代理

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  9. (转)面试必备技能:JDK动态代理给Spring事务埋下的坑!

    一.场景分析 最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下: 1.场景A ...

随机推荐

  1. Spring DAO

    Spring DAO 连接池 使用JDBC访问数据库是,频繁的打开连接和关闭连接,造成性能影响,所以有了连接池.数据库连接池负责分配.管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接, ...

  2. Django url中可以使用类视图.as_view()进行映射的原因

    说明:在练习天天生鲜项目时,对利用类视图去与正则匹配到的url做映射有点疑惑,经过查看他人博客以及自我分析算是整明白了,所以记录一下 参考:https://www.zmrenwu.com/post/5 ...

  3. nodejs-os模块

    JavaScript 标准参考教程(alpha) 草稿二:Node.js os模块 GitHub TOP os模块 来自<JavaScript 标准参考教程(alpha)>,by 阮一峰 ...

  4. HTTP 之 options预请求

    一.HTTP一共有八种常见请求方法 get:参数在url上,浏览器长度有限制,不安全 post:参数不可见,长度不受限制 put:上传最新内容到指定位置 delete:删除请求的url所表示的资源 h ...

  5. android 下动态获取控件的id

    有时候我们需要动态的取得一个一个控件的id,然后进行操作,经过在网上查找,找到了一下方法getResources().getIdentifier("textView01", &qu ...

  6. Mave 下载与安装

    一,Maven 介绍 我们在开发中经常需要依赖第三方的包,包与包之间存在依赖关系,版本间还有兼容性问题,有时还需要将旧的包升级或降级,当项目复杂到一定程度时包管理变得非常重要.Maven是当前最受欢迎 ...

  7. 【Java基础】方法调用机制——MethodHandle

    MethodHandle是Java7引入的一种机制,主要是为了JVM支持动态语言. 一个MethodHandle调用示例 共有方法调用 首先,演示一下最基本的MethodHandle使用. 第一步:创 ...

  8. 【手帐】Bullet Journal教程

    最近觉得自己的日程记录本有待提高,于是从今年开始开始入坑了手帐. *内容源自Bullet Journal官网.https://bulletjournal.com/pages/learn 快速笔记 Bu ...

  9. 【C/C++】习题3-4 周期串/算法竞赛入门经典/数组和字符串

    [题目] 如果某个字符串可以由长度为k的字符串重复多次得到,则称该串以k为周期. 输入一个长度不超过80的字符串,输出最小周期. [思路] 暴力求解.依次考察周期1~长度n. 筛选:周期一定是长度n的 ...

  10. AI 2021 年度报告

    建议大伙有空还是自己亲自读一下,虽然有点长,188页ppt. https://docs.google.com/presentation/d/1bwJDRC777rAf00Drthi9yT2c9b0Ma ...