前言

之前已经用了5篇文章完整解释了java动态代理的原理,本文将会为这个系列补上最后一块拼图,展示java动态代理的使用方式和应用场景

主要分为以下4个部分

1.为什么要使用java动态代理

2.如何使用java动态代理

3.框架中java动态代理的应用

4.java动态代理的基本原理

1.为何要使用动态代理

在设计模式中有一个非常常用的模式:代理模式。学术一些来讲,就是为某些对象的某种行为提供一个代理对象,并由代理对象完全控制该行为的实际执行。

通俗来说,就是我想点份外卖,但是手机没电了,于是我让同学用他手机帮我点外卖。在这个过程中,其实就是我同学(代理对象)帮我(被代理的对象)代理了点外卖(被代理的行为),在这个过程中,同学可以完全控制点外卖的店铺、使用的APP,甚至把外卖直接吃了都行(对行为的完全控制)

因此总结一下代理的4个要素:

代理对象

被代理的行为

被代理的对象

行为的完全控制

从实际编码的角度来说,我们假设遇到了这样一个需求,需要记录下一些方法的执行时间,于是最简单的方式当然就是在方法的开头记录一个时间戳,在return之前记录一个时间戳。但如果方法的流程很复杂,例如:

  1. public class Executor {
  2. public void execute(int x, int y) {
  3. log.info("start:{}", System.nanoTime());
  4. if (x == 3) {
  5. log.info("end:{}", System.nanoTime());
  6. return;
  7. }
  8. for (int i = 0; i < 100; i++) {
  9. if (y == 5) {
  10. log.info("end:{}", System.nanoTime());
  11. return;
  12. }
  13. }
  14. log.info("end:{}", System.nanoTime());
  15. return;
  16. }
  17. }

我们需要在每一个return前都增加一行记录时间戳的代码,很麻烦。于是我们想到可以由方法的调用者来记录时间,例如:

  1. public class Invoker {
  2. private Executor executor = new Executor();
  3. public void invoke() {
  4. log.info("start:{}", System.nanoTime());
  5. executor.execute(1, 2);
  6. log.info("end:{}", System.nanoTime());
  7. }
  8. }

我们又遇到一个问题,如果该方法在很多地方调用,或者需要记录的方法有多个,那么依然会面临重复手动写log代码的问题。

于是,我们就可以考虑创建一个代理对象,让它负责帮我们统一记录时间戳,例如:

  1. public class Proxy {
  2. Executor executor = new Executor();
  3. public void execute(int x, int y) {
  4. log.info("start:{}", System.nanoTime());
  5. executor.execute(x, y);
  6. log.info("start:{}", System.nanoTime());
  7. }
  8. }

而在Invoker中,则由直接调用Executor中的方法改为调用Proxy的方法,当然方法的名字和签名是完全相同的。当其他地方需要调用execute方法时,只需要调用Proxy中的execute方法,就会自动记录下时间戳,而对于使用者来说是感知不到区别的。如下示例:

  1. public class Invoker {
  2. private Proxy executor;
  3. public void invoke() {
  4. executor.execute(1, 2);
  5. }
  6. }

上面展示的代理,就是一个典型的静态代理,“静态”体现在代理方法是我们直接编码在类中的。

接着我们就遇到了下一个问题,如果Executor新增了一个方法,同样要记录时间,那我们就不得不修改Proxy的代码。并且如果其他类也有同样的需求,那就需要新建不同的Proxy类才能较好的实现该功能,同样非常麻烦。

那么我们就需要将静态代理升级成为动态代理了,而“动态”正是为了优化前面提到的2个静态代理遇到的问题。

2.如何使用java动态代理

创建java动态代理需要使用如下类

  1. java.lang.reflect.Proxy

调用其newProxyInstance方法,例如我们需要为Map创建一个代理:

  1. Map mapProxy = (Map) Proxy.newProxyInstance(
  2. HashMap.class.getClassLoader(),
  3. new Class[]{Map.class},
  4. new InvocationHandler(){...}
  5. );

我们接着就来分析这个方法。先查看其签名:

  1. public static Object newProxyInstance(ClassLoader loader,
  2. Class<?>[] interfaces,
  3. InvocationHandler h)

ClassLoader类型的loader:被代理的类的加载器,可以认为对应4要素中的被代理的对象

