一次性能提高30倍的JAVA类反射性能优化实践
文章来源:宜信技术学院 & 宜信支付结算团队技术分享第4期-支付结算部支付研发团队高级工程师陶红《JAVA类反射技术&优化》
分享者:宜信支付结算部支付研发团队高级工程师陶红
原文首发于宜信支付结算技术团队公号:野指针
在实际工作中的一些特定应用场景下,JAVA类反射是经常用到、必不可少的技术,在项目研发过程中,我们也遇到了不得不运用JAVA类反射技术的业务需求,并且不可避免地面临这个技术固有的性能瓶颈问题。
通过近两年的研究、尝试和验证,我们总结出一套利用缓存机制、大幅度提高JAVA类反射代码运行效率的方法,和没有优化的代码相比,性能提高了20~30倍。本文将与大家分享在探索和解决这个问题的过程中的一些有价值的心得体会与实践经验。
简述:JAVA类反射技术
首先,用最简短的篇幅介绍JAVA类反射技术。
如果用一句话来概述,JAVA类反射技术就是:
绕开编译器,在运行期直接从虚拟机获取对象实例/访问对象成员变量/调用对象的成员函数。
抽象的概念不多讲,用代码说话……举个例子,有这样一个类:
public class ReflectObj {
private String field01;
public String getField01() {
return this.field01;
}
public void setField01(String field01) {
this.field01 = field01;
}
}
如果按照下列代码来使用这个类,就是传统的“创建对象-调用”模式:
ReflectObj obj = new ReflectObj();
obj.setField01("value01");
System.out.println(obj.getField01());
如果按照如下代码来使用它,就是“类反射”模式:
// 直接获取对象实例
ReflectObj obj = ReflectObj.class.newInstance();
// 直接访问Field
Field field = ReflectObj.class.getField("field01");
field.setAccessible(true);
field.set(obj, "value01");
// 调用对象的public函数
Method method = ReflectObj.class.getMethod("getField01");
System.out.println((String) method.invoke(obj));
类反射属于古老而基础的JAVA技术,本文不再赘述。
从上面的代码可以看出:
- 相比较于传统的“创建对象-调用”模式,“类反射”模式的代码更抽象、一般情况下也更加繁琐;
- 类反射绕开了编译器的合法性检测——比如访问了一个不存在的字段、调用了一个不存在或不允许访问的函数,因为编译器设立的防火墙失效了,编译能够通过,但是运行的时候会报错;
- 实际上,如果按照标准模式编写类反射代码,效率明显低于传统模式。在后面的章节会提到这一点。
缘起:为什么使用类反射
前文简略介绍了JAVA类反射技术,在与传统的“创建对象-调用”模式对比时,提到了类反射的几个主要弱点。但是在实际工作中,我们发现类反射无处不在,特别是在一些底层的基础框架中,类反射是应用最为普遍的核心技术之一。最常见的例子:Spring容器。
这是为什么呢?我们不妨从实际工作中的具体案例出发,分析类反射技术的不可替代性。
大家几乎每天都和银行打交道,通过银行进行存款、转帐、取现等金融业务,这些动账操作都是通过银行核心系统(包括交易核心/账务核心/对外支付/超级网银等模块)完成的,因为历史原因造成的技术路径依赖,银行核心系统的报文几乎都是xml格式,而且以这种格式最为普遍:
<?xml version='1.0' encoding='UTF-8'?>
<service>
<sys-header>
<data name="SYS_HEAD">
<struct>
<data name="MODULE_ID">
<field type="string" length="2">RB</field>
</data>
<data name="USER_ID">
<field type="string" length="6">OP0001</field>
</data>
<data name="TRAN_TIMESTAMP">
<field type="string" length="9">003026975</field>
</data>
<!-- 其它字段略过 -->
</struct>
</data>
</sys-header>
<!-- 其它段落略过 -->
<body>
<data name="REF_NO">
<field type="string" length="23">OPS18112400302633661837</field>
</data>
</body>
</service>
和常用的xml格式进行对比:
<?xml version="1.0" encoding="UTF-8"?>
<recipe>
<recipename>Ice Cream Sundae</recipename>
<ingredlist>
<listitem>
<quantity>3</quantity>
<itemdescription>chocolate syrup or chocolate fudge</itemdescription>
</listitem>
<listitem>
<quantity>1</quantity>
<itemdescription>nuts</itemdescription>
</listitem>
<listitem>
<quantity>1</quantity>
<itemdescription>cherry</itemdescription>
</listitem>
</ingredlist>
<preptime>5 minutes</preptime>
</recipe>
银行核心系统的xml报文不是用标签的名字区分元素,而是用属性(name属性)区分,在解析的时候,不管是用DOM、SAX,还是Digester或其它方案,都要用条件判断语句、分支处理,伪代码如下:
// ……
接口类实例 obj = new 接口类();
List<Node> nodeList = 获取xml标签列表
for (Node node: nodeList) {
if (node.getProperty("name") == "张三") obj.set张三 (node.getValue());
else if (node.getProperty("name") == "李四") obj.set李四 (node.getValue());
// ……
}
// ……
显而易见,这样的代码非常粗劣、不优雅,每解析一个接口的报文,都要写一个专门的类或者函数,堆砌大量的条件分支语句,难写、难维护。如果报文结构简单还好,如果有一百个甚至更多的字段,怎么办?毫不夸张,在实际工作中,我遇到过一个银行核心接口有140多个字段的情况,而且这还不是最多的!
试水:优雅地解析XML
当我们碰到这种结构的xml、而且字段还特别多的时候,解决问题的钥匙就是类反射技术,基本思路是:
- 从xml中解析出字段的name和value,以键值对的形式存储起来;
- 用类反射的方法,用键值对的name找到字段或字段对应的setter(这是有规律可循的);
- 然后把value直接set到字段,或者调用setter把值set到字段。
接口类应该是这样的结构:
- nodes是存储字段的name-value键值对的列表,MessageNode就是键值对,结构如下: </