资讯专栏INFORMATION COLUMN

Java动态代理实现原理(模拟实现)

K_B_Z / 2829人阅读

摘要:很多框架底层都使用了的动态代理技术来实现的,比如大名鼎鼎的这篇文章将带你一步一步揭开动态代理技术的神秘面纱。接下来客户端就可以这样使用了毫秒到目前为止,我们实现的类可以为任何接口生成代理类了,是不是很神奇。

​ 动态代理是java语言中常用的设计模式,java在1.3版本以后也提供了动态代理技术,允许开发者在运行期间创建接口的代理对象。 很多框架底层都使用了java的动态代理技术来实现的,比如大名鼎鼎的springAOP;这篇文章将带你一步一步揭开JDK动态代理技术的神秘面纱。

​ 我们先来定义一个接口:

</>复制代码

  1. package com.yanghui.study.proxy;
  2. public interface IFlyable {
  3. int fly(int x,int y);
  4. }

再来一个实现类:

</>复制代码

  1. package com.yanghui.study.proxy;
  2. public class Plane implements IFlyable{
  3. @Override
  4. public int fly(int x, int y) {
  5. int result = x * x + y * y;
  6. try {
  7. Thread.sleep(new Random().nextInt(700));
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. return result;
  12. }
  13. }

如果我们要统计一下这个fly方法的运行时间,该怎么做呢?很简单,可以修改源码在方法fly方法里面加上两句代码①、②,这样就打印出方法的运行时间了,如下:

</>复制代码

  1. //省略不必要代码......
  2. public int fly(int x, int y) {
  3. long start = System.currentTimeMillis();//①记录开始时间
  4. int result = x * x + y * y;
  5. try {
  6. Thread.sleep(new Random().nextInt(700));
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②结束时间减去开始时间
  11. return result;
  12. }

但是如果我们没有这个方法的源码,这个类是别人写好打好jar包提供给我们用的,这时如果你还想统计下这个方法运行时间,又该怎么办呢?至少有两种方式可以来实现:

1、使用继承,写一个类继承Plane,重写fly方法,在调用父类的fly方法前后加上①②处的代码,这样就可以统计fly方法的执行时间了。

</>复制代码

  1. package com.yanghui.study.proxy;
  2. public class PlaneTimerProxy1 extends Plane{
  3. @Override
  4. public int fly(int x, int y) {
  5. long start = System.currentTimeMillis();//①
  6. int result = super.fly(x, y);
  7. System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
  8. return result;
  9. }
  10. }

2、使用聚合的方式,写一个类PlaneTimerProxy2实现跟Plane一样的接口,并且持有IFlyable的引用,当调用fly方法时,实际调用的是IFlyable的fly方法,这样就可以在方法调用前后加上①②处的代码统计fly方法的执行的时间。

</>复制代码

  1. public class PlaneTimerProxy2 implements IFlyable{
  2. private IFlyable flyable;
  3. public PlaneTimerProxy2(IFlyable flyable) {
  4. this.flyable = flyable;
  5. }
  6. @Override
  7. public int fly(int x, int y) {
  8. long start = System.currentTimeMillis();//①
  9. int result = this.flyable.fly(x, y);
  10. System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
  11. return result;
  12. }
  13. }

这两种方式都可以实现,那么哪种方式更好呢?答案是聚合的方式更好,为什么呢?想象一下,如果我还想实现更多的功能,比如给fly方法执行前后加上日志,事务控制,权限控制,这时用继承的方式你会需要新建更多的类来实现,可能你会想,聚合的实现方式不也是要新建更多的类来实现吗?是的,但是如果我要你先记录日志再记录时间,有如果我要你先记录时间再记录日志,需要实现这样随意的组合的功能,继承就显得很麻烦了,而聚合的方式就会很灵活了。在思考下,如果想给不同类的100个方法记录下时间和日志,那么你想想看是不是要产生100个代理类呢?类的数量又在不停的膨胀了。如果我们能够为实现了某个接口的类动态生成代理类就好了?想法很好,先来新建一个类Proxy,提供一个方法newProxyInstance,这个方法可以为一个实现了IFlyable接口的类产生代理类,那么客户端调用就可以这样做:

</>复制代码

  1. package com.yanghui.study.proxy.custom;
  2. public class Client {
  3. public static void main(String[] args) {
  4. IFlyable flyable = (IFlyable)Proxy.newProxyInstance();
  5. flyable.fly(1, 2);
  6. }
  7. }

那么我们如何在newProxyInstance方法里面动态的生成一个代理类呢?为了模拟JDK的实现,先定义一个接口InvocationHandler:

</>复制代码

  1. package com.yanghui.study.proxy.custom;
  2. import java.lang.reflect.Method;
  3. public interface InvocationHandler {
  4. Object invoke(Object proxy,Method method,Object[] args)throws Throwable;
  5. }

下面来个完整代码:

</>复制代码

  1. public class Proxy {
  2. private static final Map bytesMap = new HashMap<>();
  3. private static final AtomicInteger count = new AtomicInteger();
  4. public static Object newProxyInstance(Class intaface,InvocationHandler handler) {
  5. //代码①处
  6. String rn = "
  7. ";
  8. String className = "Proxy" + count.getAndIncrement();
  9. String str = "package com.yanghui.study.proxy.custom;" + rn +
  10. "public class " + className + " implements " + intaface.getName() + "{" + rn +
  11. " private InvocationHandler handler;" + rn +
  12. " public " + className + "(InvocationHandler handler){" + rn +
  13. " this.handler=handler;" + rn +
  14. " }" + rn;
  15. String methodStr = "";
  16. for(Method m : intaface.getMethods()) {
  17. methodStr = methodStr + " @Override" + rn +
  18. " public " + m.getReturnType().getName() + " " + m.getName() + "(";
  19. String parameterStr = "";
  20. String psType = "";
  21. String pname = "";
  22. for(Parameter p : m.getParameters()) {
  23. parameterStr = parameterStr + p + ",";
  24. psType = psType + p.getType().getName() + ".class,";
  25. pname = pname + p.getName() + ",";
  26. }
  27. if(!parameterStr.equals("")) {
  28. parameterStr = parameterStr.substring(0, parameterStr.length() - 1);
  29. }
  30. parameterStr = parameterStr + "){" + rn +
  31. " try{" + rn +
  32. " " + Method.class.getName() + " method = " + intaface.getName() + ".class.getDeclaredMethod("" + m.getName() + """;
  33. if(!psType.equals("")) {
  34. psType = psType.substring(0, psType.length() - 1);
  35. parameterStr = parameterStr + "," + psType + ");" + rn;
  36. }else {
  37. parameterStr = parameterStr + ");" + rn;
  38. }
  39. if(pname.length() > 0) {
  40. pname = pname.substring(0, pname.length() - 1);
  41. }
  42. String returnStr = "";
  43. if(!"void".equals(m.getReturnType().getName())) {
  44. returnStr = returnStr + " return (" + m.getReturnType().getName() + ")";
  45. }
  46. parameterStr = parameterStr +
  47. returnStr + "this.handler.invoke(this,method," + (pname.length() == 0 ? "null" : "new Object[]{" + pname + "}") + ");" + rn +
  48. " } catch (Throwable e) {" + rn +
  49. " throw new RuntimeException(e);" + rn +
  50. " }" + rn +
  51. " }" + rn;
  52. methodStr = methodStr + parameterStr;
  53. }
  54. String endStr = "}";
  55. str = str + methodStr + endStr;
  56. String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/yanghui/study/proxy/custom/";
  57. String fileStr = path + className + ".java";
  58. //代码②处
  59. //写入文件
  60. writeToFile(fileStr, str);
  61. //代码③处
  62. //动态编译
  63. String className1 = "com.yanghui.study.proxy.custom." + className;
  64. return compileToFileAndLoadclass(className1, fileStr, handler);
  65. }
  66. /**
  67. * 从源文件到字节码文件的编译方式
  68. * @param className
  69. * @param fileStr
  70. * @param handler
  71. * @return
  72. */
  73. private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) {
  74. //获取系统Java编译器
  75. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  76. //获取Java文件管理器
  77. StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
  78. //定义要编译的源文件
  79. File file = new File(fileStr);
  80. //通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元
  81. Iterable compilationUnits = fileManager.getJavaFileObjects(file);
  82. //生成编译任务
  83. JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
  84. //执行编译任务
  85. task.call();
  86. try {
  87. fileManager.close();
  88. } catch (IOException e) {
  89. e.printStackTrace();
  90. }
  91. try {
  92. Class c = Thread.currentThread().getContextClassLoader().loadClass(className);
  93. Constructor ct = c.getConstructor(InvocationHandler.class);
  94. Object object = ct.newInstance(handler);
  95. return object;
  96. } catch (Exception e) {
  97. throw new RuntimeException(e);
  98. }
  99. }
  100. private static void writeToFile(String file,String context) {
  101. FileWriter fw = null;
  102. try {
  103. fw = new FileWriter(new File(file));
  104. fw.write(context);
  105. } catch (IOException e) {
  106. e.printStackTrace();
  107. }finally {
  108. if(fw != null) {
  109. try {
  110. fw.close();
  111. } catch (IOException e) {
  112. e.printStackTrace();
  113. }
  114. }
  115. }
  116. }
  117. }

我来解释下上面代码的意思:

1、代码①处,根据传入的接口动态生成java代码的字符串,类名取名为Proxy+序号,该类实现了传入的接口,真正的方法调用将委托传入InvocationHandler的实现类来实现。

2、代码②处,将生成的java代码的字符串写入文件

3、代码③处,真正的核心,动态编译2步生成的java文件,再通过classLoader把编译生成的class文件加载进内存,然后反射创建实例。

接下来客户端就可以这样使用了:

</>复制代码

  1. public class Client {
  2. public static void main(String[] args) {
  3. Plane plane = new Plane();
  4. IFlyable flyable = (IFlyable)Proxy.newProxyInstance(IFlyable.class,new InvocationHandler() {
  5. @Override
  6. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  7. long start = System.currentTimeMillis();
  8. Object result = method.invoke(plane, args);
  9. System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");
  10. return result;
  11. }
  12. });
  13. System.out.println(flyable.fly(1, 2));
  14. }
  15. }