Class数组的interfaces:被代理的接口,这里其实对应的就是4要素中的被代理的行为,可以注意到,这里需要传入的是接口而不是某个具体的类,因此表示行为。

InvocationHandler接口的h:代理的具体行为,对应的是4要素中的行为的完全控制,当然也是java动态代理的核心。

最后返回的对象Object对应的是4要素中的代理对象

接着我们来示例用java动态代理来完成记录方法执行时间戳的需求:

首先定义被代理的行为,即接口:

  1. public interface ExecutorInterface {
  2. void execute(int x, int y);
  3. }

接着定义被代理的对象,即实现了接口的类:

  1. public class Executor implements ExecutorInterface {
  2. public void execute(int x, int y) {
  3. if (x == 3) {
  4. return;
  5. }
  6. for (int i = 0; i < 100; i++) {
  7. if (y == 5) {
  8. return;
  9. }
  10. }
  11. return;
  12. }
  13. }

接着是代理的核心,即行为的控制,需要一个实现了InvocationHandler接口的类:

  1. public class TimeLogHandler implements InvocationHandler {
  2. @Override
  3. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  4. return null;
  5. }
  6. }

这个接口中的方法并不复杂,我们还是先分析其签名

Object类型的proxy:最终生成的代理对象

Method类型的method:被代理的方法。这里其实是2个要素的复合,即被代理的对象是如何执行被代理的行为的。因为虽然我们说要对行为完全控制,但大部分时候,我们只是对行为增添一些额外的功能,因此依然是要利用被代理对象原先的执行过程的。

Object数组的args:方法执行的参数

因为我们的目的是要记录方法的执行的时间戳,并且原方法本身还是依然要执行的,所以在TimeLogHandler的构造函数中,将一个原始对象传入,method在调用invoke方法时即可使用。

定义代理的行为如下:

  1. public class TimeLogHandler implements InvocationHandler {
  2. private Object target;
  3. public TimeLogHandler(Object target) {
  4. this.target = target;
  5. }
  6. @Override
  7. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  8. log.info("start:{}", System.nanoTime());
  9. Object result = method.invoke(target, args);
  10. log.info("end:{}", System.nanoTime());
  11. return result;
  12. }
  13. }

接着我们来看Invoker如何使用代理,这里为了方便演示我们是在构造函数中实例化代理对象,在实际使用时可以采用依赖注入或者单例等方式来实例化:

  1. public class Invoker {
  2. private ExecutorInterface executor;
  3. public Invoker() {
  4. executor = (ExecutorInterface) Proxy.newProxyInstance(
  5. Executor.class.getClassLoader(),
  6. new Class[]{ExecutorInterface.class},
  7. new TimeLogHandler(new Executor())
  8. );
  9. }
  10. public void invoke() {
  11. executor.execute(1, 2);
  12. }
  13. }

此时如果Exector新增了任何方法,那么Invoker和TimeLogHandler将不需要任何改动就可以支持新增方法的的时间戳记录,有兴趣的同学可以自己尝试一下。

另外如果有其他类也需要用到时间戳的记录,那么只需要和Executor一样,通过Proxy.newProxyInstance方法创建即可,而不需要其他的改动了。

3.框架中java动态代理的应用

接着我们看一下java动态代理在现在的一些常用框架中的实际应用

Spring AOP

spring aop是我们spring项目中非常常用的功能。

例如我们在获取某个数据的时候需要先去redis中查询是否已经有缓存了,如果没有缓存再去读取数据库。我们就可以定义如下的一个切面和行为,然后在需要该功能的方法上增加相应注解即可,而不再需要每个方法单独写逻辑了。如下示例:

  1. @Aspect
  2. @Component
  3. public class TestAspect {
  4. /**
  5. * 表示所有有cn.tera.aop.RedisPoint注解的方法
  6. * 都会执行先读取Redis的行为
  7. */
  8. @Pointcut("@annotation(cn.tera.aop.RedisPoint)")
  9. public void pointCut() {
  10. }
  11. /**
  12. * 实际获取数的流程
  13. */
  14. @Around("pointCut()")
  15. public Object advise(ProceedingJoinPoint joinPoint) {
  16. try {
  17. /**
  18. * 先去查询redis
  19. */
  20. Object data = RedisUtility.get(some_key);
  21. if (data == null) {
  22. /**
  23. * joinPoint.proceed()表示执行原方法
  24. * 如果redis中没有缓存,那么就去执行原方法获取数据
  25. * 然后塞入redis中,下次就能直接获取到缓存了
  26. */
  27. data = joinPoint.proceed();
  28. RedisUtility.put(some_key, data);
  29. }
  30. return data;
  31. } catch (Throwable r) {
  32. return null;
  33. }
  34. }
  35. }

