http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments

http://www.ibm.com/developerworks/cn/java/j-lo-proxy2/

引言

Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这是一套非常灵活有弹性的代理框架。通过阅读本文,读者将会对 Java 动态代理机制有更加深入的理解。本文首先从 Java 动态代理的运行机制和特点出发,对其代码进行了分析,推演了动态生成类的内部实现。

回页首

代理:设计模式

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

图 1. 代理模式

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

回页首

相关的类和接口

要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口:

  • java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。

    清单 1. Proxy 的静态方法
    // 方法 1: 该方法用于获取指定代理对象所关联的调用处理器
    static InvocationHandler getInvocationHandler(Object proxy) // 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
    static Class getProxyClass(ClassLoader loader, Class[] interfaces) // 方法 3:该方法用于判断指定类对象是否是一个动态代理类
    static boolean isProxyClass(Class cl) // 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
    static Object newProxyInstance(ClassLoader loader, Class[] interfaces,
    InvocationHandler h)
  • java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
    清单 2. InvocationHandler 的核心方法
    // 该方法负责集中处理动态代理类上的所有方法调用。第一个参数既是代理类实例,第二个参数是被调用的方法对象
    // 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行
    Object invoke(Object proxy, Method method, Object[] args)

    每次生成动态代理类对象时都需要指定一个实现了该接口的调用处理器对象(参见 Proxy 静态方法 4 的第三个参数)。

  • java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。

    每次生成动态代理类对象时都需要指定一个类装载器对象(参见 Proxy 静态方法 4 的第一个参数)

回页首

代理机制及其特点

首先让我们来了解一下如何使用 Java 动态代理。具体有如下四步骤:

  1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
  2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
  3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
清单 3. 动态代理对象创建过程
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
// 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); // 通过反射从生成的类对象获得构造函数对象
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); // 通过构造函数对象创建动态代理类实例
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

实际使用过程更加简单,因为 Proxy 的静态方法 newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下

清单 4. 简化的动态代理对象创建过程
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 直接创建动态代理类实例
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,
new Class[] { Interface.class },
handler );

接下来让我们来了解一下 Java 动态代理机制的一些特点。

首先是动态生成的代理类本身的一些特点。1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.ibm.developerworks 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。4)类继承关系:该类的继承关系如图:

图 2. 动态代理类的继承图

由图可见,Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。

接下来让我们了解一下代理类实例的一些特点。每个实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法 getInvocationHandler 去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,值得注意的是,代理类的根类 java.lang.Object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString,可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

接着来了解一下被代理的一组接口有哪些特点。首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过 65535,这是 JVM 设定的限制。

最后再来了解一下异常处理方面的特点。从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 Throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 Throwable 异常。那么如果在 invoke 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java 动态代理类已经为我们设计好了解决方法:它将会抛出 UndeclaredThrowableException 异常。这个异常是一个 RuntimeException 类型,所以不会引起编译错误。通过该异常的 getCause 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。

回页首

代码是最好的老师

机制和特点都介绍过了,接下来让我们通过源代码来了解一下 Proxy 到底是如何实现的。

首先记住 Proxy 的几个重要的静态变量:

清单 5. Proxy 的重要静态变量
// 映射表:用于维护类装载器对象到其对应的代理类缓存
private static Map loaderToCache = new WeakHashMap(); // 标记:用于标记一个动态代理类正在被创建中
private static Object pendingGenerationMarker = new Object(); // 同步表:记录已经被创建的动态代理类类型,主要被方法 isProxyClass 进行相关的判断
private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); // 关联的调用处理器引用
protected InvocationHandler h;

然后,来看一下 Proxy 的构造方法:

清单 6. Proxy 构造方法
// 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用
private Proxy() {} // 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用
protected Proxy(InvocationHandler h) {this.h = h;}

接着,可以快速浏览一下 newProxyInstance 方法,因为其相当简单:

清单 7. Proxy 静态方法 newProxyInstance
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException { // 检查 h 不为空,否则抛异常
if (h == null) {
throw new NullPointerException();
} // 获得与制定类装载器和一组接口相关的代理类类型对象
Class cl = getProxyClass(loader, interfaces); // 通过反射获取构造函数对象并生成代理类实例
try {
Constructor cons = cl.getConstructor(constructorParams);
return (Object) cons.newInstance(new Object[] { h });
} catch (NoSuchMethodException e) { throw new InternalError(e.toString());
} catch (IllegalAccessException e) { throw new InternalError(e.toString());
} catch (InstantiationException e) { throw new InternalError(e.toString());
} catch (InvocationTargetException e) { throw new InternalError(e.toString());
}
}

