Java 下高效的反射工具包 ReflectASM 使用例解

ReflectASM 使用字节码生成的方式实现了更为高效的反射机制。执行时会生成一个存取类来 set/get 字段,访问方法或创建实例。一看到 ASM 就能领悟到 ReflectASM 会用字节码生成的方式,而不是依赖于 Java 本身的反射机制来实现的,所以它更快,并且避免了访问原始类型因自动装箱而产生的问题。


下面三个图是 ReflectASM 与 Java 自身反射机制的性能对比,表现很不错的。




测试代码包含在项目文件中. 上面图形是在 Oracle 的 Java 7u3, server VM 下测试出的结果。

下面我们自己来做个测试,测试环境是 Mac OS X 10.8, 2.4G Core 2 Duo, 4G RAM, 64 位 JDK 1.6.

待反射的类 SomeClass.java
1package cc.unmi.testreflect;
2
3public class SomeClass {
4    private String name;
5
6    public void foo(String name) {
7        this.name = name;
8    }
9}

测试类 ReflectasmClient.java
 1package cc.unmi.testreflect;
 2
 3import java.lang.reflect.Method;
 4import com.esotericsoftware.reflectasm.MethodAccess;
 5
 6/**
 7 * @author Unmi
 8 */
 9public class ReflectasmClient {
10
11    public static void main(String[] args) throws Exception {
12        testJdkReflect();
13//        testReflectAsm();
14    }
15    
16    public static void testJdkReflect() throws Exception {
17        SomeClass someObject = new SomeClass();        
18        for (int i = 0; i < 5; i++) {
19            long begin = System.currentTimeMillis();
20            for (int j = 0; j < 100000000; j++) {
21                Method method = SomeClass.class.getMethod("foo", String.class);
22                method.invoke(someObject, "Unmi");
23            }
24            System.out.print(System.currentTimeMillis() - begin +" ");
25        }
26    }
27
28    public static void testReflectAsm() {
29        SomeClass someObject = new SomeClass();
30        for (int i = 0; i < 5; i++) {
31            long begin = System.currentTimeMillis();
32            for (int j = 0; j < 100000000; j++) {
33                MethodAccess access = MethodAccess.get(SomeClass.class);
34                access.invoke(someObject, "foo", "Unmi");
35            }
36            System.out.print(System.currentTimeMillis() - begin + " ");
37        }
38    }
39}

分别运行 testJdkReflect() 和 testReflectAsm 得出各自的运行时间数据,如下:

运行 testJdkReflect(): 31473 31663 31578 31658 31552

运行 testReflectAsm(): 312814 310666 312867 311234 311792

这个数据是非常恐怖的,似乎在带领我们往相反的方向上走,用 ReflectASM 怎么反而耗时多的多,高一个数量级,为什么呢?原因是大部分的时间都耗费在了

MethodAccess access = MethodAccess.get(SomeClass.class);

上,正是生成字节码的环节上,也让你体验到 MethodAccess 是个无比耗时的操作,如果把这行放到循环之外会是什么样的结果呢,同时也把方法 testJdkReflect() 中的

Method method = SomeClass.class.getMethod("foo", String.class);

也提出去,改变后的 testJdkReflect() 和 testReflectAsm() 分别如下:
 1public static void testJdkReflect() throws Exception {
 2    SomeClass someObject = new SomeClass();        
 3    Method method = SomeClass.class.getMethod("foo", String.class);
 4    for (int i = 0; i < 5; i++) {
 5        long begin = System.currentTimeMillis();
 6        for (int j = 0; j < 100000000; j++) {
 7            method.invoke(someObject, "Unmi");
 8        }
 9        System.out.print(System.currentTimeMillis() - begin +" ");
10    }
11}
12
13public static void testReflectAsm() {
14    SomeClass someObject = new SomeClass();
15    MethodAccess access = MethodAccess.get(SomeClass.class);
16    for (int i = 0; i < 5; i++) {
17        long begin = System.currentTimeMillis();
18        for (int j = 0; j < 100000000; j++) {
19            access.invoke(someObject, "foo", "Unmi");
20        }
21        System.out.print(System.currentTimeMillis() - begin + " ");
22    }
23}

再次分别跑下 testJdkReflect() 和 testReflectAsm(),新的结果如下:

运行 testJdkReflect(): 1682 1696 1858 1774 1780 ------ 平均 1758

运行 testReflectAsm(): 327 549 520 509 514 ------ 平均 483.8

胜负十分明显,上面的实验两相一比较,用 ReflectAsm 进行方法调用节省时间是 72.48%

也因此可以得到使用 ReflectASM 时需特别注意的是,获得类似 MethodAccess 实例只做一次,或它的实例应缓存起来,才是真正用好 ReflectASM。

