مقدمة

أثناء عملية التطوير ، غالبًا ما يكون من الضروري إنشاء مثيل لفئة يتم تخزين اسمها في ملف تكوين XML ، أو لاستدعاء طريقة يتم كتابة اسمها كسلسلة كقيمة لسمة التعليق التوضيحي. في مثل هذه الحالات ، يكون الجواب واحدًا: "استخدم التفكير!".
في الإصدار الجديد من CUBA Platform ، كانت إحدى مهام تحسين الإطار هي التخلص من الإنشاء الواضح لمعالجات الأحداث في فئات التحكم في شاشات واجهة المستخدم. في الإصدارات السابقة ، كانت تصريحات المعالج في طريقة تهيئة وحدة التحكم مشوهة جدًا بالكود ، لذلك في الإصدار السابع قررنا تنظيف كل شيء.
يعد مستمع الأحداث مجرد إشارة إلى الطريقة التي يجب استدعاؤها في الوقت المناسب (انظر قالب المراقب ). مثل هذا القالب بسيط جدًا في التنفيذ باستخدام فئة java.lang.reflect.Method
. عند بدء التشغيل ، تحتاج فقط إلى مسح الفئات ، وسحب الأساليب المشروحة منها ، وحفظ المراجع إليها ، واستخدام الروابط للاتصال بالطريقة (أو الطرق) عند وقوع الحدث ، كما هو الحال في الجزء الأكبر من الأطر. الشيء الوحيد الذي أوقفنا هو أن الكثير من الأحداث يتم إنشاؤها تقليديًا في واجهة المستخدم ، وعند استخدام واجهة برمجة التطبيقات الانعكاسية ، يتعين عليك دفع بعض الأسعار في شكل وقت استدعاء الأسلوب. لذلك ، قررنا أن ننظر في كيف يمكنك إنشاء معالجات الأحداث دون استخدام التفكير.
لقد نشرنا بالفعل مواد حول MethodHandles و LambdaMetafactory على habr ، وهذه المواد هي نوع من الاستمرارية. سوف ندرس إيجابيات وسلبيات استخدام واجهة برمجة التطبيقات الانعكاسية ، وكذلك البدائل التي تولد التعليمات البرمجية مع تجميع AOT و LambdaMetafactory ، وكيف تم استخدامه في إطار CUBA.
الانعكاس: قديم. جيدة. موثوق
في علم الكمبيوتر ، يعني الانعكاس أو الانعكاس (الاسم المستعار للتأمل ، الانعكاس باللغة الإنجليزية) عملية يمكن خلالها للبرنامج تتبع وتعديل هيكله وسلوكه في وقت التشغيل. (ج) ويكيبيديا.
بالنسبة لمعظم مطوري Java ، فإن التفكير ليس شيئًا جديدًا أبدًا. يبدو لي أنه بدون هذه الآلية ، لن تصبح Java هي تلك Java التي تشغل الآن حصة كبيرة من السوق في تطوير برامج التطبيقات. فقط فكر في: أساليب التوكيل الملزمة للأحداث من خلال التعليقات التوضيحية ، وحقن التبعية ، والجوانب ، وحتى إنشاء برنامج تشغيل JDBC في الإصدارات الأولى من JDK! التفكير في كل مكان ، هو حجر الأساس لجميع الأطر الحديثة.
هل هناك أي مشاكل مع Reflection كما هو مطبق في مهمتنا؟ لقد حددنا ثلاثة:
السرعة - استدعاء الأسلوب من خلال Reflection API أبطأ من الاتصال المباشر. في كل إصدار جديد من JVM ، يقوم المطورون بتسريع المكالمات باستمرار من خلال الانعكاس ، يحاول برنامج التحويل البرمجي JIT زيادة تحسين الكود ، ولكن على أي حال ، فإن الفرق بالمقارنة مع استدعاء الأسلوب المباشر ملحوظ.
الكتابة - إذا كنت تستخدم java.lang.reflect.Method
في الكود ، فهذا مجرد إشارة إلى بعض الطرق. وليس في أي مكان هو مكتوب عدد المعلمات التي يتم تمريرها ونوع ما هي عليه. تؤدي المكالمة ذات المعلمات الخاطئة إلى حدوث خطأ في وقت التشغيل ، وليس في مرحلة تجميع التطبيق أو تنزيله.
الشفافية - إذا فشلت الطريقة التي يتم استدعاؤها من خلال الانعكاس ، فسيتعين علينا إجراء العديد من مكالمات الاستدعاء invoke()
قبل أن نصل إلى أسفل السبب الحقيقي للخطأ.
ولكن إذا نظرنا إلى رمز منشئي الأحداث Spring أو JPA في Hibernate ، java.lang.reflect.Method
القديمة الجيدة في الداخل. وفي المستقبل القريب ، أعتقد أن هذا من غير المرجح أن يتغير. هذه الأُطر كبيرة جدًا وترتبط بها كثيرًا ، ويبدو أن أداء معالجات الأحداث على جانب الخادم يكفي للتفكير فيما يمكنك استبدال المكالمات من خلال التفكير به.
وما هي الخيارات الأخرى هناك؟
تجميع AOT وتوليد الشفرة - إعطاء التطبيقات سرعة الظهر!
أول مرشح ليحل محل واجهة برمجة التطبيقات الانعكاسية هو توليد الشفرة. الآن بدأت إطارات مثل Micronaut أو Quarkus في الظهور ، والتي تحاول حل مشكلتين: تقليل سرعة تشغيل التطبيق وتقليل استهلاك الذاكرة. هذان المقياسان مهمان في عصرنا من الحاويات والخدمات المصغرة والبنى الخالية من الخوادم ، وتحاول الأطر الجديدة حل هذه المشكلة من خلال تجميع AOT. باستخدام تقنيات مختلفة (يمكنك قراءة هنا ، على سبيل المثال) ، يتم تعديل رمز التطبيق بطريقة تجعل جميع المكالمات المنعكسة على الطرق والبنائين ، إلخ. استبدالها بمكالمات مباشرة. وبالتالي ، لا تحتاج إلى مسح الفئات وإنشاء فاصوليا في وقت بدء تشغيل التطبيق ، وتقوم JIT بتحسين الكود بشكل أكثر كفاءة في وقت التشغيل ، مما يوفر زيادة كبيرة في أداء التطبيقات المبنية على هذه الأُطر. هل هذا النهج له عيوب؟ الجواب: بالطبع هناك.
أولاً ، لا تقوم بتشغيل الكود الذي كتبته ، حيث يتغير الكود المصدري أثناء التحويل البرمجي ، لذلك إذا حدث خطأ ما ، فمن الصعب أحيانًا فهم مكان الخطأ: في الكود أو في خوارزمية التوليد (عادةً ما يكون لك ، بالطبع ). ومن هنا تنشأ مشكلة التصحيح - يجب عليك تصحيح التعليمات البرمجية الخاصة بك.
الثانية - لتشغيل تطبيق مكتوب في الإطار مع تجميع AOT ، تحتاج إلى أداة خاصة. لا يمكنك فقط الحصول على تطبيق مكتوب في Quarkus وتشغيله ، على سبيل المثال. نحتاج إلى مكون إضافي خاص لـ maven / gradle ، والذي سيعالج الكود مسبقًا. والآن ، في حالة وجود أخطاء في الإطار ، تحتاج إلى تحديث ليس فقط المكتبات ، ولكن أيضًا المكون الإضافي.
في الحقيقة ، إن إنشاء الشفرة ليس جديدًا في عالم Java ؛ فهو لم يظهر مع Micronaut أو Quarkus . بشكل أو بآخر ، تستخدمه بعض الأطر. هنا يمكننا أن نتذكر lombok ، sidesj مع الجيل الأول من الكود الخاص بالجوانب أو الكسوف ، والذي يضيف الكود إلى فئات الكيانات لزيادة كفاءة إلغاء التسلسل. في CUBA ، نستخدم إنشاء الكود لإنشاء أحداث حول التغييرات في حالة الكيان ولتضمين رسائل المدقق في كود الفصل لتبسيط العمل مع الكيانات في واجهة المستخدم.
بالنسبة لمطوري CUBA ، سيكون تنفيذ إنشاء كود ثابت لمعالجات الأحداث خطوة كبيرة للغاية لأنه يجب إجراء الكثير من التغييرات في البنية الداخلية وفي المكون الإضافي لإنشاء التعليمات البرمجية. هل هناك شيء يشبه التفكير ولكنه أسرع؟
قدم Java 7 تعليمات جديدة لـ JVM - invokedynamic
. عنها تقرير ممتاز من فلاديمير إيفانوف على jug.ru هنا . تم تصميم هذا التعليم في الأصل للاستخدام في لغات حيوية مثل Groovy ، وكان مرشحًا رائعًا لاستدعاء الأساليب في Java دون استخدام التفكير. في نفس الوقت الذي تظهر فيه التعليمات الجديدة ، ظهرت واجهة برمجة تطبيقات مرتبطة في JDK:
- Class
MethodHandle
- ظهر مرة أخرى في Java 7 ، ولكن لا يزال غير مستخدم في كثير من الأحيان LambdaMetafactory
- هذه الفئة موجودة بالفعل من Java 8 ، وأصبحت تطويرًا إضافيًا لواجهة برمجة التطبيقات للمكالمات الديناميكية ، وتستخدم MethodHandle
الداخل.
يبدو أن MethodHandle
، كونه مؤشرًا مكتوبًا بطريقة ما إلى طريقة (مُنشئ ، وما إلى ذلك) ، سيكون قادرًا على أداء دور java.lang.reflect.Method
. ستكون المكالمات أسرع ، لأن كل اختبارات النوع التي يتم إجراؤها في Reflection API مع كل مكالمة ، في هذه الحالة ، يتم تنفيذها مرة واحدة فقط ، عند إنشاء MethodHandle
.
ولكن للأسف ، تبين أن MethodHandle
الخالصة MethodHandle
أبطأ من المكالمات من خلال واجهة انعكاس API. يمكن تحقيق مكاسب الأداء من خلال جعل MethodHandle
ثابتًا ، ولكن ليس في جميع الحالات. هناك مناقشة ممتازة حول سرعة مكالمات MethodHandle
في القائمة البريدية MethodHandle
.
ولكن عندما LambdaMetafactory
فئة LambdaMetafactory
، كانت هناك فرصة حقيقية لتسريع مكالمات الأسلوب. LambdaMetafactory
يسمح LambdaMetafactory
بإنشاء كائن lambda ولف استدعاء طريقة مباشرة فيه ، والتي يمكن الحصول عليها من خلال MethodHandle
. ثم ، باستخدام الكائن الذي تم إنشاؤه ، يمكنك استدعاء الطريقة المطلوبة. فيما يلي مثال على الجيل الذي يلتف طريقة getter التي تم تمريرها كمعلمة إلى 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; }
نتيجة لذلك ، حصلنا على مثيل BiFunction بدلاً من الطريقة. والآن ، حتى لو استخدمنا Method في التعليمات البرمجية الخاصة بنا ، فإن استبدالها بـ BiFunction ليس بالأمر الصعب. خذ الكود الحقيقي (المبسط قليلاً ، صحيح) للاتصال @EventListener
الطريقة ، الذي وضع علامة @EventListener
من Spring Framework:
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); } }
تغييرات طفيفة ، وظائف هي نفسها ، ولكن هناك مزايا:
لدى لامدا نوع - تم تحديده عند الخلق ، لذلك سوف يفشل استدعاء "مجرد طريقة".
مكدس التتبع أقصر - عند استدعاء طريقة من خلال lambda ، تتم إضافة مكالمة إضافية واحدة فقط - apply()
. وهذا كل شيء. بعد ذلك ، تسمى الطريقة نفسها.
ولكن يجب أن تقاس السرعة.
قياس السرعة
لاختبار الفرضية ، قمنا بعمل علامة مصغرة باستخدام JMH لمقارنة وقت التشغيل والإنتاجية عند استدعاء نفس الطريقة بطرق مختلفة: من خلال واجهة برمجة التطبيقات الانعكاسية ، من خلال LambdaMetafactory ، وقمنا أيضًا بإضافة استدعاء طريقة مباشرة للمقارنة. تم إنشاء روابط إلى Method و lambdas وتخزينها مؤقتًا قبل بدء الاختبار.
معلمات الاختبار:
@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 (قد تختلف الأرقام ، ولكن يظل الفرق ملحوظًا ونفس الشيء تقريبًا):
اختبار - الحصول على القيمة | الإنتاجية (العمليات / لنا) | وقت التنفيذ (لنا / المرجع) |
---|
LambdaGetTest | 72 | 0.0118 |
ReflectionGetTest | 65 | 0.0177 |
DirectMethodGetTest | 260 | 0.0048 |
اختبار - تعيين القيمة | الإنتاجية (العمليات / لنا) | وقت التنفيذ (لنا / المرجع |
LambdaSetTest | 96 | 0.0092 |
ReflectionSetTest | 58 | 0.0173 |
DirectMethodSetTest | 415 | 0.0031 |
في المتوسط ، اتضح أن استدعاء طريقة من خلال لامدا هو حوالي 30 ٪ أسرع من خلال انعكاس API. هناك مناقشة أخرى رائعة حول أداء طريقة الاحتجاج هنا إذا كان أي شخص مهتمًا بالتفاصيل. باختصار - يتم الحصول على الكسب في السرعة ، من بين أشياء أخرى ، نظرًا لحقيقة أن lambdas التي تم إنشاؤها يمكن تضمينها في رمز البرنامج ، وأن اختبارات النوع لم يتم تنفيذها بعد ، على عكس الانعكاس.
بالطبع ، هذا المعيار بسيط للغاية ، ولا يشمل طرق الاتصال في التسلسل الهرمي للفئة أو قياس سرعة الاتصال بالطرق النهائية. لكننا أجرينا قياسات أكثر تعقيدًا ، وكانت النتائج مؤيدة دائمًا لاستخدام LambdaMetafactory.
استخدام
في إطار عمل CUBA ، الإصدار 7 ، في وحدات التحكم في واجهة المستخدم ، يمكنك استخدام التعليق التوضيحي @Subscribe
"لتوقيع" طريقة لبعض أحداث واجهة المستخدم. داخليًا ، يتم تطبيق هذا على LambdaMetafactory
، يتم إنشاء ارتباطات إلى أساليب المستمع LambdaMetafactory
مؤقتًا في المكالمة الأولى.
مكّن هذا الابتكار من مسح الشفرة إلى حد كبير ، لا سيما في حالة النماذج التي تحتوي على عدد كبير من العناصر ، والتفاعل المعقد ، وبالتالي مع عدد كبير من معالجات الأحداث. مثال بسيط من CUBA QuickStart: تخيل أنك بحاجة إلى إعادة حساب كمية الطلب عند إضافة عناصر المنتج أو إزالتها. تحتاج إلى كتابة تعليمة برمجية تقوم بتشغيل طريقة 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 هذا المكون بواسطة المعرف.
النتائج
على الرغم من ظهور جيل جديد من الأطر مع ترجمة AOT ( Micronaut ، Quarkus ) ، والتي لها مزايا لا يمكن إنكارها على الأطر "التقليدية" (بشكل رئيسي ، تتم مقارنتها مع Spring ) ، لا يزال هناك قدر كبير من التعليمات البرمجية المكتوبة باستخدام انعكاس API (وشكرا لجميع الربيع نفسه). ويبدو أن Spring Framework لا يزال حاليًا رائدًا في أطر تطوير التطبيقات ، وسنعمل مع رمز يستند إلى الانعكاس لفترة طويلة قادمة.
وإذا كنت تفكر في استخدام واجهة برمجة تطبيقات Reflection في التعليمات البرمجية الخاصة بك - سواء كان تطبيقًا أو إطار عمل - فكر مرتين. أولاً ، حول إنشاء التعليمات البرمجية ، ثم حول MethodHandles / LambdaMetafactory. قد تكون الطريقة الثانية أسرع ، ولن يتم إنفاق جهود التطوير أكثر من حالة استخدام Reflection API.
بعض الروابط المفيدة أكثر:
بديل أسرع لجافا التفكير
القرصنة تعبيرات لامدا في جاوة
مقابض الطريقة في جافا
انعكاس جافا ، ولكن أسرع بكثير
لماذا LambdaMetafactory 10٪ أبطأ من MethodHandle ثابت لكن 80٪ أسرع من MethodHandle غير ثابت؟
سريع جدًا ، Megamorphic: ما الذي يؤثر على أداء استدعاء الأسلوب في Java؟