到目前为止,我们实现的Proxy类可以为任何接口生成代理类了,是不是很神奇。当然我们这里只是模拟实现了JDk的动态代理,还有很多细节是没有考虑的,有兴趣的同学可以自己阅读JDK源码,相信您理解了其背后的原理后,看起来也不会太费力了。

扩展

在上面我们实现了动态生成java文件,动态编译java文件,需要把文件写入磁盘,也会在java源文件的目录生成编译后的.class文件,那么可以不可以只在内存中编译加载呢?答案是可以的,代码如下(方法是Proxy类下的方法):

</>复制代码

  1. /**
  2. * 从内存到内存的编译方式
  3. * @param className
  4. * @param code
  5. * @param handler
  6. * @return
  7. */
  8. @SuppressWarnings({ "unchecked", "rawtypes" })
  9. private static Object compileMemoryToMemoryAndLoadClass(String className,String code,InvocationHandler handler) {
  10. if(bytesMap.get(className) != null) {
  11. return loadClass(className, bytesMap.get(className), handler);
  12. }
  13. //获取系统Java编译器
  14. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  15. //获取Java文件管理器
  16. StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
  17. ForwardingJavaFileManager fjf = new ForwardingJavaFileManager(fileManager) {
  18. @Override
  19. public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
  20. FileObject sibling) throws IOException {
  21. if(kind == JavaFileObject.Kind.CLASS) {
  22. return new SimpleJavaFileObject(URI.create(""), JavaFileObject.Kind.CLASS) {
  23. public OutputStream openOutputStream() {
  24. return new FilterOutputStream(new ByteArrayOutputStream()) {
  25. public void close() throws IOException{
  26. out.close();
  27. ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
  28. bytesMap.put(className, bos.toByteArray());
  29. }
  30. };
  31. }
  32. };
  33. }else{
  34. return super.getJavaFileForOutput(location, className, kind, sibling);
  35. }
  36. }
  37. };
  38. SimpleJavaFileObject sourceJavaFileObject = new SimpleJavaFileObject(URI.create(className.replace(".", "/") + Kind.SOURCE.extension),JavaFileObject.Kind.SOURCE){
  39. @Override
  40. public CharBuffer getCharContent(boolean b) {
  41. return CharBuffer.wrap(code);
  42. }
  43. };
  44. //生成编译任务
  45. JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject}));
  46. //执行编译任务
  47. task.call();
  48. try {
  49. fileManager.close();
  50. fjf.close();
  51. } catch (IOException e) {
  52. e.printStackTrace();
  53. }
  54. return loadClass(className, bytesMap.get(className), handler);
  55. }
  56. private static Object loadClass(String className,byte[] bytes,InvocationHandler handler) {
  57. try {
  58. Class c = new MyClassLoader(bytes).loadClass(className);
  59. Constructor ct = c.getConstructor(InvocationHandler.class);
  60. Object object = ct.newInstance(handler);
  61. return object;
  62. } catch (Exception e) {
  63. throw new RuntimeException(e);
  64. }
  65. }

