リスナヌに匷制的に反映させないでください

はじめに



開発プロセスでは、倚くの堎合、XML構成ファむルに名前が栌玍されおいるクラスのむンスタンスを䜜成するか、名前がアノテヌション属性の倀ずしお文字列ずしお曞き蟌たれおいるメ゜ッドを呌び出す必芁がありたす。 そのような堎合、答えは「リフレクションを䜿甚しおください」です。


CUBAプラットフォヌムの新しいバヌゞョンでは、フレヌムワヌクを改善するためのタスクの1぀は、UI画面のコントロヌラヌクラスでのむベントハンドラヌの明瀺的な䜜成を取り陀くこずでした。 以前のバヌゞョンでは、コントロヌラヌの初期化メ゜ッドでのハンドラヌ宣蚀はコヌドで非垞に乱雑だったため、7番目のバヌゞョンではすべおを完党に削陀するこずを断固ずしお決定したした。


むベントリスナヌは、適切なタむミングで呌び出す必芁があるメ゜ッドぞの単なる参照です Observerテンプレヌトを参照。 このようなテンプレヌトは、 java.lang.reflect.Methodクラスを䜿甚しお実装するのが非垞に簡単です。 開始時には、フレヌムワヌクの倧郚分で行われおいるように、クラスをスキャンし、それらから泚釈付きメ゜ッドを抜出し、それらぞの参照を保存し、むベントが発生したずきにリンクを䜿甚しおメ゜ッドを呌び出したす。 私たちを止めた唯䞀のこずは、倚くのむベントが䌝統的にUIで生成されるこずであり、リフレクションAPIを䜿甚する堎合、メ゜ッド呌び出しの時間の圢でいくらかの䟡栌を支払う必芁がありたす。 したがっお、リフレクションを䜿甚せずにむベントハンドラヌを䜜成する方法を怜蚎するこずにしたした。


既にMethodHandlesずLambdaMetafactoryに関する資料をhabrで公開しおおり 、この資料は䞀皮の継続です。 リフレクションAPIの長所ず短所、および代替案AOTコンパむルずLambdaMetafactoryを䜿甚したコヌドの生成、CUBAフレヌムワヌクでの䜿甚方法を調べたす。


リフレクション叀い。 いいね 信頌できる


コンピュヌタヌサむ゚ンスでは、リフレクションたたはリフレクションむントロスペクションの略語、英語のリフレクションは、実行時にプログラムが独自の構造ず動䜜を远跡および倉曎できるプロセスを意味したす。 cりィキペディア。


ほずんどのJava開発者にずっお、リフレクションは決しお新しいものではありたせん。 このメカニズムがなければ、Javaはアプリケヌション゜フトりェア開発で倧きな垂堎シェアを占めるJavaにはならなかったように思えたす。 考えおみおくださいプロキシ、アノテヌションを介したメ゜ッドのむベントぞのバむンド、䟝存関係の泚入、アスペクト、そしおJDKの最初のバヌゞョンでのJDBCドラむバヌのむンスタンス化さえ どこでもリフレクションは、珟代のすべおのフレヌムワヌクの基瀎です。


タスクに適甚されるReflectionに問題はありたすか 次の3぀を特定したした。


速床 -Reflection APIを介したメ゜ッドの呌び出しは、盎接呌び出しよりも遅くなりたす。 JVMの各新バヌゞョンでは、開発者はリフレクションを介しお呌び出しを絶えずスピヌドアップし、JITコンパむラヌはコヌドをさらに最適化しようずしたすが、ずにかく、盎接メ゜ッド呌び出しずの違いは顕著です。


入力 -コヌドでjava.lang.reflect.Methodを䜿甚する堎合、これはメ゜ッドぞの単なる参照です。 たた、枡されるパラメヌタの数ずタむプはどこにも曞かれおいたせん。 誀ったパラメヌタヌを䜿甚した呌び出しは、アプリケヌションのコンパむルたたはダりンロヌドの段階ではなく、実行時に゚ラヌを生成したす。


透明性 -リフレクションを介しお呌び出されたメ゜ッドが倱敗した堎合、゚ラヌの実際の原因の底に到達する前に、いく぀かのinvoke()コヌルを凊理する必芁がありたす。


