0x01 前言

首先动态代理也是一种设计模式而已
动态代理与静态代理从功能与目的来说是没有区别的,唯一有区别的就是动态与静态的差别

这个差别如下:

  1. 动态代理是指代理对象在程序运行时才进行创建的代理方式
  2. 需要通过反射实现,借助Java自带的java.lang.reflect.ProxyinvocationHandler接口
  3. 这种情况下,代理对象并不是在Java代码中直接定义死的,而是在运行时根据“指示”动态生产的

对比静态代理,动态代理最大的优势是在于可以很方便的对代理对象的各个方法进行统一的处理,而不用去修改每个代理对象的方法

0x02 环境

  1. 编辑器为: IntelliJ IDEA
  2. java版本:
  3. java version "1.7.0_80"
  4. Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
  5. Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
  6. 代码仓库: https://github.com/pmiaowu/JavaProxyTest

0x03 语法介绍

0x03.1 invocationHandler接口

InvocationHandler是一个接口,按照官方文档的解释说
每个代理的实例都有一个与之关联的InvocationHandler实现类,如果代理的方法被调用,那么代理便会通知与转发给内部的InvocationHandler实现类,由它进行处理

  1. // 该接口里只有一个方法invoke,这个方法非常重要,先记下来
  2. public interface InvocationHandler {
  3. /**
  4. * @param proxy 被代理的对象
  5. * @param method 要调用的方法
  6. * @param args 方法调用时所需要参数
  7. * @return
  8. * @throws Throwable
  9. */
  10. public Object invoke(Object proxy, Method method, Object[] args)
  11. throws Throwable;
  12. }

0x03.2 java.lang.reflect.Proxy包

  1. // Proxy动态产生的代理会调用InvocationHandler实现类,因此InvocationHandler是实际执行者
  2. public class Proxy implements java.io.Serializable {
  3. ...
  4. /**
  5. * @param loader 代理对象的类加载器
  6. * @param interfaces 代理类全部的接口
  7. * @param h 实现InvocationHandler接口的对象
  8. * @return 返回代理对象生成的类实例
  9. * @throws IllegalArgumentException
  10. */
  11. @CallerSensitive
  12. public static Object newProxyInstance(ClassLoader loader,
  13. Class<?>[] interfaces,
  14. InvocationHandler h)
  15. throws IllegalArgumentException
  16. {
  17. if (h == null) {
  18. throw new NullPointerException();
  19. }
  20. final Class<?>[] intfs = interfaces.clone();
  21. final SecurityManager sm = System.getSecurityManager();
  22. if (sm != null) {
  23. checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
  24. }
  25. /*
  26. * Look up or generate the designated proxy class.
  27. */
  28. Class<?> cl = getProxyClass0(loader, intfs);
  29. /*
  30. * Invoke its constructor with the designated invocation handler.
  31. */
  32. try {
  33. final Constructor<?> cons = cl.getConstructor(constructorParams);
  34. final InvocationHandler ih = h;
  35. if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
  36. // create proxy instance with doPrivilege as the proxy class may
  37. // implement non-public interfaces that requires a special permission
  38. return AccessController.doPrivileged(new PrivilegedAction<Object>() {
  39. public Object run() {
  40. return newInstance(cons, ih);
  41. }
  42. });
  43. } else {
  44. return newInstance(cons, ih);
  45. }
  46. } catch (NoSuchMethodException e) {
  47. throw new InternalError(e.toString());
  48. }
  49. }
  50. ...
  51. }

0x04 例子

0x04.1 案例介绍

例如我们去影院看电影:
我们平常去电影院看电影时,在电影开始与结束阶段是不是都会放广告呢?
电影是电影公司授权给影院进行播放与售卖的
那么电影公司就是目标对象,他只管和影院根据票房分钱,剩下的事情就不关它事了
而影院就是代理对象,负责其他的事情
在例如影院可以在播放电影的前后,产生一些除了电影票之外的额外收益
比如在影片开始与结束时播发一些广告
或是去洗手间的时候播发一些广告

现在我们使用代码来进行模拟

0x04.2 电影的播放(代码模拟)

首先开始创建接口,通用的接口是代理模式实现的基础
这个接口就命名为Movie,里面有个play方法,表示电影播放的能力

  1. package 动态代理;
  2. public interface Movie {
  3. void play();
  4. }

电影公司比较好授权给影院两部电影
接着创建个实现这个Movie接口的两个类,这两个类表示电影公司授权给影院的电影

  1. package 动态代理;
  2. public class RealMovie1 implements Movie {
  3. @Override
  4. public void play() {
  5. System.out.println("您正在观看电影《嗨客帝国》");
  6. }
  7. }
  1. package 动态代理;
  2. public class RealMovie2 implements Movie {
  3. @Override
  4. public void play() {
  5. System.out.println("您正在观看电影《花园宝宝历险记》");
  6. }
  7. }

0x04.3 上厕所的操作(代码模拟)

这个接口就命名为Toilet,里面有个go方法,表示可以走进厕所的动作

  1. package 动态代理;
  2. public interface Toilet {
  3. void go();
  4. }

