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

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

测试类 ReflectasmClient.java

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

分别运行 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() 分别如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     public static void testJdkReflect() throws Exception {
         SomeClass someObject = new SomeClass();        
         Method method = SomeClass. class .getMethod( "foo" , String. class );
         for ( int i = 0 ; i < 5 ; i++) {
             long begin = System.currentTimeMillis();
             for ( int j = 0 ; j < 100000000 ; j++) {
                 method.invoke(someObject, "Unmi" );
             }
             System.out.print(System.currentTimeMillis() - begin + " " );
         }
     }
 
     public static void testReflectAsm() {
         SomeClass someObject = new SomeClass();
         MethodAccess access = MethodAccess.get(SomeClass. class );
         for ( int i = 0 ; i < 5 ; i++) {
             long begin = System.currentTimeMillis();
             for ( int j = 0 ; j < 100000000 ; j++) {        
                 access.invoke(someObject, "foo" , "Unmi" );
             }
             System.out.print(System.currentTimeMillis() - begin + " " );
         }
     }

再次分别跑下 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。

本文原始链接 http://gloveangels.com/java-reflectasm-bytecode-usage/, 来自 隔叶黄莺 Unmi Blog

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

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

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


ReflectASM 反射调用方法:

1
2
3
4
SomeClass someObject = ...
MethodAccess access = MethodAccess.get(SomeClass. class );
access.invoke(someObject, "setName" , "Awesome McLovin" );
String name = (String)access.invoke(someObject, "getName" );

用 ReflectASM 反射来 set/get 字段值:

1
2
3
4
SomeClass someObject = ...
FieldAccess access = FieldAccess.get(SomeClass. class );
access.set(someObject, "name" , "Awesome McLovin" );
String name = (String)access.get(someObject, "name" );

用 ReflectASM 反射来调用构造方法:

1
2
ConstructorAccess access = ConstructorAccess.get(SomeClass. class );
SomeClass someObject = access.newInstance();

避免用方法名来查找

为了在重复性的反射来访问方法或字段时最大化性能,应该用方法和字段的索引来定位而不是名称:

1
2
3
4
5
SomeClass someObject = ...
MethodAccess access = MethodAccess.get(SomeClass. class );
int addNameIndex = access.getIndex( "addName" );
for (String name : names)
    access.invoke(someObject, addNameIndex, "Awesome McLovin" );

说到这,不妨再次来验证一下,把 testReflectAsm() 方法改为如下:

01
02
03
04
05
06
07
08
09
10
11
12
     public static void testReflectAsm() {
         SomeClass someObject = new SomeClass();
         MethodAccess access = MethodAccess.get(SomeClass. class );
         int fooIndex = access.getIndex( "foo" , String. class );
         for ( int i = 0 ; i < 5 ; i++) {
             long begin = System.currentTimeMillis();
             for ( int j = 0 ; j < 100000000 ; j++) {        
                 access.invoke(someObject, fooIndex, "Unmi" );
             }
             System.out.print(System.currentTimeMillis() - begin + " " );
         }
     }

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

206 182 171 175 171

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

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

可见性

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

有关异常

当使用 ReflectASM 有异常时,栈跟踪更清淅了。这是 Java 在反射调用方法时抛出了一个 RuntimeException 异常:

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java: 39 )
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java: 25 )
        at java.lang.reflect.Method.invoke(Method.java: 597 )
        at com.example.SomeCallingCode.doit(SomeCallingCode.java: 22 )
Caused by: java.lang.RuntimeException
        at com.example.SomeClass.someMethod(SomeClass.java: 48 )
        ... 5 more

再看用 ReflectASM 时抛出的同样的异常:

1
2
3
4
Exception in thread "main" java.lang.RuntimeException
        at com.example.SomeClass.someMethod(SomeClass.java: 48 )
        at com.example.SomeClassMethodAccess.invoke(Unknown Source)
        at com.example.SomeCallingCode.doit(SomeCallingCode.java: 22 )

如果被 ReflectASM 调用的代码抛出了需检测的异常,也需要抛出需检测异常. 因为如果你在用 try/catch 捕获块中未声明抛出的具体类型的异常时会报编译错误。(Unmi 注:这句话的意思是说,比如方法 foo() 未声明抛出 IOException,而你 try 它时却 catch(IOException) 就会出现编译错误)所以当你在用 ReflectASM 反射调用,并需要关心其中抛出的异常时,你必须捕获的异常类型是 Exception。

你可能感兴趣的:(反射-机制)