而其背后的原理使用的就是java动态代理。当然这里要求被注解的方法所在的类必须是实现了接口的(回想下Proxy.newProxyInstance方法的签名),否则就需要使用另外一个GCLib的库了,不过这就是另外一个故事了,这里就不展开了。

Spring AOP中大部分情况下都是给原执行逻辑添加一些东西。

RPC框架

在一些rpc框架中,客户端只需要关注接口的的调用,而具体的远程请求则由框架内部实现,例如我们模拟一个简单的rpc 请求,接口如下:

  1. public interface OrderInterface {
  2. /**
  3. * 生成一张新订单
  4. */
  5. void addOrder();
  6. }

rpc框架可以生成接口的代理对象,例如:

  1. public class SimpleRpcFrame {
  2. /**
  3. * 创建一个远程请求代理对象
  4. */
  5. public static <T> T getRemoteProxy(Class<T> service) {
  6. return (T) Proxy.newProxyInstance(service.getClassLoader(),
  7. new Class<?>[]{service},
  8. new RpcHandler(service));
  9. }
  10. /**
  11. * 处理具体远程调用的类
  12. */
  13. static class RpcHandler implements InvocationHandler {
  14. private Class service;
  15. public RpcHandler(Class service) {
  16. this.service = service;
  17. }
  18. @Override
  19. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  20. /**
  21. * 根据接口名和方法名,发起一次特定的网络请求,获取数据
  22. */
  23. Object result = RemoteCallUtility.request(service.getName(), method.getName(), args);
  24. return result;
  25. }
  26. }
  27. }

而客户端调用的时候不需要接口的具体实现,只需要通过rpc框架获取接口的代理即可,此时究竟是采用http协议或者直接通过socket请求数据都交由框架负责了,例如:

  1. public class RpcInvoker {
  2. public void invoke() {
  3. OrderInterface order = SimpleRpcFrame.getRemoteProxy(OrderInterface.class);
  4. order.addOrder();
  5. }
  6. }

RPC中的代理则是完全不需要原执行逻辑,而是完全地控制了行为的执行过程

那么框架使用java动态代理的示例就介绍到此。

4.java动态代理的基本原理

之前我已经通过5篇文章完整介绍了java动态代理的实现原理,不过因为实在有些晦涩,所以这里我抛弃细节代码的解析,使得大家尽量从直觉的角度来理解其基本原理。

假设我们还是实现一开始的添加时间戳的功能,此时,我们需要如下代码获取其代理:

  1. ExecutorInterface executor = (ExecutorInterface) Proxy.newProxyInstance(
  2. Executor.class.getClassLoader(),
  3. new Class[]{ExecutorInterface.class},
  4. new TimeLogHandler()
  5. );
  6. executor.execute(1, 2);

此时,我们打印一下executor的实际类名、所实现的接口和父类的名称,得到结果如下:

  1. 类名:com.sun.proxy.$Proxy11
  2. 父类:java.lang.reflect.Proxy
  3. 实现接口:ExecutorInterface

因此,生成的代理类有如下3个特点:

1.继承了Proxy类

2.实现了我们传入的接口

3.以$Proxy+随机数字的命名

接着我们还是需要略微查看一下newProxyInstance方法的源码,只需要关心下面几行核心代码,如下:

  1. public static Object newProxyInstance(ClassLoader loader,
  2. Class<?>[] interfaces,
  3. InvocationHandler h)
  4. throws IllegalArgumentException
  5. {
  6. ...
  7. /**
  8. * 根据我们传进来的接口创建一个类
  9. */
  10. Class<?> cl = getProxyClass0(loader, intfs);
  11. ...
  12. /**
  13. * 找到类的构造方法,该构造方法获取一个InvocationHandler类型的参数
  14. */
  15. final Constructor<?> cons = cl.getConstructor(constructorParams);
  16. ...
  17. /**
  18. * 通过构造方法生成代理类的实例
  19. */
  20. return cons.newInstance(new Object[]{h});
  21. }