影院比较好,建立了一个厕所
接着创建个实现这个Toilet接口的类,这个类表示影院给用户的基础设施

  1. package 动态代理;
  2. public class RealToilet implements Toilet {
  3. @Override
  4. public void go() {
  5. System.out.println("您冲进了洗手间");
  6. }
  7. }

0x04.4 影院(代码模拟)

然后创建个Cinema类,这个类表示影院要播放的电影,还有一些额外的操作
这个Cinema类就是个代理对象了

  1. package 动态代理;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Method;
  4. import java.lang.reflect.Proxy;
  5. public class Cinema implements InvocationHandler {
  6. Object target;
  7. public Object bind(Object target) {
  8. this.target = target;
  9. return Proxy.newProxyInstance(
  10. target.getClass().getClassLoader(),
  11. target.getClass().getInterfaces(),
  12. this);
  13. }
  14. @Override
  15. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  16. System.out.println(" ");
  17. System.out.println("------------------");
  18. System.out.println("该影院叫: " + this.getClass().getSimpleName());
  19. this.advertising1();
  20. Object result = method.invoke(this.target, args);
  21. this.advertising2();
  22. System.out.println("------------------");
  23. return result;
  24. }
  25. public void advertising1() {
  26. System.out.println("开始广告: 电影准备开始了,肯德基十翅一桶,只需要39元,快来买啊!");
  27. }
  28. public void advertising2() {
  29. System.out.println("结束广告: 电影结束了,肯德基十翅一桶,只需要39元,快买回家吃吧!");
  30. }
  31. }

0x04.5 运行测试代码

