面试填坑笔记-从代理模式到SpringAOP的动态代理
代理模式是一种理论上非常简单,但是各种地方的实现往往却非常复杂。本文将从代理模式的基本概念出发,探讨代理模式在java领域的应用与实现。读完本文你将get到以下几点:
- 为什么需要代理模式,它通常用来解决什么问题,以及代理模式的设计与实现思路
- Java领域中代理模式3种不同实现类型(静态代理,jdk动态代理,cglib)
- 代理模式的面试考点
为什么要使用代理模式
在生活中我们通常是去商场购买东西,而不是去工厂。最主要的原因可能有以下几种:
- 成本太高,去工厂路途遥远成本太高,并且可能从工厂进货要办理一些手续流程;
- 工厂不直接卖给你,毕竟可能设计到一些行业机密或者无良厂家有一些不想让你知道的东西;
- 商场能提供一些商品之外的服务,商场里有舒适的温度,整洁的洗手间,当然还有漂亮的小姐姐。
在面向对象的系统中也有同样的问题,有些对象由于某种原因,比如对象创建开销很大,或者某些操作需要安全控制等,直接访问会给使用者或者系统结构带来很多麻烦,这时我们就需要考虑使用代理模式。
在应用中我们可能会用代理模式解决以下问题:
- 权限控制与日志, 在客户端请求接口时我们可能需要在调用之前对权限进行验证,或者通过记录接口调用前后时间,统计执行时长,又或者说我们需要记录用户的一些操作日志信息等,我们可以对原接口进行代理,然后根据需求在接口执行前后增加一些特定的操作。
- 重量级操作, 比如创建开销大的对象, 可以先由代理对象扮演对象的替身,在需要的使用再创建对象,然后代理再将请求委托给真实的对象。
什么是代理模式
代理模式:为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。类图如下:
所谓控制,其实使用接口隔离其他对象与这个对象之间的交互;就是为client对象对RealSubject对象的访问一种隔离,本质上就是CLient→RealSuject的关系变成了Client→Subject, Proxy→RealSubject。 需要注意的时,代理类(Proxy)并不一定要求保持接口的完整的一致性(既也可以完全不需实现Subject接口),只要能够实现间接控制即可。
代理模式代码演进
背景:假设已有一个订单系统,可以保存订单信息。
需求:打印保存订单信息消耗时间。
/**
* 订单服务
*
* @author cruder
* @date 2019-11-23 15:42
**/
public class OrderService2 {
/**
* 保存订单接口
*/
public void saveOrder(String orderInfo) throws InterruptedException {
// 随机休眠,模拟订单保存需要的时间
Thread.sleep(System.currentTimeMillis() & 100);
System.out.println("订单:" + orderInfo + " 保存成功");
}
}
普通方式实现
直接修改源代码,这通常也是最简单和最容易想到的实现。
/**
* 保存订单接口, 直接修改代码
*/
public void saveOrder(String orderInfo) throws InterruptedException {
long start = System.currentTimeMillis();
// 随机休眠,模拟订单保存需要的时间
Thread.sleep(System.currentTimeMillis() & 100);
System.out.println("订单:" + orderInfo + " 保存成功");
System.out.println("保存订单用时: " + (System.currentTimeMillis() - start) + "ms");
}
面向对象设计原则中的“开闭原则”告诉我们,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
代理模式实现
/**
* 1. 定义接口,为了使代理被代理对象看起来一样。当然这一步完全可以省略
*
* @author cruder
* @date 2019-11-23 15:58
**/
public interface IOrderService {
/**
* 保存订单接口
* @param orderInfo 订单信息
*/
void saveOrder(String orderInfo) throws InterruptedException;
}
/**
* 2. 原有订单服务,也实现这个接口。注意 此步骤也完全可以省略。
*
* @author cruder
* @date 2019-11-23 15:42
**/
public class OrderService implements IOrderService{
/**
* 保存订单接口
*/
@Override
public void saveOrder(String orderInfo) throws InterruptedException {
// 随机休眠,模拟订单保存需要的时间
Thread.sleep(System.currentTimeMillis() & 100);
System.out.println("订单:" + orderInfo + " 保存成功");
}
}
/**
* 3. 创建代理类,实现订单服务接口【这才是代理模式的实现】
*
* @author cruder
* @date 2019-11-23 16:01
**/
public class OrderServiceProxy implements IOrderService{
/**
* 内部持有真实的订单服务对象,保存订单工作实际由它来完成
*/
private IOrderService orderService;
@Override
public void saveOrder(String orderInfo) throws InterruptedException {
/**
* 延迟初始化,也可以创建代理对象时就创建,或者作为构造参数传进来
* 仅作为代码实例,不考虑线程安全问题
*/
if (orderService == null) {
orderService = new OrderService();
}
long start = System.currentTimeMillis();
orderService.saveOrder(orderInfo);
System.out.println("保存订单用时: " + (System.currentTimeMillis() - start) + "ms");
}
}执行程序
执行程序
代理模式的优缺点
优点: 1、职责清晰。 2、高扩展性。 3、智能化。
缺点:
1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
Java中代理模式的实现
在java中代理模式可以按照代理类的创建时机分两类,即静态代理和动态代理,而动态代理又可以分为jdk动态代理和cglib动态代理。每种实现方式都各有千秋,接下来笔者将回针对不同的实现方式进行演示和剖析。
静态代理
在上文代理模式代码演进中就使用了静态代理模式。所谓静态代理中的“静”字,无非就是代理类的创建时机不同罢了。静态代理需要为每个被代理的对象手动创建一个代理类;而动态代理则时在运行时通过某种机制来动态生成,不需要手动创建代理类。
动态代理 - jdk
jdk动态代理模式是利用java中的反射技术,在运行时动态创建代理类。接下来我们仍借助上文中的订单服务的案例,使用jdk动态代理实现。
基于动态jdk涉及到两个核心的类Proxy类和一个 InvocationHandler接口。
/**
* 基于JDK技术 动态代理类技术核心 Proxy类和一个 InvocationHandler 接口
*
* @author cruder
* @date 2019-11-23 16:40
**/
public class ProxyFactory implements InvocationHandler {
/**
* 委托对象,既被代理的对象
*/
private Object target;
public ProxyFactory (Object target) {
this.target = target;
}
/**
* 生成代理对象
* 1. Classloader loader: 制定当前被代理对象使用的累加子啊其,获取加载器的方法固定
* 2. Class<?>[] interfaces: 委托类的接口类型,使用泛型方法确认类型
* 3. InvocationHandler handler: 事件处理,执行委托对象的方法时会触发事件处理器方法,
* 会把当前执行的委托对象方法作为参数传入
*/
public Object getProxyInstance() {
Class clazz = target.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
method.invoke(target, args);
System.out.println("保存订单用时: " + (System.currentTimeMillis() - start) + "ms");
return null;
}
}
/**
* 通过动态代理方式来保存订单
*
* @author cruder
* @date 2019-11-23 15:49
**/
public class Client {
public static void main(String[] args) throws InterruptedException {
ProxyFactory proxyFactory= new ProxyFactory (new OrderService());
IOrderService orderService = (IOrderService) proxyFactory.getProxyInstance();
orderService.saveOrder(" cruder 新买的花裤衩 ");
}
}
以上便是jdk动态代理的全部实现,有种只可意会不可言传的感觉,笔者始终感觉这种实现看起来很别扭。不过也要强行总结以下,jdk实现动态代理可以分为以下几个步骤:
- 先检查委托类是否实现了相应接口,保证被访问方法在接口中也要有定义
- 创建一个实现InvocationHandler接口的类
- 在类中定义一个被代理对象的成员属性,为了扩展方便可以直接使用Object类,也可以根据需求定义相应的接口
- 在invoke方法中实现对委托对象的调用,根据需求对方法进行增强
- 使用Proxy.newProxyInstance(...)方法创建代理对象,并提供要给获取代理对象的方法
代理类源码阅读
上文中基于jdk动态代理的代码实现中对于可*的产品经理来说已经完全满足了需求,但是对于具有Geek精神的程序员来说这远远不够,对于这种不知其所以然的东西往往让人感到不安。接下来我们将通过自定义的一个小工具类将动态生成的代理类保存到本地来一看究竟。
/**
* 将生成的代理类保存为.class文件的工具类
*
* @author cruder
* @date 2019-08-15 0:27
*/
public class ProxyUtils {
/**
* 将代理类保存到指定路径
*
* @param path 保存到的路径
* @param proxyClassName 代理类的Class名称
* @param interfaces 代理类接口
* @return
*/
public static boolean saveProxyClass(String path, String proxyClassName, Class[] interfaces){
if (proxyClassName == null || path == null) {
return false;
}
// 获取文件字节码,然后输出到目标文件中
byte[] classFile = ProxyGenerator.generateProxyClass(proxyClassName, interfaces);
try (FileOutputStream out = new FileOutputStream(path)) {
out.write(classFile);
out.flush();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
}
// 此处是重点, 生成的代理类实现了IOrderService,并且继承了Proxy
public final class $Proxy0 extends Proxy implements IOrderService {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void saveOrder(Order var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
// 通过反射获取Method对象
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("cn.mycookies.test08proxy.IOrderService").getMethod("saveOrder", Class.forName("cn.mycookies.test08proxy.Order"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
ps: 实习转正面试中被问到为什么jdk动态代理被代理的类为什么要实现接口?
cglib动态代理
对于cglib我想大多数人应该都很陌生,或者是在学习Spring中AOP(面向切面编程)时听说了它使用jdk和cglib两种方式实现了动态代理。接下来笔者将针对cglib进行简要介绍。
cglib动态代理和jdk动态代理类似,也是采用操作字节码机制,在运行时生成代理类。cglib 动态代理采取的是创建目标类的子类的方式,因为是子类化,我们可以达到近似使用被调用者本身的效果。
字节码处理机制-指得是ASM来转换字节码并生成新的类
注:spring中有完整的cglib相关的依赖,所以以下代码基于spring官方下载的demo中直接进行编写的
/**
* 1. 订单服务-委托类,不需要再实现接口
*
* @author cruder
* @date 2019-11-23 15:42
**/
public class OrderService {
/**
* 保存订单接口
*/
public void saveOrder(String orderInfo) throws InterruptedException {
// 随机休眠,模拟订单保存需要的时间
Thread.sleep(System.currentTimeMillis() & 100);
System.out.println("订单:" + orderInfo + " 保存成功");
}
}
/**
* cglib动态代理工厂
*
* @author cruder
* @date 2019-11-23 18:36
**/
public class ProxyFactory implements MethodInterceptor {
/**
* 委托对象, 即被代理对象
*/
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
/**
* 返回一个代理对象
* @return
*/
public Object getProxyInstance(){
// 1. 创建一个工具类
Enhancer enhancer = new Enhancer();
// 2. 设置父类
enhancer.setSuperclass(target.getClass());
// 3. 设置回调函数
enhancer.setCallback(this);
// 4.创建子类对象,即代理对象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("cglib代理:保存订单用时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
/**
* 使用cglib代理类来保存订单
*
* @author cruder
* @date 2019-11-23 15:49
**/
public class Client {
public static void main(String[] args) throws InterruptedException {
// 1. 创建委托对象
OrderService orderService = new OrderService();
// 2. 获取代理对象
OrderService orderServiceProxy = (OrderService) new ProxyFactory(orderService).getProxyInstance();
String saveFileName = "CglibOrderServiceDynamicProxy.class";
ProxyUtils.saveProxyClass(saveFileName, orderService.getClass().getSimpleName(), new Class[]{IOrderService.class});
orderServiceProxy.saveOrder(" cruder 新买的花裤衩 ");
}
}
cglib动态代理实现步骤和jdk及其相似,可以分为以下几个步骤:
- 创建一个实现MethodInterceptor接口的类
- 在类中定义一个被代理对象的成员属性,为了扩展方便可以直接使用Object类,也可以根据需求定义相应的接口
- 在invoke方法中实现对委托对象的调用,根据需求对方法进行增强
- 使用Enhancer创建生成代理对象,并提供要给获取代理对象的方法
cglib动态代理生成的代理类和jdk动态代理代码格式上几乎没有什么区别,唯一的区别在于cglib生成的代理类继承了仅仅Proxy类,而jdk动态代理生成的代理类继承了Proxy类的同时也实现了一个接口。代码如下:
// 生成一个Proxy的子类
public final class OrderService extends Proxy {
private static Method m1;
private static Method m2;
private static Method m0;
public OrderService(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
jdk动态代理 VS cglib
JDK Proxy 的优势:
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
- 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
- 代码实现简单。
cglib 优势:
- 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。
- 只操作我们关心的类,而不必为其他相关类增加工作量。
总结
- 代理模式: 为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。
- jdk动态代理生成的代理类继承了Proxy类并实现了被代理的接口;而cglib生成的代理类则仅继承了Proxy类。
- jdk动态代理最大缺点:只能代理接口,既委托类必须实现相应的接口
- cglib缺点:由于是通过“子类化”的方式, 所以不能代理final的委托类或者普通委托类的final修饰的方法。
Q&A
- 为什么jdk动态代理只能代理接口?
- Spring中AOP的实现采用那种代理方式?
- 都说jdk动态代理性能远比cglib要差,如果是,依据是什么?