由此可见,动态代理真正的关键是在 getProxyClass 方法,该方法负责为一组接口动态地生成代理类类型对象。在该方法内部,您将能看到 Proxy 内的各路英雄(静态变量)悉数登场。有点迫不及待了么?那就让我们一起走进 Proxy 最最神秘的殿堂去欣赏一番吧。该方法总共可以分为四个步骤:

  1. 对这组接口进行一定程度的安全检查,包括检查接口类对象是否对类装载器可见并且与类装载器所能识别的接口类对象是完全相同的,还会检查确保是 interface 类型而不是 class 类型。这个步骤通过一个循环来完成,检查通过后将会得到一个包含所有接口名称的字符串数组,记为 String[] interfaceNames。总体上这部分实现比较直观,所以略去大部分代码,仅保留留如何判断某类或接口是否对特定类装载器可见的相关代码。

    清单 8. 通过 Class.forName 方法判接口的可见性
    try {
    // 指定接口名字、类装载器对象,同时制定 initializeBoolean 为 false 表示无须初始化类
    // 如果方法返回正常这表示可见,否则会抛出 ClassNotFoundException 异常表示不可见
    interfaceClass = Class.forName(interfaceName, false, loader);
    } catch (ClassNotFoundException e) {
    }
  2. 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表,如果不存在就创建一个新的缓存表并更新到 loaderToCache。缓存表是一个 HashMap 实例,正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。当代理类正在被创建时它会临时保存(接口名字列表,pendingGenerationMarker)。标记 pendingGenerationMarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)代理类正在被创建,请保持等待直至创建完成。
    清单 9. 缓存表的使用
    do {
    // 以接口名字列表作为关键字获得对应 cache 值
    Object value = cache.get(key);
    if (value instanceof Reference) {
    proxyClass = (Class) ((Reference) value).get();
    }
    if (proxyClass != null) {
    // 如果已经创建,直接返回
    return proxyClass;
    } else if (value == pendingGenerationMarker) {
    // 代理类正在被创建,保持等待
    try {
    cache.wait();
    } catch (InterruptedException e) {
    }
    // 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待
    continue;
    } else {
    // 标记代理类正在被创建
    cache.put(key, pendingGenerationMarker);
    // break 跳出循环已进入创建过程
    break;
    } while (true);
  3. 动态创建代理类的类对象。首先是确定代理类所在的包,其原则如前所述,如果都为 public 接口,则包名为空字符串表示顶层包;如果所有非 public 接口都在同一个包,则包名与这些接口的包名相同;如果有多个非 public 接口且不同包,则抛异常终止代理类的生成。确定了包后,就开始生成代理类的类名,同样如前所述按格式“$ProxyN”生成。类名也确定了,接下来就是见证奇迹的发生 —— 动态生成代理类:
    清单 10. 动态生成代理类
    // 动态地生成代理类的字节码数组
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces);
    try {
    // 动态地定义新生成的代理类
    proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0,
    proxyClassFile.length);
    } catch (ClassFormatError e) {
    throw new IllegalArgumentException(e.toString());
    } // 把生成的代理类的类对象记录进 proxyClasses 表
    proxyClasses.put(proxyClass, null);

    由此可见,所有的代码生成的工作都由神秘的 ProxyGenerator 所完成了,当你尝试去探索这个类时,你所能获得的信息仅仅是它位于并未公开的 sun.misc 包,有若干常量、变量和方法以完成这个神奇的代码生成的过程,但是 sun 并没有提供源代码以供研读。至于动态类的定义,则由 Proxy 的 native 静态方法 defineClass0 执行。

  4. 代码生成过程进入结尾部分,根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。

走完了以上四个步骤后,至此,所有的代理类生成细节都已介绍完毕,剩下的静态方法如 getInvocationHandler 和 isProxyClass 就显得如此的直观,只需通过查询相关变量就可以完成,所以对其的代码分析就省略了。

回页首

代理类实现推演

分析了 Proxy 类的源代码,相信在读者的脑海中会对 Java 动态代理机制形成一个更加清晰的理解,但是,当探索之旅在 sun.misc.ProxyGenerator 类处嘎然而止,所有的神秘都汇聚于此时,相信不少读者也会对这个 ProxyGenerator 类产生有类似的疑惑:它到底做了什么呢?它是如何生成动态代理类的代码的呢?诚然,这里也无法给出确切的答案。还是让我们带着这些疑惑,一起开始探索之旅吧。

事物往往不像其看起来的复杂,需要的是我们能够化繁为简,这样也许就能有更多拨云见日的机会。抛开所有想象中的未知而复杂的神秘因素,如果让我们用最简单的方法去实现一个代理类,唯一的要求是同样结合调用处理器实施方法的分派转发,您的第一反应将是什么呢?“听起来似乎并不是很复杂”。的确,掐指算算所涉及的工作无非包括几个反射调用,以及对原始类型数据的装箱或拆箱过程,其他的似乎都已经水到渠成。非常地好,让我们整理一下思绪,一起来完成一次完整的推演过程吧。

清单 11. 代理类中方法调用的分派转发推演实现
// 假设需代理接口 Simulator
public interface Simulator {
short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB;
} // 假设代理类为 SimulatorProxy, 其类声明将如下
final public class SimulatorProxy implements Simulator { // 调用处理器对象的引用
protected InvocationHandler handler; // 以调用处理器为参数的构造函数
public SimulatorProxy(InvocationHandler handler){
this.handler = handler;
} // 实现接口方法 simulate
public short simulate(int arg1, long arg2, String arg3)
throws ExceptionA, ExceptionB { // 第一步是获取 simulate 方法的 Method 对象
java.lang.reflect.Method method = null;
try{
method = Simulator.class.getMethod(
"simulate",
new Class[] {int.class, long.class, String.class} );
} catch(Exception e) {
// 异常处理 1(略)
} // 第二步是调用 handler 的 invoke 方法分派转发方法调用
Object r = null;
try {
r = handler.invoke(this,
method,
// 对于原始类型参数需要进行装箱操作
new Object[] {new Integer(arg1), new Long(arg2), arg3});
}catch(Throwable e) {
// 异常处理 2(略)
}
// 第三步是返回结果(返回类型是原始类型则需要进行拆箱操作)
return ((Short)r).shortValue();
}
}