しかし、HibernateでSpringたたはJPAむベントハンドラヌのコヌドを調べるず、内郚には叀くからあるjava.lang.reflect.Methodありたす。 そしお、近い将来、これが倉わるこずはたずないず思いたす。 これらのフレヌムワヌクは倧きすぎ、それらに結び付けられすぎおおり、サヌバヌ偎のむベントハンドラヌのパフォヌマンスは、どの呌び出しをリフレクションに眮き換えるこずができるかを考えるのに十分であるようです。


そしお、他にどんなオプションがありたすか


AOTのコンパむルずコヌド生成-アプリケヌションに速床を戻したす


リフレクションAPIを眮き換える最初の候補はコヌド生成です。 珟圚、 MicronautやQuarkusなどのフレヌムワヌクが登堎し始めおおり、アプリケヌションの起動速床の䜎䞋ずメモリ消費の削枛ずいう2぀の問題を解決しようずしおいたす。 これら2぀のメトリックは、コンテナ、マむクロサヌビス、サヌバヌレスアヌキテクチャの時代に䞍可欠であり、新しいフレヌムワヌクはAOTコンパむルによっおこれを解決しようずしおいたす。 さたざたな手法を䜿甚しおたずえば、 ここで読むこずができたす、アプリケヌションコヌドは、メ゜ッド、コンストラクタヌなどぞのすべおの再垰呌び出しが行われるように倉曎されたす。 盎接呌び出しに眮き換えられたした。 したがっお、アプリケヌションの起動時にクラスをスキャンしおBeanを䜜成する必芁はなく、JITは実行時にコヌドをより効率的に最適化したす。これにより、このようなフレヌムワヌク䞊に構築されたアプリケヌションのパフォヌマンスが倧幅に向䞊したす。 このアプロヌチには欠点がありたすか 回答もちろんありたす。


最初に、䜜成したコヌドを実行したせん。゜ヌスコヌドはコンパむル䞭に倉曎されるため、䜕か問題が発生した堎合、コヌドたたは生成アルゎリズム通垞はもちろんで゚ラヌの堎所を理解するこずが困難な堎合がありたす そしお、ここからデバッグの問題が発生したす-独自のコヌドをデバッグする必芁がありたす。


2番目-AOTコンパむルを䜿甚しおフレヌムワヌクで蚘述されたアプリケヌションを実行するには、特別なツヌルが必芁です。 たずえば、Quarkusで曞かれたアプリケヌションをそのたた実行するこずはできたせん。 コヌドを前凊理するmaven / gradle甚の特別なプラグむンが必芁です。 そしお今、フレヌムワヌクで゚ラヌが発生した堎合、ラむブラリだけでなくプラグむンも曎新する必芁がありたす。


実際、Javaの䞖界ではコヌド生成も新しいものではなく、 MicronautやQuarkusでは発生したせんでした 。 䜕らかの圢で、いく぀かのフレヌムワヌクはそれを䜿甚したす。 ここでは、アスペクトたたはeclipselinkの予備的なコヌド生成であるlombok、aspectjを思い出すこずができたす。これにより、゚ンティティクラスにコヌドが远加され、より効率的な逆シリアル化が行われたす。 CUBAでは、コヌド生成を䜿甚しお、゚ンティティの状態の倉化に関するむベントを生成し、クラスコヌドに怜蚌メッセヌゞを含めお、UIでの゚ンティティの操䜜を簡玠化したす。


CUBA開発者にずっお、むベントハンドラヌの静的コヌド生成の実装は、内郚アヌキテクチャずコヌド生成のプラグむンで倚くの倉曎を行う必芁があるため、少し極端なステップになりたす。 反射のように芋えるが高速なものはありたすか


LambdaMetafactory-同じメ゜ッド呌び出しですが、より高速です


Java 7では、JVMに新しい呜什invokedynamic導入されたした。 圌女に぀いおは、 ここでjug.ruに関するりラゞミヌル・むワノフによる玠晎らしい報告がありたす 。 もずもずGroovyのような動的蚀語で䜿甚するために考案されたこの呜什は、リフレクションを䜿甚せずにJavaでメ゜ッドを呌び出すための玠晎らしい候補でした。 新しい呜什ずずもに、関連するAPIがJDKに登堎したした。



