一次性能提升30倍的JAVA类反射性能优化实践html
文章来源:宜信技术学院 & 宜信支付结算团队技术分享第4期-支付结算部支付研发团队高级工程师陶红《JAVA类反射技术&优化》java
分享者:宜信支付结算部支付研发团队高级工程师陶红node
原文首发于宜信支付结算技术团队公号:野指针程序员
在实际工做中的一些特定应用场景下,JAVA类反射是常常用到、必不可少的技术,在项目研发过程当中,咱们也遇到了不起不运用JAVA类反射技术的业务需求,而且不可避免地面临这个技术固有的性能瓶颈问题。编程
经过近两年的研究、尝试和验证,咱们总结出一套利用缓存机制、大幅度提升JAVA类反射代码运行效率的方法,和没有优化的代码相比,性能提升了20~30倍。本文将与你们分享在探索和解决这个问题的过程当中的一些有价值的心得体会与实践经验。缓存
首先,用最简短的篇幅介绍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、并且字段还特别多的时候,解决问题的钥匙就是类反射技术,基本思路是:
接口类应该是这样的结构:
public class MessageNode { private String name; private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public MessageNode() { super(); } }
这样,解析xml的代码能够变得很是优雅、简洁。若是用Digester解析以前列举的那种格式的银行报文,能够这样写:
Digester digester = new Digester(); digester.setValidating(false); digester.addObjectCreate("service/sys-header", SysHeader.class); digester.addCallMethod("service/sys-header/data/struct/data", "createNode", 2); digester.addCallParam("service/sys-header/data/struct/data", 0, "name"); digester.addCallParam("service/sys-header/data/struct/data/field", 1); parseObj = (SysHeader) digester.parse(new StringReader(msg)); parseObj.initialize();
initialize函数的代码,能够写在一个基类里面,子类继承基类便可。具体代码以下:
public void initialize() { for (MessageNode node: nodes) { try { /** * 直接获取字段、而后设置字段值 */ //String fieldName = StringUtils.camelCaseConvert(node.getName()); // 只获取调用者本身的field(private/protected/public修饰词皆可) //Field field = this.getClass().getDeclaredField(fieldName); // 获取调用者本身的field(private/protected/public修饰词皆可)和从父类继承的field(必须是public修饰词) //Field field = this.getClass().getField(fieldName); // 把field设为可写 //field.setAccessible(true); // 直接设置field的值 //field.set(this, node.getValue()); /** * 经过setter设置字段值 */ Method method = this.getSetter(node.getName()); // 调用setter method.invoke(this, node.getValue()); } catch (Exception e) { log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e); }; } }
上面被注释的段落是直接访问Field的方式,下面的段落是调用setter的方式,两种方法在效率上没有差异。
考虑到JAVA语法规范(书写bean的规范),调用setter是更通用的办法,由于接口类多是被继承、派生的,子类没法访问父类用private关键字修饰的Field。
getSetter函数很简单,就是用Field的名字反推setter的名字,而后用类反射的办法获取setter。代码以下:
private Method getSetter(String fieldName) throws NoSuchMethodException, SecurityException { String methodName = String.format("set%s", StringUtils.upperFirstChar(fieldName)); // 获取field的setter,只要是用public修饰的setter、无论是本身的仍是从父类继承的,都能取到 return this.getClass().getMethod(methodName, String.class); }
若是设计得好,甚至能够用一个解析函数处理全部的接口,这涉及到Digerser的运用技巧和接口类的设计技巧,本文不做深刻讲解。
2017年,咱们在一个和银行有关的金融增值服务项目中使用了这个解决方案,取得了很是不错的效果,以后在公司内部推广开来成为了通用技术架构。通过一年多的实践,证实这套架构性能稳定、可靠,极大地简化了代码编写和维护工做,显著提升了生产效率。
可是,随着业务量的增长,2018年底在进行压力测试的时候,发现解析xml的代码占用CPU资源居高不下。进一步分析、定位,发现问题出在类反射代码上,在某些极端的业务场景下,甚至会占用90%的CPU资源!这就提出了性能优化的迫切要求。
类反射的性能优化不是什么新课题,所以有一些成熟的第三方解决方案能够参考,好比运用比较普遍的ReflectASM,据称能够比未经优化的类反射代码提升1/3左右的性能。
(参考资料:Java高性能反射工具包ReflectASM,ReflectASM-invoke,高效率java反射机制原理)
在研究了ReflectASM的源代码之后,咱们决定不使用现成的第三方解决方案,而是从底层入手、自行解决类反射代码的优化问题。主要基于两点考虑:
前面提到ReflectASM给类的字段、函数创建索引,借此提升类反射效率。进一步分析,这其实是变相地缓存了字段和函数。那么,在咱们面临的业务场景下,能不能用缓存的方式优化类反射代码的效率呢?
咱们的业务场景须要以类反射的方式频繁调用接口类的setter,这些setter都是用public关键字修饰的函数,先是getMethod()、而后invoke()。基于以上特色,咱们用以下逻辑和流程进行了技术分析:
A.类空间/对象空间维度
B.堆/栈维度
把接口类修改成这样的结构(标红的部分是新增或修改):
setterMap就是缓存字段setter的HashMap。为何是两层嵌套结构呢?由于这个Map是写在基类里面的静态变量,每一个从基类派生出的接口类都用它缓存setter,因此第一层要区分不一样的接口类,第二层要区分不一样的字段。以下图所示:
当ClassLoader加载基类时,建立setterMap(内容为空):
static { setterMap = new HashMap<String, Map<String, Method>>(); }
这样写能够保证setterMap只被初始化一次。
Initialize()函数做以下改进:
public void initialize() { // 先检查子类的setter是否被缓存 String className = this.getClass().getName(); if (setterMap.get(className) == null) setterMap.put(className, new HashMap<String, Method>()); Map<String, Method> setters = setterMap.get(className); // 遍历报文节点 for (MessageNode node: nodes) { try { // 检查对应的setter是否被缓存了 Method method = setters.get(node.getName()); if (method == null) { // 没有缓存,先获取、再缓存 method = this.getSetter(node.getName()); setters.put(node.getName(), method); } // 用类反射方式调用setter method.invoke(this, node.getValue()); } catch (Exception e) { log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e); }; } }
基本思路就是把setter缓存起来,经过MessageNode的name(字段的名字)找setter的入口地址,而后调用。
由于只在初始化第一个对象实例的时候调用getMethod(),极大地节约了系统资源、提升了效率,测试结果也证明了这一点。
1)先写一个测试类,结构以下:
2)在构造函数中,用UUID初始化存储键值对的列表nodes:
this.createNode("test001", String.valueOf(UUID.randomUUID().toString().hashCode())); this.createNode("test002", String.valueOf(UUID.randomUUID().toString().hashCode())); // ……
之因此用UUID,是保证每一个实例、每一个字段的值都不同,避免JAVA编译器自动优化代码而破坏测试结果的原始性。
3)Initialize_ori()函数是用传统的硬编码方式直接调用setter的方法初始化实例字段,代码以下:
for (MessageNode node: this.nodes) { if (node.getName().equalsIgnoreCase("test001")) this.setTest001(node.getValue()); else if (node.getName().equalsIgnoreCase("test002")) this.setTest002(node.getValue()); // …… }
优化效果就以它做为对照标准1,对照标准2就是没有优化的类反射代码。
4)checkUnifomity()函数用来验证:代码是否用name-value键值对正确地初始化了各字段。
for (MessageNode node: nodes) { if (node.getName().equalsIgnoreCase("test001") && !node.getValue().equals(this.test001)) return false; else if (node.getName().equalsIgnoreCase("test002") && !node.getValue().equals(this.test002)) return false; // …… } return true;
每一种优化方案,咱们都会用它验证明例的字段是否正确,只要出现一次错误,该方案就会被否认。
5)建立100万个TestInvoke类的实例,而后循环调用每个实例的initialize_ori()函数(传统的硬编码,非类反射方法),记录执行耗时(只记录初始化耗时,建立实例的耗时不记录);再建立100万个实例,循环调用每个实例的类反射初始化函数(未优化),记录执行耗时;再建立100万个实例,改为调用优化后的类反射初始化函数,记录执行耗时。
6)以上是一个测试循环,获得三种方法的耗时数据,重复作10次,获得三组耗时数据,把记录下的数据去掉最大、最小值,剩下的求平均值,就是该方法的平均耗时。某一种方法的平均耗时越短则认为该方法的效率越高。
7)为了进一步验证三种方法在不一样负载下的效率变化规律,改为建立10万个实例,重复5/6两步,获得另外一组测试数据。
测试结果显示:在确保测试环境稳定、一致的前提下,8个字段的测试实例、初始化100万个对象,传统方法(硬编码)耗时850~1000毫秒;没有优化的类反射方法耗时23000~25000毫秒;优化后的类反射代码耗时600~800毫秒。10万个测试对象的状况,三种方法的耗时也大体是这样的比例关系。这个数据取决于测试环境的资源情况,不一样的机器、不一样时刻的测试,结果都有出入,但总的规律是稳定的。
基于测试结果,能够得出这样的结论:缓存优化的类反射代码比没有优化的代码效率提升30倍左右,比传统的硬编码方法提升了10~20%。有必要强调的是,这个结论偏向保守。和ReflecASM相比,性能大幅度提升也是毋庸置疑的。
缓存优化的效果很是好,可是,这个方案真的天衣无缝了么?
通过分析,咱们发现:若是数据更复杂一些,这个方案的缺陷就暴露了。好比键值对列表里的值在接口类里面并无定义对应的字段,或者是没有对应的、能够访问的setter,性能就会明显降低。
这种状况在实际业务中是很常见的,好比对接银行核心接口,每每并不须要解析报文的所有字段,不少字段是能够忽略的,因此接口类里面不用定义这些字段,但解析代码依然会把这些键值对所有解析出来,这时就会给优化代码形成麻烦了。
分析过程以下:
1)举例而言,若是键值对里有两个值在接口类(Interface01)并未定义,假定名字是fieldX、filedY,第一次执行initialize()函数:
初始状态下,setterMap检索不到Interface01类的setter缓存,initialize()函数会在第一次执行的时候,根据键值对的名字(field01/field02/……/fieldN/fieldX/fieldY)调用getMethod()函数、初始化sertter引用的缓存。由于fieldX和fieldY字段不存在,找不到它们对应的setter,缓存里也没有它们的引用。
2)第二次执行initialize()函数(也就是初始化第二个对象实例),field01/field02/……/fieldN键值对都能在缓存中找到setter的引用,调用速度很快;但缓存里找不到fieldX/fieldY的setter的引用,因而再次调用getMethod()函数,而由于它们的setter根本不存在(连这两个字段都不存在),作的是无用功,setterMap的状态没有变化。
3)第三次、第四次……第N次,都是如此,白白消耗系统资源,运行效率必然降低。
测试结果印证了这个推断:在TestInvoke的构造函数增长了两个不存在对应字段和setter的键值对(姑且称之为“无效键值对”),进行100万个实例的初始化测试,通过优化的类反射代码,耗时从原来的600~800毫秒,增长到7000~8000毫秒,性能降低10倍左右。若是增长更多的键值对(不存在对应字段),性能降低更严重。
因此必须进一步完善优化代码。为了加以区分,咱们把以前的优化代码称为V1版;进一步完善的代码称为V2版。
怎么完善?从上面的分析不难找到思路:增长忽略字段(ignore field)缓存。
基类BaseModel做以下修改(标红部分是新增或者修改),增长了ignoreMap:
ignoreMap的数据结构相似于setterMap,但第二层不是HashMap,而是Set,缓存每一个子类须要忽略的键值对的名字,使用Set更节约系统资源,以下图所示:
一样的,当ClassLoader加载基类的时候,建立ignoreMap(内容为空):
static { setterMap = new HashMap<String, Map<String, Method>>(); ignoreMap = new HashMap<String, Set<String>>(); }
Initialize()函数做以下改进:
public void initialize() { // 先检查子类的setter是否被缓存 String className = this.getClass().getName(); if (setterMap.get(className) == null) setterMap.put(className, new HashMap<String, Method>()); if (ignoreMap.get(className) == null) ignoreMap.put(className, new HashSet<String>()); Map<String, Method> setters = setterMap.get(className); Set<String> ignores = ignoreMap.get(className); // 遍历报文节点 for (MessageNode node: nodes) { String sName = node.getName(); try { // 检查该字段是否被忽略 if (ignores.contains(sName)) continue; // 检查对应的setter是否被缓存了 Method method = setters.get(sName); if (method == null) { // 没有缓存,先获取、再缓存 method = this.getSetter(sName); setters.put(sName, method); } // 用类反射方式调用setter method.invoke(this, node.getValue()); } catch (NoSuchMethodException | SecurityException e) { log.debug("It's failed to initialize field: {}, reason: {}", sName, e); // 找不到对应的setter,放到忽略字段集合,之后再也不尝试 ignores.add(sName); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("It's failed to initialize field: {}, reason: {}", sName, e); try { // 不能调用setter,多是虚拟机回收了该子类的所有实例、入口地址变化,更新地址、再试一次 Method method = this.getSetter(sName); setters.put(sName, method); method.invoke(this, node.getValue()); } catch (Exception e1) { log.debug("It's failed to initialize field: {}, reason: {}", sName, e1); } } catch (Exception e) { log.error("It's failed to initialize field: {}, reason: {}", sName, e); } } }
虽然代码复杂了一些,但思路很简单:用键值对的名字寻找对应的setter时,若是找不到,就把它放进ignoreMap,下次再也不找了。另外还增长了对setter引用失效的处理。虽然理论上说“只要虚拟机不重启,setter的入口引用永远不会变”,在测试中也历来没有遇到过这种状况,但为了覆盖各类异常状况,仍是增长了这段代码。
继续沿用前面的例子,分析改进后的代码的工做流程:
1)第一次执行initialize()函数,实例的状态是这样变化的:
由于fieldX和fieldY字段不存在,找不到它们对应的setter,它们被放到ignoreMap中。
2)再次调用initialize()函数的时候,由于检查到ignoreMap中存在fieldX和fieldY,这两个键值对被跳过,再也不徒劳无功地调用getMethod();其它逻辑和V1版相同,没有变化。
仍是用上面提到的TestInvoke类做验证(8个字段+2个无效键值对),V2版本虽然代码更复杂了,但100万条纪录的初始化耗时为600~800毫秒,V1版代码这个时候的耗时猛增到7000~8000毫秒。哪怕增长更多的无效键值对,V2版代码耗时增长也不明显,而这种状况下V1版代码的效率还会进一步降低。
至此,对JAVA类反射代码的优化已经比较完善,覆盖了各类异常状况,如前所述,咱们把这个版本称为V2版。
这样就表明优化工做已经作到最好了吗?不是这样的。
仔细观察V一、V2版的优化代码,都是循环遍历键值对,用键值对的name(和字段的名字相同)推算setter的函数名,而后去寻找setter的入口引用。第一次是调用类反射的getMethod()函数,之后是从缓存里面检索,若是存在无效键值对,那就必然出现空转循环,哪怕是V2版代码,ignoreMap也不能避免这种空转循环。虽然单次空转循环耗时很是短,但在无效键值对比较多、负载很大的状况下,依然有无效的资源开销。
若是采用逆向思惟,用setter去反推、检索键值对,又会如何?
先分析业务场景以及由业务场景所决定的数据结构特色:
综上所述,逆向思惟用setter函数反推、检索键值对,初始化接口类,就是第二次迭代的具体方向。
须要把接口类修改为这样的结构(标红的部分是新增或者修改):
1)为了便于逆向检索键值对,nodes字段改为HashMap,key是键值对的名字、value是键值对的值。
2)为了提升循环遍历的速度,setterMap的第二层改为链表,链表的成员是内部类FieldSetter,结构以下:
private class FieldSetter { private String name; private Method method; public String getName() { return name; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } public FieldSetter(String name, Method method) { super(); this.name = name; this.method = method; } }
setterMap的第二层继续使用HashMap也能实现功能,但循环遍历的效率,HashMap不如链表,因此咱们改用链表。
3)一样的,setterMap在基类被加载的时候建立(内容为空):
static { setterMap = new HashMap<String, List<FieldSetter>>(); }
4)第一次初始化某个接口类的实例时,调用initSetters()函数,初始化setterMap:
protected List<FieldSetter> initSetters() { String className = this.getClass().getName(); List<FieldSetter> setters = new ArrayList<FieldSetter>(); // 遍历类的可调用函数 for (Method method: this.getClass().getMethods()) { String methodName = method.getName(); // 若是从名字推断是setter函数,添加到setter函数列表 if (methodName.startsWith("set")) { // 反推field的名字 String fieldName = StringUtils.lowerFirstChar(methodName.substring(3)); setters.add(new FieldSetter(fieldName, method)); } } // 缓存类的setter函数列表 setterMap.put(className, setters); // 返回可调用的setter函数列表 return setters; }
5)Initialize()函数修改成以下逻辑:
public void initialize() { // 从缓存获取接口类的setter列表 List<FieldSetter> setters = setterMap.get(this.getClass().getName()); // 若是尚未缓存、初始化接口类的setter列表 if (setters == null) setters = this.initSetters(); // 遍历接口类的setter for (FieldSetter setter: setters) { // 用setter的名字(也就是字段的名字)检索键值对 String fieldName = setter.getName(); String fieldValue = nodes.get(fieldName); // 没有检索到键值对、或者键值对没有赋值,跳过 if (StringUtils.isEmpty(fieldValue)) continue; try { Method method = setter.getMethod(); // 用类反射方式调用setter method.invoke(this, fieldValue); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("It's failed to initialize field: {}, reason: {}", fieldName, e); // 不能调用setter,多是虚拟机回收了该子类的所有实例、入口地址变化,更新地址、再试一次 try { Method method = this.getSetter(fieldName); setter.setMethod(method); method.invoke(this, fieldValue); } catch (Exception e1) { log.debug("It's failed to initialize field: {}, reason: {}", fieldName, e1); } } catch (Exception e) { log.error("It's failed to initialize field: {}, reason: {}", fieldName, e); } } }
不妨把这版代码称为V3……继续沿用前面TestInvoke的例子,分析改进后代码的工做流程:
1)第一次执行initialize()函数,实例的状态是这样变化的:
经过setterMap反向检索键值对的值,fieldX、fieldY由于不存在对应的setter,不会被检索,避免了空转。
2)以后每一次初始化对象实例,都不须要再初始化setterMap,也不会消耗任何资源去检索fieldX、fieldY,最大限度地节省资源开销。
3)由于取消了ignoreMap,取消了V2版判断字段是否应该被忽略的逻辑,代码更简洁,也能节约一部分资源。
结果数据显示:用TestInvoke测试类、8个setter+2个无效键值对的状况下,进行100万/10万个实例两个量级的对比测试,V3版比V2版性能最多提升10%左右,100万实例初始化耗时550~720毫秒。若是增长无效键值对的数量,性能提升更为明显;没有无效键值对的最理想状况下,V一、V二、V3版本的代码效率没有明显差异。
至此,用缓存机制优化类反射代码的尝试,已经比较接近最优解了,V3版本的代码能够视为到目前为止最好的版本。
总结过去两年围绕着JAVA类反射性能优化这个课题,咱们所进行的探索和研究,提升到方法论层面,能够提炼出一个分析问题、解决问题的思路和流程,供你们参考:
1)从实践中来
多数状况下,探索和研究的课题并非坐在书斋里凭空想出来的,而是在实际工做中遇到具体的技术难点,在现实需求的驱动下发现须要研究的问题。
以本文为例,若是不是在对接银行核心系统的时候遇到了大量的、格式奇特的xml报文,不会促使咱们尝试用类反射技术去优雅地解析报文,也就不会面对类反射代码执行效率低的问题,天然不会有后续的研究成果。
2)拿出手术刀,解剖一只麻雀
在实践中遇到了困难,首先要分析和研究面对的问题,不能着急,要有解剖一只麻雀的精神,抽丝剥茧,把问题的根源找出来。
这个过程当中,逻辑分析和实操验证都是必不可少的。没有高屋建瓴的分析,就容易迷失大方向;没有实操验证,大几率会陷入坐而论道、脑补的怪圈。仍是那句话:实践是最宝贵的财富,也是验证一切构想的终极考官,是咱们认识世界改造世界的力量源泉。但咱们也不能陷入庸俗的经验主义,无论怎么说,这个世界的基石是有逻辑的。
回到本文的案例,咱们一方面研究JAVA内存模型,从理论上探寻类反射代码效率低下的缘由;另外一方面也在实务层面,用实实在在的时间戳验证了JAVA类反射代码的耗时分布。理论和实践的结合,才能让咱们找到解决问题的正确方向,两者不可偏废。
3)头脑风暴,敢于创新
分析问题,找到关键点,接下来就是寻找解决方案。JAVA程序员有一个很大的优点,同时也是很大的劣势:第三方解决方案很是丰富。JAVA生态比较完善,咱们面临的麻烦和问题几乎都有成熟的第三方解决方案,“吃现成的”是优点也是劣势,不少时候,咱们的创造力也所以被扼杀。因此,当面临高价值需求的时候,应该拿出大无畏的勇气,啃硬骨头,作底层和原创的工做。
就本文案例而言,ReflexASM就是看起来很不错的方案,比传统的类反射代码性能提高了至少三分之一。可是,它真的就是最优解么?咱们的实践否认了这一点。JAVA程序员要有吃苦耐劳、以底层技术为原点解决问题的精神,不然你就会被别人所绑架,失去寻求技术自由空间的机会。中国的软件行业已经发展到了这个阶段,提出了这样的需求,咱们应该顺应历史潮流。
4)螺旋式发展,波浪式前进
研究问题和解决问题,迭代是很是有效的工做方法。首先,要有精益求精的态度,不断改进,逼近最优方案,迭代必不可少。其次,对于比较复杂的问题,不要追求毕其功于一役,把一个大的目标拆分红不一样阶段,分步实施、逐渐推动,这种状况下,迭代更是解决问题的必由之路。
咱们解决JAVA类反射代码的优化问题,就是通过两次迭代、写了三个版本,才获得最终的结果,逼近了最优解。在迭代的过程当中会逐渐发现一些以前忽略的问题,这就是宝贵的经验,这些经验在解决其余技术问题时也能发挥做用。好比HashMap的数据结构很是合理、经典,平时使用的时候效率是很高的,若是不是迭代开发、逼近极限的过程,咱们又怎么可能发如今循环遍历状态下、它的性能不如链表呢?
行文至此,文章也快要写完了,细心的读者必定会有一个疑问:自始至终,举的例子、类的字段都是String类型,类反射代码根本没有考虑setter的参数类型不一样的状况。确实是这样的,由于咱们解决的是银行核心接口报文解析的问题,接口字段所有是String,没有其它数据类型。
其实,对类反射技术的研究深刻到这个程度,解决这个问题、而且维持代码的高效率,易如反掌。好比,给FieldSetter类增长一个数据类型的字段,初始化setterMap的时候把接口类对应的字段的数据类型解析出来,和setter函数的入口一块儿缓存,类反射调用setter时,把参数格式转换一下,就能够了。限于篇幅、这个问题就不展开了,感兴趣的读者能够本身尝试一下。