模拟推演为了突出通用逻辑所以更多地关注正常流程,而淡化了错误处理,但在实际中错误处理同样非常重要。从以上的推演中我们可以得出一个非常通用的结构化流程:第一步从代理接口获取被调用的方法对象,第二步分派方法到调用处理器执行,第三步返回结果。在这之中,所有的信息都是可以已知的,比如接口名、方法名、参数类型、返回类型以及所需的装箱和拆箱操作,那么既然我们手工编写是如此,那又有什么理由不相信 ProxyGenerator 不会做类似的实现呢?至少这是一种比较可能的实现。

接下来让我们把注意力重新回到先前被淡化的错误处理上来。在异常处理 1 处,由于我们有理由确保所有的信息如接口名、方法名和参数类型都准确无误,所以这部分异常发生的概率基本为零,所以基本可以忽略。而异常处理 2 处,我们需要思考得更多一些。回想一下,接口方法可能声明支持一个异常列表,而调用处理器 invoke 方法又可能抛出与接口方法不支持的异常,再回想一下先前提及的 Java 动态代理的关于异常处理的特点,对于不支持的异常,必须抛 UndeclaredThrowableException 运行时异常。所以通过再次推演,我们可以得出一个更加清晰的异常处理 2 的情况:

清单 12. 细化的异常处理 2
Object r = null; 

try {
r = handler.invoke(this,
method,
new Object[] {new Integer(arg1), new Long(arg2), arg3}); } catch( ExceptionA e) { // 接口方法支持 ExceptionA,可以抛出
throw e; } catch( ExceptionB e ) {
// 接口方法支持 ExceptionB,可以抛出
throw e; } catch(Throwable e) {
// 其他不支持的异常,一律抛 UndeclaredThrowableException
throw new UndeclaredThrowableException(e);
}

这样我们就完成了对动态代理类的推演实现。推演实现遵循了一个相对固定的模式,可以适用于任意定义的任何接口,而且代码生成所需的信息都是可知的,那么有理由相信即使是机器自动编写的代码也有可能延续这样的风格,至少可以保证这是可行的。

回页首

美中不足

诚然,Proxy 已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫 Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在 Java 中本质上就行不通。

有很多条理由,人们可以否定对 class 代理的必要性,但是同样有一些理由,相信支持 class 动态代理会更美好。接口和类的划分,本就不是很明显,只是到了 Java 中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。

但是,不完美并不等于不伟大,伟大是一种本质,Java 动态代理就是佐例。

本文希望将 Java 动态代理机制从接口扩展到类,使得类能够享有与接口类似的动态代理支持。

设计及特点

新扩展的类名为 ProxyEx,将直接继承于 java.lang.reflect.Proxy,也声明了与原 Proxy 类中同名的 public 静态方法,目的是保持与原代理机制在使用方法上的完全一致。

图 1. ProxyEx 类继承图

与原代理机制最大的区别在于,动态生成的代理类将不再从 Proxy 类继承,改而继承需被代理的类。由于 Java 的单继承原则,扩展代理机制所支持的类数目不得多于一个,但它可以声明实现若干接口。包管理的机制与原来相似,不支持一个以上的类和接口同时为非 public;如果仅有一个非 public 的类或接口,假设其包为 PackageA,则动态生成的代理类将位于包 PackageA;否则将位于被代理的类所在的包。生成的代理类也被赋予 final 和 public 访问属性,且其命名规则类似地为“父类名 +ProxyN”(N 也是递增的阿拉伯数字)。最后,在异常处理方面则与原来保持完全一致。

图 2. 动态生成的代理类的继承图

回页首

模板

通过对 Java 动态代理机制的推演,我们已经获得了一个通用的方法模板。可以预期的是,通过模板来定制和引导代理类的代码生成,是比较可行的方法。我们将主要使用两个模板:类模板和方法模板。

清单 1. 类模板
package &Package;
final public class &Name &Extends &Implements
{
private java.lang.reflect.InvocationHandler handler = null;
&Constructors
&Methods
}

类模板定制了代理类的代码框架。其中带“&”前缀的标签位被用来引导相应的代码替换。在此预留了包(&Package)、类名(&ClassName)、类继承(&Extends)、接口实现(&Implements)、构造函数集(&Constructors)及方法集(&Methods)的标签位。类模板还同时声明了一个私有型的调用处理器对象作为类成员。