进一步深入的话,不妨看看分别从 testJdkReflect()/testReflectAsm() 到 SomeClass.foo() 过程中到底发生了什么,断点看调用栈。

testJdkReflect() 到 SomeClass.foo() 的调用栈:


借助了 JDK 的 DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl。

再看 testReflectAsm() 到 SomeClass.foo()的调用栈:


可以看到,ReflectAsm 在执行 MethodAccess access = MethodAccess.get(SomeClass.class); 为你生成了类 SomeClassMethodAccess,经由它来进行后续的方法调用,使得性能上有很可观的改善。

上面只是讲述了,调用方法时如何使用 ReflectAsm,以及怎么确保高效性。下面补上 ReflectAsm 更多的用法,翻译自 ReflectAsm 官方。




ReflectASM 反射调用方法:
1SomeClass someObject = ...
2MethodAccess access = MethodAccess.get(SomeClass.class);
3access.invoke(someObject, "setName", "Awesome McLovin");
4String name = (String)access.invoke(someObject, "getName");

用 ReflectASM 反射来 set/get 字段值:
1SomeClass someObject = ...
2FieldAccess access = FieldAccess.get(SomeClass.class);
3access.set(someObject, "name", "Awesome McLovin");
4String name = (String)access.get(someObject, "name");

用 ReflectASM 反射来调用构造方法:
1ConstructorAccess<SomeClass> access = ConstructorAccess.get(SomeClass.class);
2SomeClass someObject = access.newInstance();

避免用方法名来查找

为了在重复性的反射来访问方法或字段时最大化性能,应该用方法和字段的索引来定位而不是名称:
1SomeClass someObject = ...
2MethodAccess access = MethodAccess.get(SomeClass.class);
3int addNameIndex = access.getIndex("addName");
4for (String name : names)
5    access.invoke(someObject, addNameIndex, "Awesome McLovin");

说到这,不妨再次来验证一下,把 testReflectAsm() 方法改为如下:
 1public static void testReflectAsm() {
 2    SomeClass someObject = new SomeClass();
 3    MethodAccess access = MethodAccess.get(SomeClass.class);
 4    int fooIndex = access.getIndex("foo", String.class);
 5    for (int i = 0; i < 5; i++) {
 6        long begin = System.currentTimeMillis();
 7        for (int j = 0; j < 100000000; j++) {
 8            access.invoke(someObject, fooIndex, "Unmi");
 9        }
10        System.out.print(System.currentTimeMillis() - begin + " ");
11    }
12}

运行的输出结果是,你可能想像不到的:

206 182 171 175 171

而用名称查找方法时的测试数据为:327 549 520 509 514

当然你调用的重复性应该带有一点夸张性质的。性能更优化的原因是用名称来查找最科要被转换成索引来查找。

可见性

ReflectASM 总是能访问公有成员的. 它会尝试在同一个 package 中去定义访问类的,并且同一个类加载器去加载。所以,如果安全管理器允许 setAccessible 调用成功的话,protected 或包私有(package private) 的成员也可被访问到. 假如 setAccessible 失败,仅当当有公有成员可被访问时,不会有异常抛出. 私有成员总是无法访问到。

有关异常

当使用 ReflectASM 有异常时,栈跟踪更清淅了。这是 Java 在反射调用方法时抛出了一个 RuntimeException 异常:
1Exception in thread "main" java.lang.reflect.InvocationTargetException
2        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
3        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
4        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
5        at java.lang.reflect.Method.invoke(Method.java:597)
6        at com.example.SomeCallingCode.doit(SomeCallingCode.java:22)
7Caused by: java.lang.RuntimeException
8        at com.example.SomeClass.someMethod(SomeClass.java:48)
9        ... 5 more

再看用 ReflectASM 时抛出的同样的异常:
1Exception in thread "main" java.lang.RuntimeException
2        at com.example.SomeClass.someMethod(SomeClass.java:48)
3        at com.example.SomeClassMethodAccess.invoke(Unknown Source)
4        at com.example.SomeCallingCode.doit(SomeCallingCode.java:22)

如果被 ReflectASM 调用的代码抛出了需检测的异常,也需要抛出需检测异常. 因为如果你在用 try/catch 捕获块中未声明抛出的具体类型的异常时会报编译错误。(Unmi 注:这句话的意思是说,比如方法 foo() 未声明抛出 IOException,而你 try 它时却 catch(IOException) 就会出现编译错误)所以当你在用 ReflectASM 反射调用,并需要关心其中抛出的异常时,你必须捕获的异常类型是 Exception。 永久链接 https://yanbin.blog/java-reflectasm-bytecode-usage/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。