26、动态代理
我们熟悉的 Spring AOP、Dubbo RPC 等框架都有用到动态代理,在平时的开发中,我们也经常利用动态代理,为代码添加额外的功能,比如日志、事务、鉴权限流、性能监控等
在面试中,我们也经常被问及动态代理相关的问题,比如为什么基于 JDK 实现的动态代理必须要求原始类有接口?带着这个问题,我们开始今天的学习
1、什么是静态代理
在《设计模式之美》中,我们有讲到代理模式,代理模式能够在不改变原始类的代码的情况下,通过引入代理类来给原始类附加功能,代理模式分为静态代理和动态代理
我们先通过一个例子来看静态代理是如何工作的
1.1、示例 1
示例代码如下所示,我们有一个 UserController 类,其中包含 register() 和 login() 两个函数
public interface IUserController { UserVo login(String telephone, String password); UserVo register(String telephone, String password); } public class UserController implements IUserController { @Override public UserVo login(String telephone, String password) { // ... 省略逻辑 ... } @Override public UserVo register(String telephone, String password) { // ... 省略逻辑 ... } }
如果我们希望统计 login() 和 register() 这两个函数的执行时间
那么简单的做法是:直接修改这两个函数的代码,在函数内部添加时间统计代码,但是这会导致非业务代码跟业务代码耦合
为了代码的结构更加优美,我们可以使用代理模式,让非业务代码与业务代码解耦
如下所示,代理类 UserControllerProxy 和原始类 UserController 实现相同的接口 IUserController
UserController 类只负责业务功能,UserControllerProxy 类负责在业务代码执行前后附加非业务代码,并通过委托的方式,调用原始类来执行业务代码
public class UserControllerProxy implements IUserController { private static final Logger logger = LoggerFactory.getLogger(UserControllerProxy.class); private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; } @Override public UserVo login(String telephone, String password) { long startTime = System.currentTimeMillis(); UserVo userVo = userController.login(telephone, password); // 委托 UserController long costTime = System.currentTimeMillis() - startTime; logger.info("UserController#login cost time:" + costTime); return userVo; } @Override public UserVo register(String telephone, String password) { long startTime = System.currentTimeMillis(); UserVo userVo = userController.register(telephone, password); // 委托 UserController long costTime = System.currentTimeMillis() - startTime; logger.info("UserController#register cost time:" + costTime); return userVo; } }
因为原始类和代理类实现了相同的接口,所以我们可以轻松将代码中的原始类对象,统一替换为代理类对象,如下代码所示
// 原来的代码 IUserController userController = new UserController(); // 替换之后的代码 IUserController userController = new UserControllerProxy(new UserController());
1.2、示例 2
如果原始类并没有实现接口,并且原始类代码并不是我们开发维护的(比如它来自一个第三方的类库),我们也没办法直接修改原始类,给它重新定义一个接口
在这种情况下,我们该如何实现代理模式呢?
对这种外部类的扩展,我们一般都是采用继承的方式来实现,这里也不例外,我们让代理类继承原始类,然后扩展附加功能,代码如下所示
public class UserControllerProxy extends UserController { private static final Logger logger = LoggerFactory.getLogger(UserControllerProxy.class); @Override public UserVo login(String telephone, String password) { long startTime = System.currentTimeMillis(); UserVo userVo = super.login(telephone, password); // 委托给父类 UserController long costTime = System.currentTimeMillis() - startTime; logger.info("UserController#login cost time:" + costTime); return userVo; } @Override public UserVo register(String telephone, String password) { long startTime = System.currentTimeMillis(); UserVo userVo = super.register(telephone, password); // 委托给父类 UserController long costTime = System.currentTimeMillis() - startTime; logger.info("UserController#register cost time:" + costTime); return userVo; } }
基于继承实现的代理类,我们也可以轻松地将代码中的原始类对象,替换为代理类对象, 如下所示
// 原来的代码 UserController userController = new UserController(); // 替换之后的代码 UserController userController = new UserControllerProxy();
上述方式实现的代理叫做静态代理,虽然静态代理实现非常简单,但它会导致项目中的类成倍增加
当项目中所有的 Controller 类都需要添加时间统计功能时,我们需要为了所有的 Controller 类都创建对应的代理类
显然这样做费时费力,并且不够优雅,于是动态代理便被发明,用来解决这个问题
2、什么是动态代理
静态和动态两个概念在前面章节中也反复提到,一般来讲,静态指的是某个事件发生在编译时,动态指的是某个事件发生在运行时
将这个规则应用到代理模式,
- 静态代理指的就是:在编译时生成代理类的字节码(代理类的 Class 文件)
- 动态代理指的就是:在运行时生成代理类的字节码
代理类的字节码仅仅存在于内存中,并不会生成对应的 class 文件,从而避免了静态代理需要编写大量代理类的问题
之所以可以实现动态代理,是因为 JVM 设计得非常灵活,只要是符合类的格式的字节码,都可以在运行时被 JVM 解析并加载
不管这个字节码是来自预先编译好的(Class 文件),还是在内存中临时生成的(典型应用:动态代理),又或者从网络加载而来的(典型应用:Applet)
这部分内容涉及到 JVM 的类加载机制,我们在后面的章节中再详细讲解
一句话概括一下,动态代理就是动态地生成代理类的字节码,动态代理实现的方式有两类
- 一类是使用 JDK 提供的类来实现
- 一类是使用第三方的字节码类库来实现,比如 CGLIB、BECL、ASM、Javassit 等
字节码类库功能非常强大,可以动态修改字节码、生成字节码,所以生成代理类的字节码也不在话下
不过字节码比较底层,直接编辑字节码,对于程序员是一个很大挑战,因此我们一般采用 JDK 提供的类或者 CGLIB 这种使用起来比较友好的字节码类库来实现动态代理
接下来,我们就介绍一下这两种常用的动态代理实现方式
3、基于 JDK 实现动态代理
我们从基本用法、实现原理、性能分析三个方面,来分析一下基于 JDK 的动态代理实现方式
3.1、基本用法
我们来看下如何使用 JDK 提供的类,为 UserController 类实现动态代理,代码如下所示
当我们为其他 Controller 类中的方法也添加时间统计代码时,我们可以复用 CtrlProxyHandler 类,并通过 Proxy 类的 newProxyInstance() 静态方法生成对应的代理类对象
public class CtrlProxyHandler implements InvocationHandler { private Object originBean; public CtrlProxyHandler(Object originBean) { this.originBean = originBean; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long startTime = System.currentTimeMillis(); Object res = method.invoke(originBean, args); // 调用原始类完成业务逻辑 long costTime = System.currentTimeMillis() - startTime; System.out.println(originBean.getClass().getSimpleName() + "#" + method.getName() + " cost time: " + costTime); return res; } } public class JDKProxyDemo { public static void main(String[] args) { IUserController userController = new UserController(); CtrlProxyHandler handler = new CtrlProxyHandler(userController); IUserController userControllerProxy = (IUserController) Proxy.newProxyInstance( handler.getClass().getClassLoader(), UserController.class.getInterfaces(), handler); userControllerProxy.login("139********", "******"); } }
从上述代码,我们可以发现,基于 JDK 来实现动态代理主要用到了 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类
其中,InvocationHandler 接口比较简单,只包含一个 invoke() 函数,如下所示
简单了解 InvocationHandler 接口之后,我们再来看下 Proxy 类,Proxy 类要比 InvocationHandler 复杂很多,使用 Proxy 类生成代理类和实例化对象的方法如下所示
// Foo 为接口 InvocationHandler handler = new MyInvocationHandler(...); // InvocationHandler Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class); // 生成代理类 Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler); // 实例化对象
大部分情况下,我们只需要用到代理类的实例化对象,所以,上述代码中的后两行代码可以简化为如下一行代码
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] { Foo.class }, handler);
3.2、实现原理
接下来,通过分析 newProxyInstance() 函数的源码,我们来看下基于 JDK 的动态代理的底层实现原理,newProxyInstance() 函数定义如下所示
newProxyInstance() 函数中包含 3 部分逻辑:生成动态代理类、加载动态代理类到 JVM、实例化动态代理类对象
- 参数 loader 表示类加载器,用来加载生成的动态代理类
- 参数 interfaces 表示接口,JDK 依据接口生成动态代理类,接口中包含哪些方法,动态代理类中就包含哪些方法
- 参数 h 用于实例化动态代理类对象
生成动态代理类
我们先来看下生成动态代理类的过程,如下代码所示
newProxyInstance() 函数调用 ProxyGenerator 类(JDK 提供的生成字节码的类),按照类的字节码格式,生成动态代理类的字节码,并存储到内存(proxyClassFile)中
// 生成动态代理类的名称 final String proxyClassNamePrefix = "$Proxy"; long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; // ProxyGenerator 类似字节码类库, 可以生成动态代理类的字节码 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); try { // 通过 JVM 的类加载器来加载动态代理类 return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { // 如果生成的动态代理类的字节码格式有误, 则报错 throw new IllegalArgumentException(e.toString()); }
从上述动态代理类的生成过程,我们可以发现
- 动态代理类具有哪些方法,只跟接口有关,跟原始类没有任何关系,这也是基于 JDK 实现的动态代理,要求原始类必须有接口定义才行的原因
- 除此之外,加载到 JVM 中的类都要有类名,这样才能在实例化对象时按照类名查找到对应的类的定义
- 静态代理类的类名是程序员指定的,动态代理类的类名是自动生成的,如上代码所示,动态代理类的类名由 2 部分构成:$Proxy + 自增编号
如果项目中使用 Proxy 类生成了两个动态代理类,那么它们的名称就分别为:$Proxy0、$Proxy1
以上讲解的动态代理类生成的过程如下图所示
保存并查看动态代理类
上图只展示动态代理类大概的样子,那么动态代理类具体长什么样子呢?每个函数都是如何实现的呢?
我们可以将通过 ProxyGenerator 类生成的动态代理类的字节码,保存在文件中,然后再通过反编译工具,得到对应的 Java 文件,这样就能知道动态代理类具体长什么样子了
动态代理类字节码的保存方法如下代码所示
public class ProxyUtils { public static void main(String[] args) { byte[] byteCodes = ProxyGenerator.generateProxyClass( "CtrlProxy", new Class[]{IUserController.class} ); try (FileOutputStream out = new FileOutputStream("F:\\test-file\\CtrlProxy.class")) { out.write(byteCodes); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
执行上述代码,我们得到 CtrlProxy.class 字节码文件,然后再利用反编译工具,反编译 CtrlProxy.class 字节码文件,于是就得到对应的 Java 文件,如下所示
m3、m4 对应 IUserController 接口中定义的方法,也就是 login() 和 register(),m0、m1、m2 为继承自顶级父类 Object 类的方法,也就是 toString()、equals()、hashCode()
public final class CtrlProxy extends Proxy implements IUserController { private static Method m0; // hashCode() private static Method m1; // equals() private static Method m2; // toString() private static Method m3; // login() private static Method m4; // register() public CtrlProxy(InvocationHandler var1) throws { super(var1); } public final UserVo login(String var1, String var2) throws { try { return (UserVo) super.h.invoke(this, m3, new Object[]{var1, var2}); } catch (RuntimeException | Error var4) { throw var4; } catch (Throwable var5) { throw new UndeclaredThrowableException(var5); } } public final UserVo register(String var1, String var2) throws { try { return (UserVo) super.h.invoke(this, m4, new Object[]{var1, var2}); } catch (RuntimeException | Error var4) { throw var4; } catch (Throwable var5) { throw new UndeclaredThrowableException(var5); } } 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); } } 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); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m4 = Class.forName("demo.proxy.IUserController").getMethod("register", Class.forName("java.lang.String"), Class.forName("java.lang.String")); m2 = Class.forName("java.lang.Object").getMethod("toString"); m3 = Class.forName("demo.proxy.IUserController").getMethod("login", Class.forName("java.lang.String"), Class.forName("java.lang.String")); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
上述代码中,类型为 InvocationHandler 的 super.h 的值,在实例化动态代理类对象时,赋值为自定义的 InvocationHandler 实现类对象(比如 CtrlProxyHandler 类对象)
如下简化之后的 newProxyInstance() 函数代码所示
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) { // (1) 生成动态代理类 // (2) 加载动态代理类 Class<?> cl = getProxyClass0(loader, interfaces); // (3) 实例化动态代理类对象 final Class<?>[] constructorParams = {InvocationHandler.class}; final Constructor<?> cons = cl.getConstructor(constructorParams); return cons.newInstance(new Object[]{h}); }
当我们调用动态代理类上方法时,比如调用 login() 方法,login() 方法会调用 CtrlProxyHandler 上的 invoke() 方法
invoke() 方法执行一些附加逻辑,然后再拿传递过来的方法和参数,利用反射在原始类对象上执行,如下图所示
3.3、性能分析
从上述对基于 JDK 实现动态代理的原理分析,我们可以发现,基于 JDK 实现动态代理,耗时的地方主要有两处:一是运行时动态生成代理类的字节码,二是利用反射执行方法
对于反射执行方法的性能,我们在第 24 节讲解反射时已经分析过了,反射会额外增加一部分耗时
- 如果方法中的逻辑比较复杂,执行时间比较长,那么相比而言,反射额外增加的耗时可以忽略
- 相反,如果方法中的逻辑比较简单,执行时间很短,那么反射额外增加的耗时就不能忽略了
对于动态生成代理类的字节码,可想而知,这部分耗时确实会比较多,对比静态代理,静态代理类的字节码是在编译时生成的,不占用运行时间
4、基于 CGLIB 实现动态代理
基于 CGLIB 实现动态代理,跟基于 JDK 实现动态代理相比,基本用法和实现原理非常类似
前面已经详细讲解了基于 JDK 的动态代理实现方式,接下来,我们对基于 CGLIB 的动态代理实现方式就简单介绍一下
使用 CGLIB 需要先在项目中引入 CGLIB 包,Maven 配置如下所示
<!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
基于 CGLIB 实现动态代理,也要用到一个核心接口 MethodInterceptor 和一个核心类 Enhancer,它们的用法跟 JDK 中的 InvocationHandler 接口和 Proxy 类相似
示例代码如下所示,这里我就不详细讲解用法了,留给你自己查阅
public class ProxyFactory implements MethodInterceptor { private Object originBean; public ProxyFactory(Object originBean) { this.originBean = originBean; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { long startTime = System.currentTimeMillis(); Object res = method.invoke(originBean, args); // 调用原始类完成业务逻辑 long costTime = System.currentTimeMillis() - startTime; System.out.println(originBean.getClass().getSimpleName() + "#" + method.getName() + " cost time: " + costTime); return res; } } public class CGLIBProxyDemo { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserController.class); enhancer.setCallback(new ProxyFactory(new UserController())); UserController userControllerProxy = (UserController) enhancer.create(); userControllerProxy.login("", ""); } }
5、JDK 与 CGLIB 异同点
在讲解静态代理时,我们提到,静态代理也有两种实现方式,一种是基于接口,一种基于继承,同理,动态代理也有基于接口和基于继承这两种实现方式
- 基于JDK 的动态代理实现方式,使用接口来生成动态代理类,要求原始类必须定义接口,因此是一种基于接口的动态代理实现方式
- 基于 CGLIB 的动态代理实现方式,并不依赖接口,通过继承原始类来生成动态代理类,因此是一种基于继承的动态代理实现方式
基于 CGLIB 的动态代理实现方式的实现原理,跟基于 JDK 的动态代理的实现方式的实现原理,大致是一样的,根据接口或原始类,在运行时,动态生成代理类字节码
在执行代理类对象上的方法时,委托给实现了 InvocationHandler 或者 MethodInterceptor 接口的类对象,使用反射执行方法,并附加执行额外代码
因此基于 CGLIB 的动态代理实现方式,耗时的地方也是生成动态代理类字节码和利用反射执行方法
实际上,为了优化性能,CGLIB 可以不使用 Java 反射来执行方法
如下代码所示,MethodProxy 的 invokeSuper() 方法底层依赖 CGLIB 自己实现的反射(也就是 net.sf.cglib.reflect.FastClass)来执行方法
不过即便如此,两种动态代理实现方式的性能仍然相差无几,甚至在高版本的 JDK 中,基于 JDK 实现的的动态代理,比基于 CGLIB 实现的动态代理性能还要好
public class ProxyFactory implements MethodInterceptor { private Object originBean; public ProxyFactory(Object originBean) { this.originBean = originBean; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { long startTime = System.currentTimeMillis(); // Object res = method.invoke(originBean, args); // Java 反射 Object res = methodProxy.invokeSuper(obj, args); // CGLIB 反射 long costTime = System.currentTimeMillis() - startTime; System.out.println(originBean.getClass().getSimpleName() + "#" + method.getName() + " cost time: " + costTime); return res; } }
6、课后思考题
请描述一下 Spring AOP 的底层实现原理
Spring AOP 基于动态代理来实现,既可以使用 JDK 动态代理,也可以使用 CGlib 动态代理 Spring AOP 生成动态代理类,在原业务类的基础之上附加功能,比如日志、统计、限流等等
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17471401.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步