清单 2. 方法模板
&Modifiers &ReturnType &MethodName(&Parameters) &Throwables
{
java.lang.reflect.Method method = null;
try {
method = &Class.getMethod( \"& MethodName\", &ParameterTypes );
}
catch(Exception e){
}
Object r = null;
try{
r = handler.invoke( this, method, &ParameterValues );
}&Exceptions
&Return
}

方法模板定制了代理类方法集合中各个方法的代码框架,同样的带“&”前缀的标签位被用来引导相应的代码替换。在此预留了修饰符(&Modifiers)、返回类型(&ReturnType)、方法名(&MethodName)、参数列表(Parameters)、异常列表(&Throwables)、方法的声明类(&Class)、参数类型列表(&ParameterTypes)、调用处理器的参数值列表(&ParameterValues),异常处理(&Exceptions)及返回值(&Return)的标签位。

回页首

代码生成

有了类模板和方法模板,代码生成过程就变得有章可依。基本过程可分为三步:1)生成代理类的方法集合;2)生成代理类的构造函数;3)最后生成整个代理类。

生成代理类的方法集

第一步,通过反射获得被代理类的所有 public 或 protected 且非 static 的 Method 对象列表,这些方法将被涵盖的原因是它们是可以被其他类所访问的。

第二步,遍历 Method 对象列表,对每个 Method 对象,进行相应的代码生成工作。

清单 3. 对标签位进行代码替换生成方法代码
String declTemplate = "&Modifiers &ReturnType &MethodName(&Parameters) &Throwables";
String bodyTemplate = "&Declaration &Body";
// 方法声明
String declare = declTemplate.replaceAll("&Modifiers", getMethodModifiers( method ))
.replaceAll("&ReturnType", getMethodReturnType( method ))
.replaceAll("&MethodName", method.getName())
.replaceAll("&Parameters", getMethodParameters( method ))
.replaceAll("&Throwables", getMethodThrowables( method )); // 方法声明以及实现
String body = bodyTemplate.replaceAll("&Declaration", declare )
.replaceAll("&Body", getMethodEntity( method ));

这里涉及了一些 ProxyEx 类的私有的辅助函数如 getMethodModifiers 和 getMethodReturnType 等等,它们都是通过反射获取所需的信息,然后动态地生成各部分代码。函数 getMethodEntity 是比较重要的辅助函数,它又调用了其他的辅助函数来生成代码并替换标签位。

清单 4. ProxyEx 的静态方法 getMethodEntity()
private static String getMethodEntity( Method method )
{
String template = "\n{"
+ "\n java.lang.reflect.Method method = null;"
+ "\n try{"
+ "\n method = &Class.getMethod( \"&MethodName\", &ParameterTypes );"
+ "\n }"
+ "\n catch(Exception e){"
+ "\n }"
+ "\n Object r = null;"
+ "\n try{"
+ "\n r = handler.invoke( this, method, &ParameterValues );"
+ "\n }&Exceptions"
+ "\n &Return"
+ "\n}"; String result = template.replaceAll("&MethodName", method.getName() )
.replaceAll("&Class", method.getDeclaringClass().getName() + ".class")
.replaceAll("&ParameterTypes", getMethodParameterTypesHelper(method))
.replaceAll("&ParameterValues", getMethodParameterValuesHelper(method) )
.replaceAll("&Exceptions", getMethodParameterThrowablesHelper(method))
.replaceAll("&Return", getMethodReturnHelper( method ) ); return result;
}

当为 Class 类型对象生成该类型对应的字符代码时,可能涉及数组类型,反推过程会需要按递归方法生成代码,这部分工作由 getTypeHelper 方法提供

清单 5. ProxyEx 的静态方法 getTypeHelper()
private static String getTypeHelper(Class type)
{
if( type.isArray() )
{
Class c = type.getComponentType();
return getTypeHelper(c) + "[]";
}
else
{
return type.getName();
}
}

第三步,将所生成的方法保存进一个 map 表,该表记录的是键值对(方法声明,方法实现)。由于类的多态性,父类的方法可能被子类所覆盖,这时以上通过遍历所得的方法列表中就会出现重复的方法对象,维护该表可以很自然地达到避免方法重复生成的目的,这就维护该表的原因所在。

生成代理类的构造函数

相信读者依然清晰记得代理类是通过其构造函数反射生成的,而构造时传入的唯一参数就是调用处理器对象。为了保持与原代理机制的一致性,新的代理类的构造函数也同样只有一个调用处理器对象作为参数。模板简单如下

清单 6. 构造函数模板
public &Constructor(java.lang.reflect.InvocationHandler handler)
{
super(&Parameters);
this.handler = handler;
}

需要特别提一下的是 super 方法的参数值列表 &Parameters 的生成,我们借鉴了 Mock 思想,侧重于追求对象构造的成功,而并未过多地努力分析并寻求最准确最有意义的赋值。对此,相信读者会多少产生一些疑虑,但稍后我们会提及改进的方法,请先继续阅读。

生成整个代理类

通过以上步骤,构造函数和所有需被代理的方法的代码已经生成,接下来就是生成整个代理类的时候了。这个过程也很直观,通过获取相关信息并对类模板中各个标签位进行替换,便可以轻松的完成整个代理类的代码生成。

回页首

被遗忘的角落:类变量