首先通过自己定义sourceJavaFileObject类来加载java格式的字符串,通过ForwardingJavaFileManager类来重新定义编译文件的输出行为,这里我直接写入内存,用一个map(bytesMap)来保存,key就是类名,value就是编译好的.class的二进制文件。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71942.html

相关文章

  • Spring AOP的实现原理

    摘要:使用与的静态代理不同,使用的动态代理,所谓的动态代理就是说框架不会去修改字节码,而是在内存中临时为方法生成一个对象,这个对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存...

    ephererid 评论0 收藏0
  • 动态代理模式实现原理

    摘要:代理模式概念代理模式分为两种,一种是静态代理模式,一种是动态代理模式。面向切面的编程也是使用动态代理模式来实现的。 1.代理模式概念 代理模式分为两种,一种是静态代理模式,一种是动态代理模式。 静态代理模式:在程序运行之前需要写好代理类 动态代理模式:在程序运行期间动态生成代理类 2.动态代理的实现 动态代理实现的步骤: (1)写一个代理类SubjectHandler实现Invoca...

    songjz 评论0 收藏0
  • Java反射-动态代理

    摘要:动态代理有多种不同的用途,例如,数据库连接和事务管理用于单元测试的动态模拟对象其他类似的方法拦截。调用序列和下面的流程类似单元测试动态对象模拟利用动态代理实现单元测试的动态存根代理和代理。框架把包装成动态代理。 使用反射可以在运行时动态实现接口。这可以使用类java.lang.reflect.Proxy。这个类的名称是我将这些动态接口实现称之为动态代理的原因。动态代理有多种不同的用途,...

    Acceml 评论0 收藏0
  • Java动态追踪技术探究

    摘要:对于人类来说,字节码文件的可读性远远没有代码高。尽管如此,还是有一些杰出的程序员们创造出了可以用来直接编辑字节码的框架,提供接口可以让我们方便地操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。 引子 在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话: Debug一下吧。 线上机器,没开Debu...

    BlackFlagBin 评论0 收藏0

发表评论

0条评论

K_B_Z

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<