非常重要: 蚂蚁金服年底海量hc,关键竞争压力小,部门核心,千万DAU,6位数QPS,有意向找我内推,可做适当面试指导,机会难得。
刚开始学Java 一般不太会关注到反射,但是如果看很多框架的源码,发现反射无处不在。最近一个业务需求中用了反射,感觉非常丝滑。
前言
鲁班: 什么是反射?
安琪拉: 反射是Java 中提供的运行期获取对象信息的能力。先记住二个关键词:运行期、对象信息。
鲁班: 那为什么Java 需要反射呢? 需要在运行期获取对象信息呢?
安琪拉: 比如你希望调用某个对象的方法,例如下面这段代码:
angela 对象你如果运行期不知道它是否有dance 方法, 可以调用getClass().getMethod("dance")
判断一下。
鲁班: 那岂不是很弱鸡,我可以直接调angela.dance()
方法啊!
安琪拉: 你要用angela.dance()
方法,包里是不是需要 import Angela 类,一定要有确定的Angela 对象,很多框架场景,是不知道目标对象的Class类型的,要动态获取对象的类类型。
鲁班: 我知道了,反射就是运行的时候知道这个对象能不能调某个方法。
安琪拉: 不止如此,反射就是对于任意一个对象,我们能够运行时访问它的方法和属性。
鲁班: 为什么强调运行时?
安琪拉: 因为是编译期,类型是确定的,很多时候在拿不到确定的对象的属性和值的时候,需要运行时动态调用方法或获取属性。后面会介绍一个通用框架能力通过反射实现的sample。
先说 Java 反射API相关的类有下面几个:
这里可以引出一个很有意思的话题,Java 中一切皆对象,那Class 也是对象,另外所有对象都有对应的Class(类),Class(类)就像饼干模板,Object(对象)是根据Class(类) 做出的饼干,那JDK 加载时先有Class 还是先有Object 呢?如何加载 ?这个可以留个思考题。
真实业务场景
鲁班: 那知道反射有什么用? 对我平常写 curd 有帮助吗?
安琪拉: 有几点原因要知道反射,一个是一些框架代码里面会有很多反射,例如, 我们经常接触的动态代理, Spring的自定义注解。另外我们如果希望把从业务层代码抽象出一些平台能力,就可以用反射。
鲁班: 你这么说没有体感,能不能举个例子?
安琪拉: 那你继续说说上次你的需求。
鲁班:你说我最近接到了一个需求啊,要在下路把对方每一波过来的小兵做标注,只有遇到特定的小兵,我才开火。
安琪拉: 那这些小兵有什么特点呢? 你打算怎么精准定位要开火的小兵?
鲁班:如果小兵身上的符文是红色符文(除此以外,还有蓝色符文和紫色符文),法术防御是魔法防御(除此以外,还有物理防御),我只对这些小兵开火,当然咯,可能以后还需要对带各种属性组合的小兵进行开火打击。
安琪拉: 需求我大概清楚了,你有思路了吗?我们先把模型建立起来,如下图所示,是小兵的模型
鲁班:你上次说了,写业务代码的时候要考虑通用性和可扩展性,但是这个功能也能用反射吗?
安琪拉: 我们拆解一下需求,希望对于指定对象,这个对象上具有指定属性值或某些属性值时,我们做一些后置业务处理。这个是我们做的业务逻辑抽象。
安琪拉: 我们列一下有几个变量: 对象不确定、提取的属性不确定、 提取属性的个数不确定、属性值不确定,最后是要做的后续业务处理逻辑不确定。怎么把模型做的足够通用呢? 我们来设计一下。
鲁班:但是产品给我这个需求就是判断小兵对象的符文和防御属性值啊?
安琪拉: 如果只是按照产品的需求搞,以后有的改,所以索性一次把模型设计的通用。我们可以这个搞:
安琪拉: 我们抽象后可以把这个服务叫做定位服务,如上所示,我们希望无论是什么对象,可以判断对象的指定属性值和预期值是否一致。这里用反射获取到属性的get 方法,然后调用get 方法获取属性值,和预期值做比较,这里 getReadMethod 方法为了方便说明做了简化,很多情况没写进入,比如属性是boolean 类型,get方法前缀是is,比如是父类或接口的方法等等。
鲁班:这样写有什么好处呢?
安琪拉: 这样就把原来的只对Batman 对象的属性做判断做了一层抽象,这样以后类似的需求都可以满足了。我们来做一下对比:
鲁班:这二个方案都是判断 batman(小兵)身上带的 rune(符文)是不是红色,如果是红色,就开火。但是新方案用了反射,有什么优势吗?
安琪拉: 实际业务场景里面,规则往往比这个复杂很多,而且还会一直变化,怎么把方案做的通用性和可扩展性更新,同时性能损耗减少到最少使我们要考虑的问题。例如:产品经理跟你说,这次除了对batman(小兵)身上带的 rune(符文)一定是红色开火,条件还要加一条必须盔甲是防法术伤害的才开火,或者是二者满足其中一条就开火,除了batman(小兵)做判断,也要对野怪、对方英雄做属性值判断。
鲁班:你的意思是业务需求这么变,我用反射做了通用性功能,可以不需要重复写代码吗?
安琪拉: 对呀。到时候你可以抽更多时间来研究🔫的技能。
鲁班:用反射可以实现对不同对象做业务逻辑处理,我可以理解,但是你刚才说的那些条件之前的业务规则,比如同时满足,二者满足其一就可以怎么能做到复用呢?
安琪拉: 你可以建一个规则表,一个条件表,规则表中有规则的场景、规则关联的条件(可以多个),条件之前的关系。条件的关系你可以设计的灵活一些,支持四类:
simple 简单条件,满足一个属性值就符合and 多个条件都要满足or 多个条件满足其中一个expression 表达式,如果上面都满足不了,你还可以支持自定义表达式,例如: (A & B) | (C &D)
以上面的需求举例,条件表存二条记录,分别是
propertyKey=“rune” , propertyValue= “red”, conditionName="****", conditionId="**"propertyKey=“armor” , propertyValue= “magic-defend”, conditionName="****", conditionId="**"
规则定为:
条件组: 条件1,条件2条件关系: and (代表条件都要满足)
然后你的产品经理需求变更了,你只需要新增规则和条件,或者修改规则表的记录就可以了。
鲁班:既然都说到这个份上了,能给写段代码吗? 干说不练假把式
安琪拉: 好的,如下图代码所示,这套带有规则的反射可以应付来自产品各种花样需求了。
鲁班:那规则表和条件表我都建在数据库吗?
安琪拉: 有配置中心,可以把表建在配置中心,本地做份缓存, 没有放在数据库也可以,做好一致性。
鲁班:并发量非常高的时候,反射不会影响程序的性能吗? 听说反射很耗性能。
安琪拉: 反射的确对性能有损耗,但你知道反射为什么影响性能吗? 性能主要损耗在哪里?
鲁班: 不知道。
反射性能问题
安琪拉: 反射影响性能是因为运行时,程序需要动态解析的类型,例如Class.getDeclaredMethod
的时候方法方法的类型都是运行时检查,Java虚拟机也没办法优化,每次Method 执行都要从Class 类信息中加载,我们知道类的方法信息是放在单独的方法区的,对象在堆区,但是相比于反射带来的便利,如果不是高并发需要十分频繁的调用,反射的性能损耗可以忽略,并且反射性能损耗也有方法优化降低。
鲁班:怎么优化反射的性能损耗?
安琪拉: 例如,我们前面每次调 locateObject 时都需要查找Method,我们可以把第一次查找的 Method 缓存起来,下次就不需要再调Class.getDeclaredMethod
了。我们可以看下Class.getDeclaredMethod
内部处理逻辑,是比较耗性能的。
下图是截取的一段源码:
鲁班:那如果我要用反射,这个性能问题需要我自己做缓存吗?
安琪拉: 其实已经有现成的框架想到了反射的性能问题,因此可以直接用就好了。
鲁班:啊。。。在哪里?
安琪拉: 就是大名鼎鼎的 springframework 的 BeanUtils,我们上面的那段自己实现的获取Method的代码可以改写成如下这样:
在BeanUtils中实现了Method 的缓存。
我们对反射做一个简单的性能测试, 对反射代码执行100万次,打印耗时, 同时看Cpu、堆内存和非堆内存占用情况:
性能指标截图如下:
CPU、内存反射和常规内存占用基本差别不到,100万次耗时多290ms左右,
100万次反射调用:
299ms,多次执行,在上下浮动。因此使用Spring framework提供的BeanUtils 包,反射性能影响很少。
在阿里巴巴开发规约有一条
【强制】避免用Apache Beanutils进行属性的copy。
说明:Apache BeanUtils性能较差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。
反例:[性能提升300%:Apache的BeanUtils的坑]
Apache BeanUtils 在类似copyProperties 方法实现机制上和Spring BeanUtils 略有不同,Apache BeanUtils 拷贝机制做了各种转换和解析逻辑, 导致性能变差,大家使用的时候注意区分。
关注Wx公众号:【安琪拉的博客】—揭秘Java后端技术,还原技术背后的本质
系列文章
一个HashMap能跟面试官扯上半个小时
一个synchronized跟面试官扯了半个小时