等等,似乎遗忘了什么?从调用者的角度出发,我们希望代理类能够作为被代理类的如实代表呈现在用户面前,包括其内部状态,而这些状态通常是由类变量所体现出来的,于是就涉及到类变量的代理问题。

要解决这个问题,首先需要思考何时两者的类变量可能出现不一致?回答了这个问题,也就找到了解决思路。回顾代理类的构造函数,我们以粗糙的方式构造了代理类实例。它们可能一开始就已经不一致了。还有每次方法调用也可能导致被两者的类变量的不一致。如何解决?直观的想法是:1)构造时需设法进行同步;2)方法调用之前和之后也需设法进行同步。这样,我们就能够有效避免代理类和被代理类的类变量不一致的问题的出现了。

但是,如何获得被代理类的实例呢?从当前的的设计中已经没有办法做到。既然如此,那就继续我们的扩展之旅。只不过这次扩展的对象是调用处理器接口,我们将在扩展后的接口里加入获取被代理类对象的方法,且扩展调用处理器接口将以 static 和 public 的形式被定义在 ProxyEx 类中。

清单 7. ProxyEx 类内的静态接口 InvocationHandlerEx
public static interface InvocationHandlerEx extends InvocationHandler
{
// 返回指定 stubClass 参数所对应的被代理类实体对象
Object getStub(Class stubClass);
}

新的调用处理器接口具备了获取被代理类对象的能力,从而为实现类变量的同步打开了通道。接下来还需要的就是执行类变量同步的 sync 方法,每个动态生成的代理类中都会被悄悄地加入这个私有方法以供调用。每次方法被分派转发到调用处理器执行之前和之后,sync 方法都会被调用,从而保证类变量的双向实时更新。相应的,方法模板也需要更新以支持该新特性。

清单 8. 更新后的方法模板(部分)
Object r = null;
try{
// 代理类到被代理类方向的变量同步
sync(&Class, true);
r = handler.invoke( this, method, &ParameterValues );
// 被代理类到代理类方向的变量同步
sync(&Class, false);
}&Exceptions &Return

sync 方法还会在构造函数尾部被调用,从而将被代理类对象的变量信息同步到代理类对象,实现类似于拷贝构造的等价效果。相应的,构造函数模板也需要更新以支持该新特性。

清单 9. 更新后的构造函数模板
public &Name(java.lang.reflect.InvocationHandler handler)
{
super(&Parameters);
this.handler = handler;
// 被代理类到代理类方向的变量同步
sync(null, false);
}

接下来介绍 sync 方法的实现,其思想就是首先获取被代理类的所有 Field 对象的列表,并通过扩展的调用处理器获得方法的声明类说对应的 stub 对象,然后遍历 Field 对象列表并对各个变量进行拷贝同步。

清单 10. 声明在动态生成的代理类内部的 snyc 函数
private synchronized void sync(java.lang.Class clazz, boolean toStub)
{
// 判断是否为扩展调用处理器
if( handler instanceof InvocationHandlerEx )
{
java.lang.Class superClass = this.getClass().getSuperclass();
java.lang.Class stubClass = ( clazz != null ? clazz : superClass ); // 通过扩展调用处理器获得stub对象
Object stub = ((InvocationHandlerEx)handler).getStub(stubClass);
if( stub != null )
{
// 获得所有需同步的类成员列表,遍历并同步
java.lang.reflect.Field[] fields = getFields(superClass);
for(int i=0; fields!=null&&i<fields.length; i++)
{
try
{
fields[i].setAccessible(true);
// 执行代理类和被代理类的变量同步
if(toStub)
{
fields[i].set(stub, fields[i].get(this));
}
else
{
fields[i].set(this, fields[i].get(stub));
}
}
catch(Throwable e)
{
}
}
}
}
}

这里涉及到一个用于获取类的所有 Field 对象列表的静态辅助方法 getFields。为了提高频繁查询时的性能,配合该静态方法的是一个静态的 fieldsMap 对象,用于记录已查询过的类其所包含的 Field 对象列表,使得再次查询时能迅速返回其对应列表。相应的,类模板也需进行更新。

清单 11. 增加了静态 fieldsMap 变量后的类模板
package &Package;
final public class &Name &Extends &Implements
{
private static java.util.HashMap fieldsMap = new java.util.HashMap();
private java.lang.reflect.InvocationHandler handler = null;
&Constructors
&Methods
}
清单 12. 声明在动态生成的代理类内部的静态方法 getFields
private static java.lang.reflect.Field[] getFields(java.lang.Class c)
{
if( fieldsMap.containsKey(c) )
{
return (java.lang.reflect.Field[])fieldsMap.get(c);
} java.lang.reflect.Field[] fields = null;
if( c == java.lang.Object.class )
{
fields = c.getDeclaredFields();
}
else
{
java.lang.reflect.Field[] fields0 = getFields(c.getSuperclass());
java.lang.reflect.Field[] fields1 = c.getDeclaredFields();
fields = new java.lang.reflect.Field[fields0.length + fields1.length];
System.arraycopy(fields0, 0, fields, 0, fields0.length);
System.arraycopy(fields1, 0, fields, fields0.length, fields1.length);
}
fieldsMap.put(c, fields);
return fields;
}