因此总结一下动态代理对象创建的过程

1.根据我们传入的接口动态地创建一个Class

2.获取类的构造函数

3.将InvocationHandler作为参数传入构造函数,实例化代理对象的实例,并将其返回

当然,这里最核心的方法自然是类的创建,简而言之,就是在运行时,一个字节一个字节地构造一个字节数组,而这个字节数组正是一个.class字节码,然后通过一个native方法,将其转化为我们运行时的Class类。

再通俗一些来说:平时我们使用的类都是预先编译好的.class文件,而动态代理则是直接在运行时通过组装一个byte数组的方式创建一个.class文件,这样应该就是比较好理解了吧。

如果对这个byte数组是如何构建的有兴趣,那么欢迎看一下我之前写的5篇文章,里面不仅介绍了动态代理的源码,还能深入了解一下class字节码更细节的结构

1.https://www.cnblogs.com/tera/p/13267630.html

2.https://www.cnblogs.com/tera/p/13280547.html

3.https://www.cnblogs.com/tera/p/13336627.html

4.https://www.cnblogs.com/tera/p/13419025.html

5.https://www.cnblogs.com/tera/p/13442018.html

最后,我们来看一下这个生成出来的代理类究竟长啥样,正符合我们之前总结出的代理对象的3个特点(在之前的文章中也有展示如何看到该内容)。特别注意的是因为所有的类都是继承自Object,因此除了我们自己接口中定义的方法,还会有Object类的种的方法:

  1. public final class $Proxy11 extends Proxy implements ExecutorInterface {
  2. private static Method m1;
  3. private static Method m2;
  4. private static Method m0;
  5. private static Method m3;
  6. public $Proxy11(InvocationHandler var1) throws {
  7. super(var1);
  8. }
  9. public final boolean equals(Object var1) throws {
  10. try {
  11. return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
  12. } catch (RuntimeException | Error var3) {
  13. throw var3;
  14. } catch (Throwable var4) {
  15. throw new UndeclaredThrowableException(var4);
  16. }
  17. }
  18. public final String toString() throws {
  19. try {
  20. return (String)super.h.invoke(this, m2, (Object[])null);
  21. } catch (RuntimeException | Error var2) {
  22. throw var2;
  23. } catch (Throwable var3) {
  24. throw new UndeclaredThrowableException(var3);
  25. }
  26. }
  27. public final int hashCode() throws {
  28. try {
  29. return (Integer)super.h.invoke(this, m0, (Object[])null);
  30. } catch (RuntimeException | Error var2) {
  31. throw var2;
  32. } catch (Throwable var3) {
  33. throw new UndeclaredThrowableException(var3);
  34. }
  35. }
  36. public final void execute(int var1, int var2) throws {
  37. try {
  38. super.h.invoke(this, m3, new Object[]{var1, var2});
  39. } catch (RuntimeException | Error var4) {
  40. throw var4;
  41. } catch (Throwable var5) {
  42. throw new UndeclaredThrowableException(var5);
  43. }
  44. }
  45. static {
  46. try {
  47. m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
  48. m2 = Class.forName("java.lang.Object").getMethod("toString");
  49. m0 = Class.forName("java.lang.Object").getMethod("hashCode");
  50. m3 = Class.forName("cn.tera.aopproxy.proxyuse.ExecutorInterface").getMethod("execute", Integer.TYPE, Integer.TYPE);
  51. } catch (NoSuchMethodException var2) {
  52. throw new NoSuchMethodError(var2.getMessage());
  53. } catch (ClassNotFoundException var3) {
  54. throw new NoClassDefFoundError(var3.getMessage());
  55. }
  56. }
  57. }

到此,java动态代理的基本介绍就结束了

最后我们总结一下java动态代理的思想和原理

1.代理的4要素:代理对象、被代理的行为、被代理的对象、行为的完全控制

2.代理的应用:方便地为某些行为添加一些共同的逻辑(Spring AOP)或者是将行为的执行完全交由代理控制(RPC)

3.java动态代理的原理:在运行时构建一个class字节码数组,并将其转换成一个运行时的Class对象,然后构造其实例