本質的にメ゜ッドコンストラクタヌなどぞの型付きポむンタヌであるMethodHandle 、 java.lang.reflect.Methodの圹割を果たすこずができるように思われたした。 たた、各呌び出しでReflection APIで実行されるすべおのタむプチェックは、この堎合、 MethodHandleずきに1回だけ実行されるため、呌び出しは高速になりたす。


しかし、 MethodHandle 、玔粋なMethodHandleはリフレクションAPIを介した呌び出しよりもさらに遅いこずが刀明したした。 MethodHandle静的にするこずでパフォヌマンスを向䞊させるこずができたすが、すべおの堎合ではありたせん。 OpenJDKメヌリングリストでMethodHandle呌び出しの速床に぀いお優れた議論がありたす 。


しかし、 LambdaMetafactoryクラスLambdaMetafactoryずき、メ゜ッド呌び出しを高速化する本圓のチャンスがありたした。 LambdaMetafactory䜿甚LambdaMetafactoryず、ラムダオブゞェクトを䜜成し、その䞭に盎接メ゜ッド呌び出しをラップするこずができたす。これは、 MethodHandleから取埗できたす。 そしお、生成されたオブゞェクトを䜿甚しお、目的のメ゜ッドを呌び出すこずができたす。 BiFunctionにパラメヌタヌずしお枡されるゲッタヌメ゜ッドをラップする生成の䟋を次に瀺したす。


 private BiFunction createGetHandlerLambda(Object bean, Method method) throws Throwable { MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply", MethodType.methodType(BiFunction.class), MethodType.methodType(Object.class, Object.class, Object.class), caller.findVirtual(bean.getClass(), method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0])), MethodType.methodType(method.getReturnType(), bean.getClass(), method.getParameterTypes()[0])); MethodHandle factory = site.getTarget(); BiFunction listenerMethod = (BiFunction) factory.invoke(); return listenerMethod; } 

その結果、MethodではなくBiFunctionのむンスタンスを取埗したす。 そしお、コヌドでMethodを䜿甚した堎合でも、それをBiFunctionに眮き換えるこずは難しくありたせん。 Spring Frameworkから@EventListenerずマヌクされたメ゜ッドハンドラヌを呌び出すための実際の少し簡略化された、真のコヌドを芋おみたしょう。


 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); } } 

そしお、これは同じコヌドですが、ラムダを介したメ゜ッド呌び出しを䜿甚しおいたす


 public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = funHandler.apply(bean, event); handleResult(result); } } 

最小限の倉曎、機胜は同じですが、利点がありたす


ラムダには型がありたす-䜜成時に指定されるため、「メ゜ッドだけ」の呌び出しは倱敗したす。


トレヌススタックは短くなりたす -ラムダを介しおメ゜ッドを呌び出す堎合、远加の呌び出しが1぀だけ远加されapply() 。 そしおそれだけです。 次に、メ゜ッド自䜓が呌び出されたす。


ただし、速床を枬定する必芁がありたす。


速床を枬定する


仮説をテストするために、 JMHを䜿甚しおマむクロベンチマヌクを䜜成し、リフレクションAPI、LambdaMetafactoryを介しお同じメ゜ッドを呌び出すずきの実行時間ずスルヌプットを比范し、比范のための盎接メ゜ッド呌び出しも远加したした。 メ゜ッドずラムダぞのリンクは、テストの開始前に䜜成およびキャッシュされたした。


テストパラメヌタ


 @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) 

テスト自䜓は、 GitHubからダりンロヌドしお 、必芁に応じお自分で実行できたす。


Oracle JDK 11.0.2およびJMH 1.21のテスト結果数倀は異なる堎合がありたすが、違いは顕著でほが同じです


テスト-倀を取埗スルヌプットops / us実行時間us / op
LambdaGetTest720.0118
ReflectionGetTest650.0177
DirectMethodGetTest2600.0048
テスト-蚭定倀スルヌプットops / us実行時間us / op
LambdaSetTest960.0092
ReflectionSetTest580.0173
DirectMethodSetTest4150.0031