回页首

动态编译及装载

代码生成以后,需要经过编译生成 JVM 所能识别的字节码,而字节码还需要通过类装载器载入 JVM 才能最终被真正使用,接下来我们将阐述如何动态编译及装载。

首先是动态编译。这部分由 ProxyEx 类的 getProxyClassCodeSource 函数完成。该函数分三步进行:第一步保存源代码到 .java 文件;第二步编译该 .java 文件;第三步从输出的 .class 文件读取字节码。

清单 13. ProxyEx 的静态方法 getProxyClassCodeSource
private static byte[] getProxyClassCodeSource( String pkg, String className,
String declare ) throws Exception
{
// 将类的源代码保存进一个名为类名加“.java”的本地文件
File source = new File(className + ".java");
FileOutputStream fos = new FileOutputStream( source );
fos.write( declare.getBytes() );
fos.close(); // 调用com.sun.tools.javac.Main类的静态方法compile进行动态编译
int status = com.sun.tools.javac.Main.compile( new String[] {
"-d",
".",
source.getName() } ); if( status != 0 )
{
source.delete();
throw new Exception("Compiler exit on " + status);
} // 编译得到的字节码将被输出到与包结构相同的一个本地目录,文件名为类名加”.class”
String output = ".";
int curIndex = -1;
int lastIndex = 0;
while( (curIndex=pkg.indexOf('.', lastIndex)) != -1 )
{
output = output + File.separator + pkg.substring( lastIndex, curIndex );
lastIndex = curIndex + 1;
}
output = output + File.separator + pkg.substring( lastIndex );
output = output + File.separator + className + ".class"; // 从输出文件中读取字节码,并存入字节数组
File target = new File(output);
FileInputStream f = new FileInputStream( target );
byte[] codeSource = new byte[(int)target.length()];
f.read( codeSource );
f.close(); // 删除临时文件
source.delete();
target.delete(); return codeSource;
}

得到代理类的字节码,接下来就可以动态装载该类了。这部分由 ProxyEx 类的 defineClassHelper 函数完成。该函数分两步进行:第一步通过反射获取父类 Proxy 的静态私有方法 defineClass0;第二步传入字节码数组及其他相关信息并反射调用该方法以完成类的动态装载。

清单 14. ProxyEx 的静态方法 defineClassHelper
private static Class defineClassHelper( String pkg, String cName, byte[] codeSource )
throws Exception
{
Method defineClass = Proxy.class.getDeclaredMethod( "defineClass0",
new Class[] { ClassLoader.class,
String.class,
byte[].class,
int.class,
int.class } ); defineClass.setAccessible(true);
return (Class)defineClass.invoke( Proxy.class,
new Object[] { ProxyEx.class.getClassLoader(),
pkg.length()==0 ? cName : pkg+"."+cName,
codeSource,
new Integer(0),
new Integer(codeSource.length) } );
}

回页首

性能改进

原动态代理机制中对接口数组有一些有趣的特点,其中之一就是接口的顺序差异会在一定程度上导致生成新的代理类,即使其实并无必要。其中的原因就是因为缓存表是以接口名称列表作为关键字,所以不同的顺序就意味着不同的关键字,如果对应的关键字不存在,就会生成新但是作用重复的代理类。在 ProxyEx 类中,我们通过主动排序避免了类似的问题,提高动态生成代理类的效率。而且,如果发现数组中都是接口类型,则直接调用父类 Proxy 的静态方法 getProxyClass 生成代理类,否则才通过扩展动态代理机制生成代理类,这样也一定程度上改进了性能。

回页首

兼容性问题

接下来需要考虑的是与原代理机制的兼容性问题。曾记否,Proxy 中还有两个静态方法:isProxyClass 和 getInvocationHandler,分别被用于判断 Class 对象是否是动态代理类和从 Object 对象获取对应的调用处理器(如果可能的话)。

清单 15. Proxy 的静态方法 isProxyClass 和 getInvocationHandler
static boolean isProxyClass(Class cl)
static InvocationHandler getInvocationHandler(Object proxy)

现在的兼容性问题,主要涉及到 ProxyEx 类与父类 Proxy 在关于动态生成的代理类的信息方面所面临的如何保持同步的问题。曾介绍过,在 Proxy 类中有个私有的 Map 对象 proxyClasses 专门负责保存所有动态生成的代理类类型。Proxy 类的静态函数 isProxyClass 就是通过查询该表以确定某 Class 对象是否为动态代理类,我们需要做的就是把由 ProxyEx 生成的代理类类型也保存入该表。这部分工作由 ProxyEx 类的静态方法 addProxyClass 辅助完成。

清单 16. ProxyEx 的静态方法 addProxyClass
private static void addProxyClass( Class proxy ) throws IllegalArgumentException
{
try
{
// 通过反射获取父类的私有 proxyClasses 变量并更新
Field proxyClasses = Proxy.class.getDeclaredField("proxyClasses");
proxyClasses.setAccessible(true);
((Map)proxyClasses.get(Proxy.class)).put( proxy, null );
}
catch(Exception e)
{
throw new IllegalArgumentException(e.toString());
}
}