Java动态代理——框架中的应用场景和基本原理的更多相关文章

  1. java动态代理框架

             java动态代理是一个挺有意思的东西,他有时候可以被使用的很灵活.像rpc的调用,调用方只是定义的一个接口,动态代理让他匹配上对应的不同接口:mybatis内部的实现,编码时,只是实 ...

  2. 详解java动态代理机制以及使用场景

    详解java动态代理机制以及使用场景 https://blog.csdn.net/u011784767/article/details/78281384 深入理解java动态代理的实现机制 https ...

  3. 【转载】Java 动态代理

    Java 动态代理 本文为 Android 开源项目源码解析 公共技术点中的 动态代理 部分项目地址:Jave Proxy,分析的版本:openjdk 1.6,Demo 地址:Proxy Demo分析 ...

  4. 深入浅出Java动态代理

    文章首发于[博客园-陈树义],点击跳转到原文深入浅出Java动态代理 代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位.代理 ...

  5. Java动态代理 深度详解

    代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位.代理模式从类型上来说,可以分为静态代理和动态代理两种类型. 今天我将用非常 ...

  6. [转]Java动态代理

    动态代理在Java中有着广泛的应用,比如Spring AOP,Hibernate数据查询.测试框架的后端mock.RPC,Java注解对象获取等.静态代理的代理关系在编译时就确定了,而动态代理的代理关 ...

  7. Java动态代理:一个面包店的动态代理帝国

    文章首发于[博客园-陈树义],点击跳转到原文大白话说Java动态代理:一个面包店的动态代理帝国 代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中, ...

  8. Java 动态代理是基于什么原理

    动态代理 ①动态代理概念理解 动态代理是一种方便运行时动态构建代理.动态处理代理方法调用的机制,很多场景都利用类似机制做到的,比如用来包装RPC调用.面向切面的变成(AOP) 实现动态代理的方式很多, ...

  9. JAVA动态代理的全面深层理解

    Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类.代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过 ...

随机推荐

  1. Python对象的空间边界:独善其身与开放包容

    导读:Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python.我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章.如果你是第一次看到这个系列文章,那我强烈建议,请先看看它 ...

  2. uni-app支付功能

    扫码查看原文 前言 近期一直在使用APP开发多端应用,IOS的APP.安卓的APP和H5网页,其中开发的APP使用到了微信和支付宝的支付,在此给大家分享出来,一起使用 前置条件: 开发环境:windo ...

  3. SpringBoot-03-JSR303数据校验和多环境切换

    3.3 JSR303数据校验 先看如何使用 ​ Springboot中可以用@Validated来校验数据,如果数据异常则统一抛出异常,方便异常中心统一处理. ​ 这里我们写个注解让name只支持Em ...

  4. Spring 配置文件配置事务

    一.引入事务的头文件 xmlns:tx="http://www.springframework.org/schema/tx" http://www.springframework. ...

  5. 引用类型之Array(一)

    Array类型 除了Object之外,Array类型在ECMAScript中也很常用.ECMAScript中的数组与其他多数语言中的数组有着相当大的区别.ECMAScript数组的每一项可以保存任何类 ...

  6. 064 01 Android 零基础入门 01 Java基础语法 08 Java方法 02 无参带返回值方法

    064 01 Android 零基础入门 01 Java基础语法 08 Java方法 02 无参带返回值方法 本文知识点:无参带返回值方法 说明:因为时间紧张,本人写博客过程中只是对知识点的关键步骤进 ...

  7. Java知识系统回顾整理01基础04操作符06三元运算符

    一.三元运算符 表达式?值1:值2 如果表达式为真 返回值1 如果表达式为假 返回值2 if语句学习链接:if语句 public class HelloWorld { public static vo ...

  8. C语言中最常用的标准库函数

    标准头文件包括: <asset.h>      <ctype.h>       <errno.h>       <float.h> <limits ...

  9. 099 01 Android 零基础入门 02 Java面向对象 03 综合案例(学生信息管理) 02 案例分析及实现 03 编写并测试Student类

    099 01 Android 零基础入门 02 Java面向对象 03 综合案例(学生信息管理) 02 案例分析及实现 03 编写并测试Student类 本文知识点:编写并测试Subject类 说明: ...

  10. 35岁老半路程序员的Python从0开始之路

    9年的ERP程式开发与维护,继而转向一年的售前,再到三年半的跨行业务,近4的兜兜转转又转回来做程式了,不过与之前不同的,是这次是新的程序语言Python, 同时此次是为了教学生而学习! 从今天开始,正 ...