一文带你彻底了解缓存机制实现JAVA类反射性能提升30倍技术!

本文深入探讨了JAVA类反射技术,揭示了其性能瓶颈并提出缓存优化方案。通过缓存setter方法,优化后的类反射代码性能提升了约30倍。文章详细介绍了优化过程,包括问题的起源、思路、实践和多次迭代,展示了如何通过缓存和忽略字段处理提高反射效率。
摘要由CSDN通过智能技术生成

个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 

简述: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到字段。

接口类应该是这样的结构:

1573612066629020939.pnguploading.4e448015.gif转存失败重新上传取消1573612066629020939.pnguploading.4e448015.gif转存失败重新上传取消1573612066629020939.pnguploading.4e448015.gif转存失败重新上传取消

1573612072659090104.pnguploading.4e448015.gif转存失败重新上传取消1573612072659090104.pnguploading.4e448015.gif转存失败重新上传取消1573612072659090104.pnguploading.4e448015.gif转存失败重新上传取消

  • nodes是存储字段的name-value键值对的列表,MessageNode就是键值对,结构如下:
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();
	}
}
  • createNode是在解析xml的时候,把键值对添加到列表的函数;
  • initialize是用类反射方法,根据键值对初始化每个字段的函数。

这样,解析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是更通用的办法,

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值