JVAV面试-静态代理动态代理
前面已经更新了非常多的java基础知识和面试技巧,这一篇文章可以说是java面试中java基础最后的绝唱,也是基础知识的压轴戏,和前面反射内容息息相关。代理是一种设计模式,在每种语言上都有体现,但是能窥见一斑的人不多,一般人认为自己会用就行,面试会背就行,但是作为关注博主的你,我希望你可以是巅峰赛的佼佼者,可以从容应对所有的大厂的面试,曾经的博主就是靠着这个技术征服了盒马生鲜的面试官,上机拿捏它。
一、代理模式
1.1 设计模式简介
设计模式可以根据其目的和作用分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns) 和 行为型模式(Behavioral Patterns)。每一类设计模式都解决特定类型的问题,并帮助开发者编写更加灵活、可维护和可复用的代码。代理模式是一种结构性设计模式。简单举例子说明一下这三种模式:
- 创建型模式主要
关注对象的创建过程
,旨在将对象的创建与使用分离
,从而提高灵活性和可复用性。
就好比建房子时候需要一个系统的创建工厂,比如烧砖的工厂,如果没有这个工厂,每当你需要一口砖的时候你就要去自己烧一块,效率很低,那么此时需要一个砖厂,你和他约定一块砖1元,那么你就直接负责给钱拿砖就行,需要多少拿多少,效率高,速度快。 - 结构型模式关注如何
组合类和对象以形成更大的结构
,目的是简化系统
的设计并提高其灵活性和效率。
就好比利用原材料开始建房子,但是如果想一点是一点的话就很慢而且不美观,我建造的圆形有的大有的小,不协调等等问题,那么此时我可以使用到结构型模式,比如我把装修包给建筑公司,让他来代理,我只负责出设计图,至于在设计图上他们要做什么改动我不管,这就是代理模式,再比如我想把一楼和二楼连起来,可以用到桥接模式,将俩个不属于同一层的装修使用某种手段关联等等。 - 行为型模式关注对象之间的
责任分配
以及对象之间的通信方式
,旨在提高系统的灵活性和可维护性。
就好比房子建好之后,需要做一个全屋的小米生态,这样才能更好的控制每个房间元素的联动性。如果没有全屋生态,你需要亲自去控制每个房间的电灯的线路,非常繁琐难用,此时你只需要去安装几个开关,在手机上直接啪啪点几下可以控制全部的电灯,还可以设置什么时候开什么时候关,这就是策略模式,在不同的场景下设计出不同的方案去应对。
1.2 代理模式简介
代理模式作为一种结构型设计模式,主要是用来处理代理方和被代理方之间的一个拓展关系。在生活中会经常听到中介
这个词,比如你卖房子的时候自己找不到客源,只能找到中介卖房子。那么此时卖房子
这个举动可以理解成是你的需求,也是代理内容的核心,因为你的目的是要把房子卖出去。对于中介来说卖房子是被代理方的核心需求,他的最终结果是要把这个房子卖出去,但是它可以通过更多的手段去卖这个房子,让这个卖房子的内容更加充实,比如他可以利用公司资源给每个客户打电话询问需不需要房子,可以利用小红书推广,可以利用公司平台对外推广等等手段来对卖房子这个行为做一个加强的动作,这个就是代理的核心。在代码中,代理模式就是在不改变原有对象的前提下,通过引入一个代理对象来控制原有对象的访问,实现额外的功能。例如控制权限、延迟加载、缓存等。
代理模式的特点:
- 间接访问:客服端通过代理访问实际对象,代理对象负责对实际对象的控制。
- 功能增强:代理对象可以在访问实际对象之前或之后添加额外的功能。
- 解耦性:客户端不直接与实际对象交互,通过代理对象可以透明的扩展实际对象的功能。
1.3 代理模式UML
代理模式 (Proxy Pattern) UML 类图
二、JAVA的静态代理&动态代理
2.1 静态代理&动态代理
特性 | 静态代理(Static Proxy) | 动态代理(Dynamic Proxy) |
---|---|---|
定义 | 在编译时确定代理类,代理类直接实现与目标对象相同的接口或继承目标类。 | 在运行时生成代理类,通常基于接口或通过子类化生成代理对象。 |
灵活性 | 固定的代理逻辑,每个需要代理的目标类都需要手动编写对应的代理类。 | 更加灵活,可以在运行时动态生成代理类,适用于多种不同的目标对象。 |
实现方式 | 代理类实现与目标对象相同的接口,并在构造函数中持有目标对象的引用。 | 使用java.lang.reflect.Proxy (JDK动态代理)或CGLIB等工具在运行时生成代理类。 |
适用场景 | 适合代理数量较少且相对固定的场景。 | 适合代理数量较多或动态变化的场景,如AOP(面向切面编程)。 |
性能 | 编译期确定,性能较好,但代码膨胀问题严重。 | 运行时生成代理类,初次生成可能较慢,但后续调用性能较好。 |
复杂度 | 实现较为简单,但扩展性差,需为每个目标类编写代理类。 | 实现较为复杂,但扩展性好,只需编写一次代理逻辑即可复用。 |
类型 | 基于接口或类的代理,必须显式地为每个被代理类编写代理类。 | 基于接口(JDK动态代理)或基于类(CGLIB),无需为每个被代理类编写代理类。 |
接口依赖 | 必须实现与目标对象相同的接口。 | JDK动态代理要求目标对象实现接口,CGLIB则不需要接口。 |
功能增强 | 可以在方法调用前后添加额外的功能,如日志记录、权限验证等。 | 同样可以在方法调用前后添加额外的功能,但可以更加灵活地处理多个目标对象。 |
典型应用场景 | 日志记录、权限控制等简单的代理需求。 | AOP(面向切面编程)、事务管理、缓存管理等复杂的代理需求。 |
2.2 静态代理详解
在编译阶段,由代理类直接持有被代理的对象进行方法增强。
举个例子当你需要卖房子之前就已经和中介串通好了,这个房子的卖之前
需要确认对方能够全款才同意
,卖之后
给中介一块钱的中介费
。中介欣然接受并要求你签合同白字黑色,但是他的心里骂你是奸商。
代码展示如下:
public class Main {
public static void main(String[] args) {
// 开始由中介来卖房子了
Xiaoshou xiaoshou = new Xiaoshou(new XiaoMing());
xiaoshou.sellHouse();
}
}
// 定义一个Person类 有一个专门卖房子的方法
interface Person{
void sellHouse();
}
// 假设你是小明,实现Person 开始买房子
// 此时这个类就是被代理的类
class XiaoMing implements Person{
@Override
public void sellHouse() {
System.out.println("我是小明,我要开始卖房子了!");
}
}
class Xiaoshou implements Person{
// 此时销售持有小明这个类(也就是代理类持有被代理的对象)
// 相当于前面说的销售已经和小明做了沟通同意自己帮忙卖房子
private final XiaoMing xm;
// 构造函数传入小明的对象
public Xiaoshou(XiaoMing xm) {
this.xm = xm;
}
@Override
public void sellHouse() {
// 在卖之前要看是不是全款(方法增强)
System.out.println("我是销售,我要看买房客户是不是全款?");
System.out.println("开始传达客户的需求:");
System.out.println("---------------------------------------");
// 这个是小明原需求
xm.sellHouse();
// 卖完之后给销售一块钱(方法增强)
System.out.println("---------------------------------------");
System.out.println("我是销售,卖了房子得到了一块钱,小明真是个奸商!");
}
}
代码运行结果如下:
我是销售,我要看买房客户是不是全款?
开始传达客户的需求:
---------------------------------------
我是小明,我要开始卖房子了!
---------------------------------------
我是销售,卖了房子得到了一块钱,小明真是个奸商!
可以清晰的看出在横线的两侧是销售对于小明需求的增强操作,中间就是小明原本的操作,从这个代码示例中就可以很好的理解静态代理。在编译时确定代理类,代理类直接实现与目标对象相同的接口或继承目标类。但是其实很容易看出一个问题也是静态代理的缺陷,就是小明如果以后还想做其他的事情需要中介帮忙的话就需要重新不断的添加代理类去实现,代码复杂度增加,数量也是不断膨胀。所以静态代理存在巨大的缺陷,不符合设计原则,只适用于代理数量较少且相对固定的场景。
2.3 动态代理详解
2.3.1 java实现动态代理的步骤
- JDK动态代理主要依赖于以下两个核心组件:
java.lang.reflect.Proxy
:用于创建动态代理实例。java.lang.reflect.InvocationHandler
:定义了代理对象的方法调用逻辑。
- 动态代理的实现步骤
- 定义接口:目标对象需要实现的接口。
- 实现接口:目标对象实现接口中的具体业务逻辑。
- 实现 InvocationHandler 接口:定义代理对象的行为逻辑,包括方法调用前后的操作。
- 创建代理对象:使用
Proxy.newProxyInstance()
方法创建代理对象。
2.3.2 java动态代理详解
动态代理(Dynamic Proxy
)是Java中一种强大的设计模式,它允许在运行时创建代理对象。动态代理的主要优势在于其灵活性和可扩展性,特别是在需要为多个接口或类添加通用功能时非常有用。
举例说明还是使用上述卖房子的例子来比较一下静态代理
和动态代理
的区别。
代码展示如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 开始由中介来卖房子了
// 创建被代理对象
Person person = new XiaoMing();
// 创建代理对象
XiaoshouHandler xiaoshouHandler = new XiaoshouHandler(person);
Person xm =(Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
xiaoshouHandler
);
// 执行卖房的核心方法
xm.sellHouse();
}
}
// 定义一个Person类 有一个专门卖房子的方法
interface Person{
void sellHouse();
}
// 假设你是小明,实现Person 开始买房子
// 此时这个类就是被代理的类
class XiaoMing implements Person{
@Override
public void sellHouse() {
System.out.println("我是小明,我要开始卖房子了!");
}
}
// 定义代理对象的行为逻辑
// 相当于销售事先虽然沟通但是并没有在之前签合同,
// 而是在每次和客户交涉的时候,将交涉内容作为一个合同临时去签署,
// 相当于没有提前准备合同,只要有人来谈生意才准备合同,方式就变得很灵活
class XiaoshouHandler implements InvocationHandler {
// 持有小明对象,但是此时不能用具体的xiaoming去实现,因为对象通用
private final Object target;
public XiaoshouHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在卖之前要看是不是全款(方法增强)
System.out.println("我是销售,我要看买房客户是不是全款?");
System.out.println("开始传达客户的需求:");
System.out.println("---------------------------------------");
// 这个是小明原需求 反射去找到小明的sellHouse方法
Object invoke = method.invoke(target, args);
// 卖完之后给销售一块钱(方法增强)
System.out.println("---------------------------------------");
System.out.println("我是销售,卖了房子得到了一块钱,小明真是个奸商!");
return invoke;
}
}
代码运行结果如下:
我是销售,我要看买房客户是不是全款?
开始传达客户的需求:
---------------------------------------
我是小明,我要开始卖房子了!
---------------------------------------
我是销售,卖了房子得到了一块钱,小明真是个奸商!
2.3.3 java动态代理源码分析
- 首先从被代理的类这个地方看起
这个要说明一下为什么要用接口,不能直接使用类吗?答案是因为jdk的动态代理实现就是基于接口实现的,一定要有接口才可以利用反射获取到实例。
为什么只能代理接口?
- 灵活性:通过代理接口而不是具体的类,可以在不改变原有代码的情况下为多个不同的实现类添加通用的功能。
- 解耦合:代理模式通过接口解耦了代理对象和目标对象之间的依赖关系,使得代理对象可以轻松替换为目标对象的不同实现。
- Java语言特性:Java是一种强类型语言,每个对象都有一个具体的类型。JDK动态代理通过生成实现了指定接口的代理类来实现代理功能。由于Java不允许在运行时动态地修改现有类的行为,因此不能直接代理具体的类。
- 代理类的生成:JDK动态代理在运行时生成代理类,这些代理类必须实现目标对象所实现的接口。由于Java没有提供直接修改类结构的机制,因此无法生成继承自具体类的代理类。
- 性能考虑:如果允许代理具体类,那么代理类需要继承目标类,并且还需要覆盖所有目标类的方法以添加额外的功能。这种方式不仅复杂,而且可能导致性能问题,特别是在目标类有很多方法或层次结构较深的情况下。
一句话总结:实现起来会非常复杂,不同的类可以通过实现同一个接口去实现,非常简单。
-
然后从
InvocationHandler
开始
InvocationHandler
的核心在于定义了代理对象的行为逻辑,就是方法增强的地方,所有的方法增强都在InvocationHandler
的实现类进行,最后传入new出来的实力对象中,对对象做代码增强。 -
最后看到调用的是
Proxy.newProxyInstance
方法,该对方法主要是通过反射代理对象创建实例,然后将切面InvocationHandler切入。 -
总结整个过程(
重点
):
- 首先通过实现
InvocationHandler
接口得到一个切面类。 - 然后利用
Proxy
根据目标类的类加载器、接口和切面类得到一个代理类。 - 代理类的逻辑就是把所有的接口方法的调用转发到切面类的
invoke()
方法上,然后根据反射调用目标类的方法。
- 整个过程的核心还是发射,建议可以回去看看之前关于反射里面invoke()方法的详解。
一定要多谢多去看源码的invoke()实现,反射是java的核心思想。
2.4 CGLIB代理详解
CGLIB是基于ASM字节码生成工具,它是通过继承的方式实现代理类,所以不需要接口,可以直接代理普通的类,但是需要注意的是final方法不可继承。
package org.example;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* Hello world!
*
*/
public class App {
public static void main( String[] args ) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Xiaoming.class);
enhancer.setCallback(new Xiaoshou());
Xiaoming xiaoming =(Xiaoming) enhancer.create();
xiaoming.sellHouse();
}
}
class Xiaoming{
public void sellHouse() {
System.out.println("我是小明,我要开始卖房子了!");
}
}
class Xiaoshou implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 在卖之前要看是不是全款(方法增强)
System.out.println("我是销售,我要看买房客户是不是全款?");
System.out.println("开始传达客户的需求:");
System.out.println("---------------------------------------");
Object object = methodProxy.invokeSuper(o, objects);
// 卖完之后给销售一块钱(方法增强)
System.out.println("---------------------------------------");
System.out.println("我是销售,卖了房子得到了一块钱,小明真是个奸商!");
return object;
}
}
2.4.1 java动态代理源码分析
- 被代理的类:因为cglib是代理的类,所以不需要像之前一样给到接口
- 实现代理的行为逻辑,类似
InvocationHandler
,使用上是不一样的,可以看下代码,需要实现MethodInterceptor
接口:
- 最后通过
字节码生成技术
完成:
2.5 CGLIB代理和java动态代理总结(干货
)
Spring Framework
默认使用的是JDK动态代理,SpringBoot2.x
版本默认动态代理是CGLIB。- JDK动态基于接口实现,通过
java.lang.reflect.Proxy
动态生成代理类。CGLIB基于类继承,通过字节码技术
生成目标类的子类。也就决定了JDK动态代理的使用场景是接口,cglib的使用场景是类,但是一定要注意final方法。 - 简单的比较一下俩者自己的性能
JDK之间的性能随着版本的不同而不同,参考文章:haiq的博客
- jdk6下,在运行次数较少的情况下,jdk动态代理和cglib差不多,甚至更快;当次数调用多了之后,cglib表现稍微更快一下。
- jdk7下,在运行次数较少(1,000,000)的情况下,jdk动态代理比cglib快了差不多30%;当调用次数增加之后(50,000,000),动态代理比cglib快了接近一倍。
- jdk8和jdk7基本一致。