• Java 静态代理和动态代理


    Java 静态代理

    静态代理通常用于对原有业务逻辑的扩充。比如持有二方包的某个类,并调用了其中的某些方法。然后出于某种原因,比如记录日志、打印方法执行时间,但是又不好将这些逻辑写入二方包的方法里。所以可以创建一个代理类实现和二方方法相同的方法,通过让代理类持有真实对象,然后在原代码中调用代理类方法,来达到添加我们需要业务逻辑的目的。

    这其实也就是代理模式的一种实现,通过对真实对象的封装,来实现扩展性。

    一个典型的代理模式通常有三个角色,这里称之为**代理三要素**

    共同接口

    1. public interface Action {
    2. public void doSomething();
    3. }

    真实对象

    1. public class RealObject implements Action{
    2.  
    3. public void doSomething() {
    4. System.out.println("do something");
    5. }
    6. }

    代理对象

    1. public class Proxy implements Action {
    2. private Action realObject;
    3.  
    4. public Proxy(Action realObject) {
    5. this.realObject = realObject;
    6. }
    7. public void doSomething() {
    8. System.out.println("proxy do");
    9. realObject.doSomething();
    10. }
    11. }

    运行代码

    1. Proxy proxy = new Proxy(new RealObject());
    2. proxy.doSomething();

    simple_proxy.png

    这种代理模式也最为简单,就是通过proxy持有realObject的引用,并进行一层封装。

    静态代理的优点和缺点

    先看看代理模式的优点: 扩展原功能,不侵入原代码。

    再看看这种代理模式的缺点:

    假如有这样一个需求,有十个不同的RealObject,同时我们要去代理的方法是不同的,比要代理方法:doSomething、doAnotherThing、doTwoAnotherThing,添加代理前,原代码可能是这样的:

    1. realObject.doSomething();
    2. realObject1.doAnotherThing();
    3. realObject2.doTwoAnother();

    为了解决这个问题,我们有方案一:

    为这些方法创建不同的代理类,代理后的代码是这样的:

    1. proxy.doSomething();
    2. proxy1.doAnotherThing();
    3. proxy2.doTwoAnother();

    当然,也有方案二:

    通过创建一个proxy,持有不同的realObject,实现Action1、Action2、Action3接口,来让代码变成这样:

    1. proxy.doSomething();
    2. proxy.doAnotherThing();
    3. proxy.doTwoAnother();

    于是你的代理模型会变成这样:

    dynamic_proxy.png

    毫无疑问,仅仅为了扩展同样的功能,在方案一种,我们会重复创建多个逻辑相同,仅仅RealObject引用不同的Proxy。

    而在方案二中,会导致proxy的膨胀,而且这种膨胀往往是无意义的。此外,假如方法签名是相同的,更需要在调用的时候引入额外的判断逻辑。

    java 动态代理

    搞清楚静态代理的缺点十分重要,因为动态代理的目的就是为了解决静态代理的缺点。通过使用动态代理,我们可以通过在运行时,动态生成一个持有RealObject、并实现代理接口的Proxy,同时注入我们相同的扩展逻辑。哪怕你要代理的RealObject是不同的对象,甚至代理不同的方法,都可以动过动态代理,来扩展功能。

    简单理解,动态代理就是我们上面提到的方案一,只不过这些proxy的创建都是自动的并且是在运行期生成的。

    动态代理基本用法

    使用动态代理,需要将要扩展的功能写在一个InvocationHandler 实现类里:

    1. public class DynamicProxyHandler implements InvocationHandler {
    2. private Object realObject;
    3.  
    4. public DynamicProxyHandler(Object realObject) {
    5. this.realObject = realObject;
    6. }
    7.  
    8. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    9. //代理扩展逻辑
    10. System.out.println("proxy do");
    11.  
    12. return method.invoke(realObject, args);
    13. }
    14. }

    这个Handler中的invoke方法中实现了代理类要扩展的公共功能。

    到这里,需要先看一下这个handler的用法:

    1. public static void main(String[] args) {
    2. RealObject realObject = new RealObject();
    3. Action proxy = (Action) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Action.class}, new DynamicProxyHandler(realObject));
    4. proxy.doSomething();
    5. }

    Proxy.newProxyInstance 传入的是一个ClassLoader, 一个代理接口,和我们定义的handler,返回的是一个Proxy的实例。

    仔细体会这个过程,其实有点类似我们在静态代理中提到的方案一,生成了一个包含我们扩展功能,持有RealObject引用,实现Action接口的代理实例Proxy。只不过这个Proxy不是我们自己写的,而是java帮我们生成的,有没有一点动态的味道。

    让我们再回顾一下代理三要素:真实对象:RealObject,代理接口:Action,代理实例:Proxy

    上面的代码实含义也就是,输入 RealObject、Action,返回一个Proxy。妥妥的代理模式。

    综上,动态生成+代理模式,也就是动态代理。
    网上搜了不少文章,到了这里,接下来就是和cglib等动态代理实现方法做一下横向比较。本文不做横向比较,为了不偏离主题,接下来做纵向挖掘。

    看一下源码

    道理清楚了,但是这篇文章题目是搞懂,所以来看一下这个Proxy是如何自动被生成的。入口就在newProxyInstance方法,核心代码如下:

    1. private static final Class<?>[] constructorParams =
    2. { InvocationHandler.class };
    3.  
    4. public static Object newProxyInstance(ClassLoader loader,
    5. Class<?>[] interfaces,
    6. InvocationHandler h)
    7. throws IllegalArgumentException
    8. {
    9. Class<?> cl = getProxyClass0(loader, intfs);
    10. ...
    11. final Constructor<?> cons = cl.getConstructor(constructorParams);
    12.  
    13. if (!Modifier.isPublic(cl.getModifiers())) {
    14. AccessController.doPrivileged(new PrivilegedAction<Void>() {
    15. public Void run() {
    16. cons.setAccessible(true);
    17. return null;
    18. }
    19. });
    20. }
    21. return cons.newInstance(new Object[]{h});
    22. }

    整体流程就是:

    1、生成代理类Proxy的Class对象。

    2、如果Class作用域为私有,通过 setAccessible 支持访问

    3、获取Proxy Class构造函数,创建Proxy代理实例。

    生成Proxy的Class文件

    生成Class对象的方法中,先是通过传进来的ClassLoader参数和Class[] 数组作为组成键,维护了一个对于Proxy的Class对象的缓存。这样需要相同Proxy的Class对象时,只需要创建一次。

    第一次创建该Class文件时,为了线程安全,方法进行了大量的处理,最后会来到ProxyClassFactory的apply方法中,经过以下流程:

    1、校验传入的接口是否由传入的ClassLoader加载的。

    2、校验传入是否是接口的Class对象。

    3、校验是否传入重复的接口。

    4、拼装代理类包名和类名,生成.class 文件的字节码。

    5、调用native方法,传入字节码,生成Class对象。

    1. proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
    2. long num = nextUniqueNumber.getAndIncrement();
    3. String proxyName = proxyPkg + proxyClassNamePrefix + num;
    4. byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    5. proxyName, interfaces, accessFlags);
    6. return defineClass0(loader, proxyName,
    7. proxyClassFile, 0, proxyClassFile.length);

    看一下第四步生成.class文件字节码的过程,主要分为两个阶段:

    1. addProxyMethod(hashCodeMethod, Object.class);
    2. addProxyMethod(equalsMethod, Object.class);
    3. addProxyMethod(toStringMethod, Object.class);
    4.  
    5. for (int i = 0; i < interfaces.length; i++) {
    6. Method[] methods = interfaces[i].getMethods();
    7. for (int j = 0; j < methods.length; j++) {
    8. addProxyMethod(methods[j], interfaces[i]);
    9. }
    10. }
    11. methods.add(this.generateConstructor());
    12.  
    13. for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
    14. for (ProxyMethod pm : sigmethods) {
    15. fields.add(new FieldInfo(pm.methodFieldName,
    16. "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
    17. methods.add(pm.generateMethod());
    18. }
    19. }
    20. methods.add(generateStaticInitializer());

    第一个阶段的代码比较清晰,主要就是添加各种Method,比如toString()、equals,以及传入的代理接口中的方法。再添加一下构造方法以及静态初始化方法。这要构成了一个对象,存储生成Proxy的Class的一些信息。

    到了这里,已经把要构造的Proxy的方法基本定义完成了,接下来就要生成这个.class文件了。

    1. ByteArrayOutputStream bout = new ByteArrayOutputStream();
    2. DataOutputStream dout = new DataOutputStream(bout);
    3. dout.writeInt(0xCAFEBABE);
    4. ...
    5. dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
    6. ...
    7. return bout.toByteArray();

    看到这个CAFEBABE,就清楚第二阶段的内容了。CAFEBABE是Class文件的魔数,关于Class文件这个咖啡宝贝的魔数,相信做Java的人都知道。没错,第二阶段就是生成字节码。按JVM规范,写入Class文件中包括权限控制、方法表、字段表等内容,生成符合规范的Class文件。最后返回对应的字节码。

    字节码生成以后,通过调用native方法defineClass解析字节码,就生成了Proxy的Class对象。

    Proxy构造方法

    看一下Proxy的构造方法字节码生成部分:

    1. MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);
    2. DataOutputStream out = new DataOutputStream(minfo.code);
    3. code_aload(0, out);
    4. code_aload(1, out);
    5. out.writeByte(opc_invokespecial);
    6. out.writeShort(cp.getMethodRef(superclassName,"<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
    7. ...

    关键在于,生成了一个参数为InvocationHandler的构造方法,code加载的是jvm方法区中的代码,然后通过invokespecial指令调用了父类构造方法。

    查看生成的Class文件

    上面利用字节码生成技术产生Class文件的过程,看起来可能比较晦涩,其实我们可以查看这个产生的Proxy到底是个什么样子。

    注意ProxyGenerator中有这样一个逻辑:

    1. if(saveGeneratedFiles) {
    2. ...
    3. FileOutputStream file = new FileOutputStream(dotToSlash(name) + ".class");
    4. file.write(classFile);
    5. ...
    6. }

    再看一下saveGeneratedFiles这个变量:

    1. private final static boolean saveGeneratedFiles =
    2. java.security.AccessController.doPrivileged(
    3. new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))
    4. .booleanValue();

    这是一个final类型的变量,通过GetBooleanAction方法读取系统变量,获取系统设置。默认这个值是false,稍微看一下System这个类的源码,发现有可以设置系统变量的Api,然后在程序的main 函数设置一下这个变量:

    System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    

    这个时候,再跑一遍程序,就可以看到生成的Proxy的Class文件了,直接双击利用 ide 反编译。

    1.  
    2. package com.sun.proxy;
    3.  
    4. import java.lang.reflect.InvocationHandler;
    5. import java.lang.reflect.Method;
    6. import java.lang.reflect.Proxy;
    7. import java.lang.reflect.UndeclaredThrowableException;
    8.  
    9. public final class $Proxy0 extends Proxy implements Action {
    10. private static Method m1;
    11. private static Method m3;
    12. private static Method m2;
    13. private static Method m0;
    14.  
    15. public $Proxy0(InvocationHandler var1) throws {
    16. super(var1);
    17. }
    18.  
    19.  
    20. public final void doSomething() throws {
    21. try {
    22. super.h.invoke(this, m3, (Object[])null);
    23. } catch (RuntimeException | Error var2) {
    24. throw var2;
    25. } catch (Throwable var3) {
    26. throw new UndeclaredThrowableException(var3);
    27. }
    28. }
    29.  
    30. ...
    31.  
    32. static {
    33. try {
    34. ...
    35. m3 = Class.forName("Action").getMethod("doSomething", new Class[0]);
    36. } catch (NoSuchMethodException var2) {
    37. throw new NoSuchMethodError(var2.getMessage());
    38. } catch (ClassNotFoundException var3) {
    39. throw new NoClassDefFoundError(var3.getMessage());
    40. }
    41. }
    42. }
    43.  

    省略一些无关代码,可以看到两个重要的方法。

    一个就是我们的代理方法doSomething、另一个就是构造方法。

    这个$Proxy0 继承 Proxy并调用了父类的构造方法,回忆一下上文提到的invokeSpecial,怎么样,对上了吧。

    看一下Proxy中这个构造方法:

    1. protected Proxy(InvocationHandler h) {
    2. Objects.requireNonNull(h);
    3. this.h = h;
    4. }

    在看一下$Proxy0 的代理方法:

    super.h.invoke(this, m3, (Object[])null);
    

    再来回顾一下生成Proxy实例的过程:

    1. private static final Class<?>[] constructorParams =
    2. { InvocationHandler.class };
    3. ...
    4. final Constructor<?> cons = cl.getConstructor(constructorParams);
    5. ...
    6. return cons.newInstance(new Object[]{h});

    其实newInstance生成Proxy实例时,通过$Proxy0的Class对象,选择了这个InvocationHandler为参数的构造方法,传入我们定义的InvocationHandler并生成了一个 Proxy0的实例!InvocationHandler 里有realObject的逻辑以及我们的扩展逻辑,当我们调用Proxy0的doSomething方法时,就会调用到我们InvocationHandler 里 实现的invoke方法。

    对上面这个过程,做一张图总结一下:

    flow.png

    版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/WangQYoho/article/details/77584832
    个人分类: Java
    合群是堕落的开始 优秀的开始是孤行
  • 相关阅读:
    2020.4.26 resources
    Visual Studio M_PI定义
    12.3 ROS Costmap2D代价地图源码解读_1
    Delphi GDI对象之剪切区域
    用GDI+DrawImage画上去的图片会变大
    简单的GDI+双缓冲的分析与实现
    双缓冲绘图
    C++中的成员对象
    鼠标在某个控件上按下,然后离开后弹起,如何捕获这个鼠标弹起事件
    CStatic的透明背景方法
  • 原文地址:https://www.cnblogs.com/biaogejiushibiao/p/9470420.html
Copyright © 2020-2023  润新知