三思笔记_使用反射前先三思
三思筆記
介紹
有時(shí),作為開發(fā)人員,您可能會(huì)遇到無法使用new運(yùn)算符實(shí)例化對(duì)象的情況,因?yàn)槠漕惷Q存儲(chǔ)在配置XML中的某個(gè)位置,或者您需要調(diào)用一個(gè)名稱指定為注釋屬性的方法。 在這種情況下,您總會(huì)有一個(gè)答案:“使用反射!”。
在新版本的CUBA框架中 ,我們決定改進(jìn)體系結(jié)構(gòu)的許多方面,最重要的變化之一是在控制器UI中棄用了“經(jīng)典”事件偵聽器。 在該框架的先前版本中,屏幕的init()方法中注冊(cè)了許多樣板代碼的偵聽器,使您的代碼幾乎不可讀,因此新概念應(yīng)該可以解決此問題。
您始終可以通過為帶注釋的方法存儲(chǔ)java.lang.reflect.Method實(shí)例來實(shí)現(xiàn)方法偵聽器,并像在許多框架中實(shí)現(xiàn)的那樣調(diào)用它們,但是我們決定看看其他選項(xiàng)。 反射調(diào)用需要付出一定的成本,如果您開發(fā)了生產(chǎn)級(jí)框架,則即使是很小的改進(jìn)也可能在短時(shí)間內(nèi)得到回報(bào)。
在本文中,我們將介紹反射API的用法,優(yōu)缺點(diǎn),并查看其他替代反射API調(diào)用的選項(xiàng)-AOT和代碼生成以及LambdaMetafactory。
反射–良好的舊可靠API
根據(jù)維基百科,“反射是計(jì)算機(jī)程序在運(yùn)行時(shí)檢查,自省和修改其自身的結(jié)構(gòu)和行為的能力”。
對(duì)于大多數(shù)Java開發(fā)人員而言,反射并不是新事物,它在許多情況下都被使用。 我敢說Java在沒有反思的情況下不會(huì)變成現(xiàn)在的樣子。 只需考慮批注處理,數(shù)據(jù)序列化,通過批注或配置文件進(jìn)行方法綁定…對(duì)于最流行的IoC框架,由于廣泛使用類代理,方法引用等,反射API是基石。此外,您還可以添加面向方面的編程到此列表–一些AOP框架依靠反射來進(jìn)行方法執(zhí)行攔截。
反射有什么問題嗎? 我們可以考慮其中的三個(gè):
速度 –反射呼叫比直接呼叫慢。 我們可以看到,隨著每個(gè)JVM版本的發(fā)布,反射API的性能都有了很大的提高,JIT編譯器的優(yōu)化算法越來越好,但是反射方法的調(diào)用速度仍然比直接調(diào)用慢3倍。
類型安全性 –如果您在代碼中使用方法引用,則它只是方法引用。 如果編寫的代碼通過其引用調(diào)用方法并傳遞錯(cuò)誤的參數(shù),則該調(diào)用將在運(yùn)行時(shí)失敗,而不是在編譯時(shí)或加載時(shí)失敗。
可追溯性 –如果反射方法調(diào)用失敗,則可能很難找到導(dǎo)致這一問題的代碼行,因?yàn)槎褩8櫷ǔ:荦嫶蟆?您需要深入研究所有這些invoke()和proxy()調(diào)用。
但是,如果您研究Spring中的事件偵聽器實(shí)現(xiàn)或Hibernate中的JPA回調(diào),則會(huì)在其中看到熟悉的java.lang.reflect.Method引用。 而且我懷疑它是否會(huì)在不久的將來更改–成熟的框架又大又復(fù)雜,用在許多關(guān)鍵任務(wù)系統(tǒng)中,因此開發(fā)人員應(yīng)該謹(jǐn)慎地進(jìn)行重大更改。
讓我們看看其他選項(xiàng)。
AOT編譯和代碼生成–使應(yīng)用程序再次快速
反射替換的第一個(gè)候選人–代碼生成。 如今,我們可以看到諸如Micronaut和Quarkus之類的新框架的興起,它們針對(duì)兩個(gè)目標(biāo):快速啟動(dòng)時(shí)間和低內(nèi)存占用。 在微服務(wù)和無服務(wù)器應(yīng)用程序時(shí)代,這兩個(gè)指標(biāo)至關(guān)重要。 最近的框架正試圖通過使用提前編譯和代碼生成來完全擺脫反思。 通過使用注釋處理,類型訪問者和其他技術(shù),他們將直接方法調(diào)用,對(duì)象實(shí)例化等添加到代碼中,從而使應(yīng)用程序更快。 那些不使用Class.newInstance()在啟動(dòng)期間創(chuàng)建和注入bean,不在偵聽器中使用反射方法調(diào)用,等等。這看起來很有希望,但是這里有什么取舍嗎? 答案是–是的。
第一個(gè)–您運(yùn)行的代碼不完全是您自己的。 代碼生成會(huì)更改您的原始代碼,因此,如果出現(xiàn)問題,您將無法確定是您的錯(cuò)誤還是代碼處理算法中的故障。 并且不要忘記,現(xiàn)在您應(yīng)該調(diào)試生成的代碼,而不是代碼。
第二個(gè)權(quán)衡–您必須使用供應(yīng)商提供的單獨(dú)工具/插件才能使用該框架。 您不能“只是”運(yùn)行代碼,而應(yīng)以特殊方式對(duì)其進(jìn)行預(yù)處理。 并且,如果您在生產(chǎn)中使用框架,則應(yīng)將供應(yīng)商的錯(cuò)誤修正應(yīng)用于框架代碼庫(kù)和代碼處理工具。
代碼生成早已為人所知,但Micronaut或Quarkus卻沒有出現(xiàn)。 例如,在CUBA中,我們使用自定義Grails插件和Javassist庫(kù)在編譯期間使用類增強(qiáng)。 我們添加了額外的代碼來生成實(shí)體更新事件,并將bean驗(yàn)證消息作為String字段包含在類代碼中,以用于漂亮的UI表示形式。
但是為事件偵聽器實(shí)現(xiàn)代碼生成看起來有些極端,因?yàn)檫@將需要對(duì)內(nèi)部體系結(jié)構(gòu)進(jìn)行徹底的更改。 有反射這樣的東西,但是更快嗎?
LambdaMetafactory –更快的方法調(diào)用
在Java 7中,引入了新的JVM指令invokedynamic 。 最初針對(duì)基于JVM的動(dòng)態(tài)語(yǔ)言實(shí)現(xiàn),它已成為API調(diào)用的良好替代。 該API可以使我們?cè)谛阅苌蟽?yōu)于傳統(tǒng)反射。 還有一些特殊的類可以在Java代碼中構(gòu)造invokedynamic調(diào)用:
- MethodHandle –此類是Java 7中引入的,但仍不為人所知。
- LambdaMetafactory –在Java 8中引入。它是動(dòng)態(tài)調(diào)用概念的進(jìn)一步發(fā)展。 該API基于MethodHandle。
方法句柄API可以很好地替代標(biāo)準(zhǔn)反射,因?yàn)镴VM僅在MethodHandle創(chuàng)建期間執(zhí)行一次所有預(yù)調(diào)用檢查。 長(zhǎng)話短說–方法句柄是對(duì)基礎(chǔ)方法,構(gòu)造函數(shù),字段或類似的低級(jí)操作的類型化,直接可執(zhí)行的引用,具有參數(shù)或返回值的可選轉(zhuǎn)換。
令人驚訝的是,除非您按照本電子郵件列表中的方法將MethodHandle引用設(shè)為靜態(tài),否則與反射API相比,純MethodHandle引用調(diào)用不會(huì)提供更好的性能。
但是LambdaMetafactory是另一回事–它允許我們?cè)谶\(yùn)行時(shí)中生成功能接口的實(shí)例,該實(shí)例包含對(duì)由MethodHandle解析的方法的MethodHandle 。 使用此lambda對(duì)象,我們可以直接調(diào)用引用的方法。 這是一個(gè)例子:
private BiConsumer createVoidHandlerLambda(Object bean, Method method) throws Throwable { MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "accept" , MethodType.methodType(BiConsumer. class ), MethodType.methodType( void . class , Object. class , Object. class ), caller.findVirtual(bean.getClass(), method.getName(), MethodType.methodType( void . class , method.getParameterTypes()[ 0 ])), MethodType.methodType( void . class , bean.getClass(), method.getParameterTypes()[ 0 ])); MethodHandle factory = site.getTarget(); BiConsumer listenerMethod = (BiConsumer) factory.invoke(); return listenerMethod; }請(qǐng)注意,使用這種方法,我們可以只使用java.util.function.BiConsumer而不是java.lang.reflect.Method ,因此不需要太多的重構(gòu)。 讓我們考慮一下事件偵聽器處理程序代碼–它是對(duì)Spring Framework的簡(jiǎn)化改編:
public class ApplicationListenerMethodAdapter implements GenericApplicationListener { private final Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this .method.invoke(bean, event); handleResult(result); } }這就是可以使用基于Lambda的方法參考進(jìn)行更改的方式:
public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = handler.apply(bean, event); handleResult(result); } }該代碼具有微妙的更改,并且功能相同。 但是與傳統(tǒng)反射相比,它具有一些優(yōu)勢(shì):
類型安全性 –您在LambdaMetafactory.metafactory調(diào)用中指定方法簽名,因此,您將無法將“公正”方法綁定為事件偵聽器。
可追溯性 – lambda包裝器僅對(duì)方法調(diào)用堆棧跟蹤添加了一個(gè)額外的調(diào)用。 它使調(diào)試更加容易。
速度 –這是應(yīng)該衡量的事情。
標(biāo)桿管理
對(duì)于新版本的CUBA框架,我們創(chuàng)建了一個(gè)基于JMH的微基準(zhǔn),以比較“傳統(tǒng)”反射方法調(diào)用(基于lambda)的執(zhí)行時(shí)間和吞吐量,并添加了直接方法調(diào)用以進(jìn)行比較。 方法引用和lambda都是在測(cè)試執(zhí)行之前創(chuàng)建和緩存的。
我們使用了以下基準(zhǔn)測(cè)試參數(shù):
@BenchmarkMode ({Mode.Throughput, Mode.AverageTime}) @Warmup (iterations = 5 , time = 1000 , timeUnit = TimeUnit.MILLISECONDS) @Measurement (iterations = 10 , time = 1000 , timeUnit = TimeUnit.MILLISECONDS)您可以從GitHub下載基準(zhǔn)測(cè)試,然后自己運(yùn)行測(cè)試。
對(duì)于JVM 11.0.2和JMH 1.21,我們得到以下結(jié)果(運(yùn)行次數(shù)可能略有不同):
| LambdaGetTest | 72 | 0.0118 |
| ReflectionGetTest | 65 | 0.0177 |
| DirectMethodGetTest | 260 | 0.0048 |
| LambdaSetTest | 96 | 0.0092 |
| ReflectionSetTest | 58 | 0.0173 |
| DirectMethodSetTest | 415 | 0.0031 |
如您所見,基于lambda的方法處理程序平均快30%。 關(guān)于基于lambda的方法調(diào)用性能, 這里有很好的討論。 結(jié)果-LambdaMetafactory生成的類可以被內(nèi)聯(lián),從而獲得一些性能改進(jìn)。 它比反射更快,因?yàn)榉瓷湔{(diào)用必須在每次調(diào)用時(shí)通過安全檢查。
該基準(zhǔn)測(cè)試是很貧乏的,沒有考慮類的層次結(jié)構(gòu),最終方法等,它只測(cè)量“公正”的方法調(diào)用,但足以滿足我們的目的。
實(shí)作
在CUBA中,您可以使用@Subscribe注釋使方法“監(jiān)聽”各種特定于CUBA的應(yīng)用程序事件。 在內(nèi)部,我們使用此新的基于MethodHandles / LambdaMetafactory的API來加快偵聽器的調(diào)用。 第一次調(diào)用后,將緩存所有方法句柄。
新的體系結(jié)構(gòu)使代碼更整潔,更易于管理,尤其是在具有大量事件處理程序的復(fù)雜UI的情況下。 只看一個(gè)簡(jiǎn)單的例子。 假設(shè)您需要根據(jù)添加到此訂單的產(chǎn)品重新計(jì)算訂單金額。 您有一個(gè)calculateAmount()方法,您需要在訂單中的一組產(chǎn)品更改后立即調(diào)用它。 這是UI控制器的舊版本:
LambdaGetTest在新版本中的外觀如下:
LambdaGetTest代碼更加簡(jiǎn)潔,我們能夠擺脫通常用事件處理程序創(chuàng)建語(yǔ)句填充的“魔術(shù)” init()方法。 而且,我們甚至不需要將數(shù)據(jù)組件注入控制器中-框架將通過組件ID找到它。
結(jié)論
盡管最近引入了新一代框架( Micronaut , Quarkus ),它們比“傳統(tǒng)”框架具有一些優(yōu)勢(shì),但是由于Spring的支持 ,仍有大量基于反射的代碼。 我們將看到市場(chǎng)在不久的將來將如何變化,但是如今,Spring在Java應(yīng)用程序框架中是顯而易見的領(lǐng)導(dǎo)者,因此,我們將使用反射API已有相當(dāng)長(zhǎng)的時(shí)間。
而且,如果您考慮在代碼中使用反射API,無論是實(shí)現(xiàn)自己的框架還是僅是應(yīng)用程序,請(qǐng)考慮另外兩個(gè)選項(xiàng)-代碼生成,尤其是LambdaMetafactory。 后者將提高代碼執(zhí)行速度,而與“傳統(tǒng)”反射API相比,開發(fā)不會(huì)花費(fèi)更多時(shí)間。
翻譯自: https://www.javacodegeeks.com/2019/09/think-twice-before-using-reflection.html
三思筆記
總結(jié)
以上是生活随笔為你收集整理的三思笔记_使用反射前先三思的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 星的拼音怎么写 星的含义
- 下一篇: 到处近义词 到处近义词有哪些