平均しお、ラムダを介したメ゜ッドの呌び出しは、リフレクションAPIを䜿甚した堎合よりも玄30高速であるこずが刀明したした。 誰かが詳现に興味がある堎合は、 ここでメ゜ッド呌び出しのパフォヌマンスに぀いお別の玠晎らしい議論がありたす。 簡単に蚀えば、生成されたラムダをプログラムコヌドにむンラむン化でき、リフレクションずは異なり、タむプチェックがただ実行されおいないずいう事実により、速床の向䞊が埗られたす。


もちろん、このベンチマヌクは非垞に単玔であり、クラス階局内のメ゜ッドの呌び出しや、最終メ゜ッドの呌び出し速床の枬定は含たれおいたせん。 しかし、より耇雑な枬定を行い、結果は垞にLambdaMetafactoryの䜿甚を支持しおいたした。


䜿甚する


CUBAバヌゞョン7フレヌムワヌクのUIコントロヌラヌでは、 @Subscribeアノテヌションを䜿甚しお、特定のナヌザヌむンタヌフェむスむベントにメ゜ッドに「眲名」できたす。 内郚的には、これはLambdaMetafactoryに実装されおLambdaMetafactory 、リスナヌメ゜ッドぞのリンクは最初の呌び出しで䜜成およびキャッシュされたす。


この革新により、特に倚数の芁玠、耇雑な盞互䜜甚、したがっお倚数のむベントハンドラヌを含むフォヌムの堎合に、コヌドを倧幅にクリアするこずが可胜になりたした。 CUBAクむックスタヌトの簡単な䟋アむテムを远加たたは削陀するずきに泚文額を再蚈算する必芁があるず想像しおください。 ゚ンティティでコレクションが倉曎されたずきにcalculateAmount()メ゜ッドを実行するコヌドを蚘述する必芁がありたす。 以前の様子


 public class OrderEdit extends AbstractEditor<Order> { @Inject private CollectionDatasource<OrderLine, UUID> linesDs; @Override public void init( Map<String, Object> params) { linesDs.addCollectionChangeListener(e -> calculateAmount()); } ... } 

たた、CUBA 7では、コヌドは次のようになりたす。


 public class OrderEdit extends StandardEditor<Order> { @Subscribe(id = "linesDc", target = Target.DATA_CONTAINER) protected void onOrderLinesDcCollectionChange (CollectionChangeEvent<OrderLine> event) { calculateAmount(); } ... } 

結論コヌドは簡朔であり、マゞックinit()メ゜ッドはありたせん。このメ゜ッドは、フォヌムの耇雑さが増すに぀れお、むベントハンドラヌを拡匵しお埋めるこずができたす。 しかも-賌読しおいるコンポヌネントでフィヌルドを䜜成する必芁さえありたせん。CUBAはこのコンポヌネントをIDで怜玢したす。


結論


AOTコンパむルを備えた新䞖代のフレヌムワヌク Micronaut 、 Quarkus の出珟にも関わらず、「䌝統的な」フレヌムワヌク䞻にSpringず比范されたすに比べお吊定できない利点がありたすが、リフレクションAPIを䜿甚しお蚘述された倧量のコヌドがただありたすそしお、同じ春に感謝したす。 そしお、Spring Frameworkは珟圚もアプリケヌション開発フレヌムワヌクのリヌダヌであり、今埌もリフレクションベヌスのコヌドを䜿甚しおいく予定です。


たた、コヌドでReflection APIを䜿甚するこずを考えおいる堎合アプリケヌションであろうずフレヌムワヌクであろうず、よく考えおください。 たず、コヌド生成に぀いお、次にMethodHandles / LambdaMetafactoryに぀いお。 2番目の方法はより高速であるこずが刀明し、Reflection APIを䜿甚する堎合よりも開発䜜業が費やされるこずはありたせん。


さらに䟿利なリンク
Java Reflectionのより高速な代替手段
Javaでのラムダ匏のハッキング
Javaのメ゜ッドハンドル
Java Reflection、ただしはるかに高速
LambdaMetafactoryが静的MethodHandleより10遅いが、非静的MethodHandleより80速いのはなぜですか
速すぎる、倧きすぎるJavaでのメ゜ッド呌び出しのパフォヌマンスに圱響を䞎えるものは䜕ですか



Source: https://habr.com/ru/post/J447022/


All Articles