相对而言,原来 Proxy 类的静态方法 getInvocationHandler 实现相当简单,先判断是否为代理类,若是则直接类型转换到 Proxy 并返回其调用处理器成员,而扩展后的代理类并不非从 Proxy 类继承,所以在获取调用处理器对象的方法上需要一些调整。这部分由 ProxyEx 类的同名静态方法 getInvocationHandler 完成。

清单 17. ProxyEx 的静态方法 getInvocationHandler
public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException
{
// 如果Proxy实例,直接调父类的方法
if( proxy instanceof Proxy )
{
return Proxy.getInvocationHandler( proxy );
} // 如果不是代理类,抛异常
if( !Proxy.isProxyClass( proxy.getClass() ))
{
throw new IllegalArgumentException("Not a proxy instance");
} try
{
// 通过反射获取扩展代理类的调用处理器对象
Field invoker = proxy.getClass().getDeclaredField("handler");
invoker.setAccessible(true);
return (InvocationHandler)invoker.get(proxy);
}
catch(Exception e)
{
throw new IllegalArgumentException("Suspect not a proxy instance");
}
}

回页首

坦言:也有局限

受限于 Java 的类继承机制,扩展的动态代理机制也有其局限,它不能支持:

  1. 声明为 final 的类;
  2. 声明为 final 的函数;
  3. 构造函数均为 private 类型的类;

回页首

实例演示

阐述了这么多,相信读者一定很想看一下扩展动态代理机制是如何工作的。本文最后将以 2010 世博门票售票代理为模型进行演示。

首先,我们定义了一个售票员抽象类 TicketSeller。

清单 18. TicketSeller
public abstract class TicketSeller
{
protected String theme;
protected TicketSeller(String theme)
{
this.theme = theme;
}
public String getTicketTheme()
{
return this.theme;
}
public void setTicketTheme(String theme)
{
this.theme = theme;
}
public abstract int getTicketPrice();
public abstract int buy(int ticketNumber, int money) throws Exception;
}

其次,我们会实现一个 2010 世博门票售票代理类 Expo2010TicketSeller。

清单 19. Expo2010TicketSeller
public class Expo2010TicketSeller extends TicketSeller
{
protected int price;
protected int numTicketForSale;
public Expo2010TicketSeller()
{
super("World Expo 2010");
this.price = 180;
this.numTicketForSale = 200;
}
public int getTicketPrice()
{
return price;
}
public int buy(int ticketNumber, int money) throws Exception
{
if( ticketNumber > numTicketForSale )
{
throw new Exception("There is no enough ticket available for sale, only "
+ numTicketForSale + " ticket(s) left");
}
int charge = money - ticketNumber * price;
if( charge < 0 )
{
throw new Exception("Money is not enough. Still needs "
+ (-charge) + " RMB.");
}
numTicketForSale -= ticketNumber;
return charge;
}
}

接着,我们将通过购票者类 TicketBuyer 来模拟购票以演示扩展动态代理机制。

清单 20. TicketBuyer
public class TicketBuyer
{
public static void main(String[] args)
{
// 创建真正的TickerSeller对象,作为stub实体
final TicketSeller stub = new Expo2010TicketSeller(); // 创建扩展调用处理器对象
InvocationHandler handler = new InvocationHandlerEx()
{
public Object getStub(Class stubClass)
{
// 仅对可接受的Class类型返回stub实体
if( stubClass.isAssignableFrom(stub.getClass()) )
{
return stub;
}
return null;
} public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Object o;
try
{
System.out.println(" >>> Enter method: "
+ method.getName() );
o = method.invoke(stub, args);
}
catch(InvocationTargetException e)
{
throw e.getCause();
}
finally
{
System.out.println(" <<< Exit method: "
+ method.getName() );
}
return o;
}
}; // 通过ProxyEx构造动态代理
TicketSeller seller = (TicketSeller)ProxyEx.newProxyInstance(
TicketBuyer.class.getClassLoader(),
new Class[] {TicketSeller.class},
handler); // 显示代理类的类型
System.out.println("Ticket Seller Class: " + seller.getClass() + "\n");
// 直接访问theme变量,验证代理类变量在对象构造时同步的有效性
System.out.println("Ticket Theme: " + seller.theme + "\n");
// 函数访问price信息
System.out.println("Query Ticket Price...");
System.out.println("Ticket Price: " + seller.getTicketPrice() + " RMB\n");
// 模拟票务交易
buyTicket(seller, 1, 200);
buyTicket(seller, 1, 160);
buyTicket(seller, 250, 30000);
// 直接更新theme变量
System.out.println("Updating Ticket Theme...\n");
seller.theme = "World Expo 2010 in Shanghai";
// 函数访问theme信息,验证扩展动态代理机制对变量同步的有效性
System.out.println("Query Updated Ticket Theme...");
System.out.println("Updated Ticket Theme: " + seller.getTicketTheme() + "\n");
}
// 购票函数
protected static void buyTicket(TicketSeller seller, int ticketNumber, int money)
{
try
{
System.out.println("Transaction: Order " + ticketNumber + " ticket(s) with "
+ money + " RMB");
int charge = seller.buy(ticketNumber, money);
System.out.println("Transaction: Succeed - Charge is " + charge + " RMB\n");
}
catch (Exception e)
{
System.out.println("Transaction: Fail - " + e.getMessage() + "\n");
}
}
}

