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
测试类 ReflectasmClient.java
分别运行 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() 分别如下:
再次分别跑下 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 反射调用方法:
用 ReflectASM 反射来 set/get 字段值:
用 ReflectASM 反射来调用构造方法:
避免用方法名来查找
为了在重复性的反射来访问方法或字段时最大化性能,应该用方法和字段的索引来定位而不是名称:
说到这,不妨再次来验证一下,把 testReflectAsm() 方法改为如下:
运行的输出结果是,你可能想像不到的:
206 182 171 175 171
而用名称查找方法时的测试数据为:327 549 520 509 514
当然你调用的重复性应该带有一点夸张性质的。性能更优化的原因是用名称来查找最科要被转换成索引来查找。
可见性
ReflectASM 总是能访问公有成员的. 它会尝试在同一个 package 中去定义访问类的,并且同一个类加载器去加载。所以,如果安全管理器允许 setAccessible 调用成功的话,protected 或包私有(package private) 的成员也可被访问到. 假如 setAccessible 失败,仅当当有公有成员可被访问时,不会有异常抛出. 私有成员总是无法访问到。
有关异常
当使用 ReflectASM 有异常时,栈跟踪更清淅了。这是 Java 在反射调用方法时抛出了一个 RuntimeException 异常:
再看用 ReflectASM 时抛出的同样的异常:
如果被 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) 进行许可。
下面三个图是 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) 进行许可。