运行看看结果

  1. package 动态代理;
  2. import sun.misc.ProxyGenerator;
  3. import java.io.FileOutputStream;
  4. public class Test {
  5. public static void main(String[] args) {
  6. Cinema cinema = new Cinema();
  7. // 观看电影《嗨客帝国》
  8. RealMovie1 realMovie1 = new RealMovie1();
  9. Movie movieProxy1 = (Movie) cinema.bind(realMovie1);
  10. movieProxy1.play();
  11. // 观看电影《花园宝宝历险记》
  12. RealMovie2 realMovie2 = new RealMovie2();
  13. Movie movieProxy2 = (Movie) cinema.bind(realMovie2);
  14. movieProxy2.play();
  15. // 冲进洗手间
  16. RealToilet realToilet = new RealToilet();
  17. Toilet toiletProxy = (Toilet) cinema.bind(realToilet);
  18. toiletProxy.go();
  19. System.out.println(" ");
  20. System.out.println("------------------");
  21. System.out.println("动态代理类名为: " + movieProxy1.getClass().getName());
  22. System.out.println("动态代理类名为: " + movieProxy2.getClass().getName());
  23. System.out.println("动态代理类名为: " + toiletProxy.getClass().getName());
  24. System.out.println("------------------");
  25. // 将代理类反编译到文件中
  26. byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", RealMovie1.class.getInterfaces());
  27. // Path: 为反编译后保存的文件路径
  28. String path = "./src/main/java/动态代理/$Proxy0.class";
  29. try {
  30. FileOutputStream fos = new FileOutputStream(path);
  31. fos.write(classFile);
  32. fos.flush();
  33. System.out.println(" ");
  34. System.out.println("------------------");
  35. System.out.println("动态代理类写入成功");
  36. System.out.println("路径: " + path);
  37. System.out.println("------------------");
  38. } catch (Exception e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }
  43. // 执行结果
  44. ------------------
  45. 该影院叫: Cinema
  46. 开始广告: 电影准备开始了,肯德基十翅一桶,只需要39元,快来买啊!
  47. 您正在观看电影《嗨客帝国》
  48. 结束广告: 电影结束了,肯德基十翅一桶,只需要39元,快买回家吃吧!
  49. ------------------
  50. ------------------
  51. 该影院叫: Cinema
  52. 开始广告: 电影准备开始了,肯德基十翅一桶,只需要39元,快来买啊!
  53. 您正在观看电影《花园宝宝历险记》
  54. 结束广告: 电影结束了,肯德基十翅一桶,只需要39元,快买回家吃吧!
  55. ------------------
  56. ------------------
  57. 该影院叫: Cinema
  58. 开始广告: 电影准备开始了,肯德基十翅一桶,只需要39元,快来买啊!
  59. 您冲进了洗手间
  60. 结束广告: 电影结束了,肯德基十翅一桶,只需要39元,快买回家吃吧!
  61. ------------------
  62. ------------------
  63. 动态代理类名为: com.sun.proxy.$Proxy0
  64. 动态代理类名为: com.sun.proxy.$Proxy0
  65. 动态代理类名为: com.sun.proxy.$Proxy1
  66. ------------------
  67. ------------------
  68. 动态代理类写入成功
  69. 路径: ./src/main/java/动态代理/$Proxy0.class
  70. ------------------

0x05 自动调用invoke方法的原理

就用下面提供的示例代码作为分析的入口

  1. package 动态代理;
  2. import sun.misc.ProxyGenerator;
  3. import java.io.FileOutputStream;
  4. public class Test {
  5. public static void main(String[] args) {
  6. Cinema cinema = new Cinema();
  7. // 观看电影《嗨客帝国》
  8. RealMovie1 realMovie1 = new RealMovie1();
  9. Movie movieProxy1 = (Movie) cinema.bind(realMovie1);
  10. movieProxy1.play();
  11. // 将代理类反编译到文件中
  12. byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", RealMovie1.class.getInterfaces());
  13. // Path: 为反编译后保存的文件路径
  14. String path = "./src/main/java/动态代理/$Proxy0.class";
  15. try {
  16. FileOutputStream fos = new FileOutputStream(path);
  17. fos.write(classFile);
  18. fos.flush();
  19. System.out.println(" ");
  20. System.out.println("------------------");
  21. System.out.println("动态代理类写入成功");
  22. System.out.println("路径: " + path);
  23. System.out.println("------------------");
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }

让我们在自问自答中了解自动调用invoke方法的原理

第一个问题,我们通过java.lang.reflect.ProxynewProxyInstance方法
生成的Movie movieProxy1对象,是Cinema类的实例化的吗?

第一个问题答案:
�答案很简单当然不是,可以打个debug查看
image.png
通过debug,可以很清楚的看到movieProxy1的类是$Proxy0

第二个问题,查看一下$Proxy0(生成的代理对象)干了什么
要想知道这个可以打开我们前面测试代码保存的$Proxy0.class文件
通过IDEA打开$Proxy0.class如下:

  1. //
  2. // Source code recreated from a .class file by IntelliJ IDEA
  3. // (powered by Fernflower decompiler)
  4. //
  5. import java.lang.reflect.InvocationHandler;
  6. import java.lang.reflect.Method;
  7. import java.lang.reflect.Proxy;
  8. import java.lang.reflect.UndeclaredThrowableException;
  9. import 动态代理.Movie;
  10. public final class $Proxy0 extends Proxy implements Movie {
  11. private static Method m1;
  12. private static Method m0;
  13. private static Method m3;
  14. private static Method m2;
  15. public $Proxy0(InvocationHandler var1) throws {
  16. super(var1);
  17. }
  18. public final boolean equals(Object var1) throws {
  19. try {
  20. return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
  21. } catch (RuntimeException | Error var3) {
  22. throw var3;
  23. } catch (Throwable var4) {
  24. throw new UndeclaredThrowableException(var4);
  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 play() throws {
  37. try {
  38. super.h.invoke(this, m3, (Object[])null);
  39. } catch (RuntimeException | Error var2) {
  40. throw var2;
  41. } catch (Throwable var3) {
  42. throw new UndeclaredThrowableException(var3);
  43. }
  44. }
  45. public final String toString() throws {
  46. try {
  47. return (String)super.h.invoke(this, m2, (Object[])null);
  48. } catch (RuntimeException | Error var2) {
  49. throw var2;
  50. } catch (Throwable var3) {
  51. throw new UndeclaredThrowableException(var3);
  52. }
  53. }
  54. static {
  55. try {
  56. m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
  57. m0 = Class.forName("java.lang.Object").getMethod("hashCode");
  58. m3 = Class.forName("动态代理.Movie").getMethod("play");
  59. m2 = Class.forName("java.lang.Object").getMethod("toString");
  60. } catch (NoSuchMethodException var2) {
  61. throw new NoSuchMethodError(var2.getMessage());
  62. } catch (ClassNotFoundException var3) {
  63. throw new NoClassDefFoundError(var3.getMessage());
  64. }
  65. }
  66. }

通过查看$Proxy0.class源码,就清楚许多了
主要查看上面$Proxy0.class源码的play()方法,里面的h = Cinema类,简单把

以上面的代码为例,整个动态代理的执行流程如下:
Proxy.newProxyInstance生成代理类$Proxy0并实例化,赋值为movieProxy1返回
在执行movieProxy1.play()
先执行$Proxy0.play()
在执行Cinema.invoke()
最后面在执行realMovie1.play()结束整个流程

0x06 总结

通过这个简单的案例可以得出动态代理的好处

  1. Cinema类的代码量被固定下来,不会因为业务的逐渐庞大而庞大
  2. 可以实现AOP编程
  3. 解耦,例如在web业务中,可以实现数据层和业务层的分离

对于黑客们来说,一定要知道以下几点

  1. java.lang.reflect.Proxy,实现了java.io.Serializable接口,所以被代理的类会支持序列化/反序列化
  2. 当某个对象(Cinema)用java.lang.reflect.Proxy进行代理的时候会变成代理对象(cinema.bind())
  3. 代理对象(cinema.bind())执行任何方法(movieProxy1.play())都会调用该对象(Cinema)的invoke方法
  4. 代理对象(cinema.bind())会被java.lang.reflect.Proxy重写java.lang.Object类的equals()toString()hashCode()方法
  5. 如果动态代理生成了多个动态代理类,新生成的类名中的0会自增,com.sun.proxy.``$Proxy0/$Proxy1/$Proxy2

黑客们必须知道上面提到的几点,脑子记得就行,不用太深入原理,后面学习反序列化漏洞时,需要使用到!!!!