最后,见演示程序的执行结果。

清单 21. 执行输出
Ticket Seller Class: class com.demo.proxy.test.TicketSellerProxy0

Ticket Theme: World Expo 2010

Query Ticket Price...
>>> Enter method: getTicketPrice
<<< Exit method: getTicketPrice
Ticket Price: 180 RMB Transaction: Order 1 ticket(s) with 200 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Succeed - Charge is 20 RMB Transaction: Order 1 ticket(s) with 160 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Fail - Money is not enough. Still needs 20 RMB. Transaction: Order 250 ticket(s) with 30000 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Fail - There is no enough ticket available for sale, only 199 ticket(s) left Updating Ticket Theme... Query Updated Ticket Theme...
>>> Enter method: getTicketTheme
<<< Exit method: getTicketTheme
Updated Ticket Theme: World Expo 2010 in Shanghai

Java 动态代理机制分析及扩展--转的更多相关文章

  1. Java 动态代理机制分析及扩展

    Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...

  2. Java 动态代理机制分析及扩展,第 1 部分

    Java 动态代理机制分析及扩展,第 1 部分 http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ 本文通过分析 Java 动态代理的机制和特 ...

  3. [转]Java 动态代理机制分析及扩展

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

  4. Java代理和动态代理机制分析和应用

    本博文中项目代码已开源下载地址:GitHub Java代理和动态代理机制分析和应用 概述 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息 ...

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

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

  6. 大厂高级工程师面试必问系列:Java动态代理机制和实现原理详解

    代理模式 Java动态代理运用了设计模式中常用的代理模式 代理模式: 目的就是为其他对象提供一个代理用来控制对某个真实对象的访问 代理类的作用: 为委托类预处理消息 过滤消息并转发消息 进行消息被委托 ...

  7. Java 动态代理机制详解

    在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...

  8. java动态代理机制

    首先了解代理设计模式,其思想是为其他对象提供一种代理以控制对这个对象的访问. java动态代理就是遵循这种思想,spring中的AOP实现原理就是java的动态代理. 在java的动态代理机制中,有两 ...

  9. 深入理解 Java 动态代理机制

    Java 有两种代理方式,一种是静态代理,另一种是动态代理.对于静态代理,其实就是通过依赖注入,对对象进行封装,不让外部知道实现的细节.很多 API 就是通过这种形式来封装的. 代理模式结构图(图片来 ...

随机推荐

  1. MVC中控制器向视图传值的四种方式

    MVC中的控制器向视图传值有四种方式分别是 1 ViewDate  2.ViewBag   3.TempDate  4.Model 下面分别介绍四种传值方式 首先先显示出控制器中的代码 using S ...

  2. 数据库表结构文档查看器 基于netcore

    前言 日常开发业务代码,新接手一块不熟悉的业务时需要频繁的查看对应业务的数据库表设计文档.相比于直接翻看业务代码,有必要提供一个数据库表结构文档查看器来解决这些繁琐的问题. CML.SqlDoc CM ...

  3. 使用Anaconda的python安装虚拟环境是出现错误:python -m venv venvdir----Error: Command '['D:\\Development\\Django\\test\\Scripts\\python.exe', '-Im', 'ensurepip', '--upgrade', '--default-pip']' returned non-zero exit

    在创建python虚拟环境的时候,如果使用的是Anaconda中集成的python -m venv venvdir就会出现不能安装pip的错误,原因是Anaconda没有ensurepip, 解决办法 ...

  4. linux强制安装rpm包的命令

    rpm -ivh *********.rpm  --nodeps --force 强制安装会忽略掉所有依赖关系,强制进行安装

  5. 如何学习sql语言?

    如何学习 SQL 语言? https://www.zhihu.com/question/19552975 没有任何基础的人怎么学SQL? https://www.zhihu.com/question/ ...

  6. BZOJ3729: Gty的游戏(伪ETT)

    题面 传送门 前置芝士 巴什博奕 \(Nim\)游戏的改版,我们现在每次最多只能取走\(k\)个石子,那么\(SG\)函数很容易写出来 \[SG(x)=mex_{i=1}^{\min(x,k)}SG( ...

  7. jmeter进行https协议的测试

    一.HTTPS和HTTP的区别     超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息.HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和 ...

  8. angular核心原理解析1:angular自启动过程

    angularJS的源代码整体上来说是一个自执行函数,在angularJS加载完成后,就会自动执行了. angular源代码中: angular = window.angular || (window ...

  9. 47.ActiveMQ集群

    (声明:本文非EamonSec原创) 使用ZooKeeper实现的Master-Slave实现方式,是对ActiveMQ进行高可用的一种有效的解决方案,高可用的原理:使用ZooKeeper(集群)注册 ...

  10. Visual Studio性能计数器,负载测试结果分析- Part III

    对于一个多用户的应用程序,性能是非常重要的.性能不仅是执行的速度,它包括负载和并发方面.Visual Studio是可以用于性能测试的工具之一.Visual Studio Test版或Visual S ...