الوصول الصحيح إلى طرق الواجهة الافتراضية من خلال الانعكاس في Java 8 ، 9 ، 10

ملاحظة المترجم: تطوير إطار عمل CUBA يولد عددًا كبيرًا من مشاريع البحث والتطوير. في سياق أحد هذه المشاريع ، اتضح أننا بحاجة إلى استدعاء طرق الواجهة الافتراضية من فئات الوكيل. لقد تعثرنا على مقال مفيد للغاية ، يبدو لي أن التجربة المقدمة فيه ستكون على الأقل مثيرة للاهتمام ، وعلى الأكثر مفيدة لدائرة واسعة من المطورين.

عندما يتعلق الأمر بالوصول إلى طرق الواجهة الافتراضية في Java من خلال الانعكاس ، فإن Google لا تساعد كثيرًا. على سبيل المثال ، لا يعمل حل StackOverflow إلا في حالات معينة وليس على جميع إصدارات Java.

ستناقش هذه المقالة طرقًا مختلفة لاستدعاء طرق الواجهة الافتراضية من خلال الانعكاس ، وقد يكون ذلك ضروريًا ، على سبيل المثال ، عند إنشاء فئات الوكيل.

TL ؛ DR إذا لم تتمكن من الانتظار ، فإن جميع طرق الاتصال بالطرق الافتراضية الموضحة في هذه المقالة متاحة على هذا الرابط ، وقد تم حل هذه المشكلة بالفعل في مكتبة jOOR الخاصة بنا.

واجهات وكيل مع الأساليب الافتراضية


واجهة برمجة التطبيقات المفيدة java.lang.reflect.Proxy موجودة منذ فترة طويلة ، يمكننا من خلالها القيام بأشياء رائعة ، على سبيل المثال:

import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { void quack(); } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { System.out.println("Quack"); return null; } ); duck.quack(); } } 

ينتج هذا الرمز ببساطة:

 Quack 

في هذا المثال ، قمنا بإنشاء مثيل لوكيل يقوم بتنفيذ واجهة برمجة تطبيقات Duck باستخدام InvocationHandler ، وهو في الأساس مجرد لامدا تسمى كل طريقة لواجهة Duck.

سيبدأ الجزء المثير للاهتمام عندما نريد إضافة تطبيق طريقة إلى الواجهة وتفويض مكالمة إلى هذه الطريقة:

 interface Duck { default void quack() { System.out.println("Quack"); } } 

على الأرجح ، ستحتاج إلى كتابة هذا الرمز:

 import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { default void quack() { System.out.println("Quack"); } } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { method.invoke(proxy); return null; } ); duck.quack(); } } 

ولكن هذا لن يؤدي إلا إلى إنشاء مجموعة طويلة من الاستثناءات المتداخلة (وهذا لا يرتبط بالاتصال بتنفيذ الطريقة في الواجهة ، فهو ممنوع ببساطة للقيام بذلك):

 Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:20) Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 2 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 7 more Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 8 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 13 more Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 14 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 19 more ... ... ... goes on forever 

ليس مفيدًا جدًا.

استخدام الطريقة API Handles


لذا ، يخبرنا بحث Google أننا بحاجة إلى استخدام API MethodHandles . حسنا ، دعنا نحاول!

 import java.lang.invoke.MethodHandles; import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { default void quack() { System.out.println("Quack"); } } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { MethodHandles .lookup() .in(Duck.class) .unreflectSpecial(method, Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } } 

رائع ، يبدو أنه يعمل!

 Quack 

... ولكن لا.

استدعاء طريقة واجهة ذات وصول غير خاص


تم إنشاء الواجهة من المثال أعلاه بعناية بحيث يكون لرمز الاتصال وصول خاص إليه ، أي تم دمج الواجهة في فئة الاستدعاء. ولكن ماذا لو كان لدينا واجهة غير متداخلة؟

 import java.lang.invoke.MethodHandles; import java.lang.reflect.Proxy; interface Duck { default void quack() { System.out.println("Quack"); } } public class ProxyDemo { public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { MethodHandles .lookup() .in(Duck.class) .unreflectSpecial(method, Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } } 

تقريبا نفس الرمز لم يعد يعمل. احصل على IllegalAccessException:

 Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:26) Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface Duck, from Duck/package at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850) at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572) at java.lang.invoke.MethodHandles$Lookup.unreflectSpecial(MethodHandles.java:1231) at ProxyDemo.lambda$0(ProxyDemo.java:19) ... 2 more 

خرج هراء. إذا كنت تستخدم google حتى الآن ، يمكنك العثور على الحل التالي ، الذي يصل إلى المكونات الداخلية لـ MethodHandles.Lookup من خلال التفكير.

 import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; interface Duck { default void quack() { System.out.println("Quack"); } } public class ProxyDemo { public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { Constructor<Lookup> constructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(Duck.class) .in(Duck.class) .unreflectSpecial(method, Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } } 

في صحتك ، نحصل على:

 Quack 

تمكنا من القيام بذلك على JDK 8. ماذا عن JDK 9 أو 10؟

 WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by ProxyDemo (file:/C:/Users/lukas/workspace/playground/target/classes/) to constructor java.lang.invoke.MethodHandles$Lookup(java.lang.Class) WARNING: Please consider reporting this to the maintainers of ProxyDemo WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Quack 

الجرموق. هذا ما يحدث بشكل افتراضي . إذا قمنا بتشغيل البرنامج مع العلم --illegal-access=deny :

 java --illegal-access=deny ProxyDemo 

حسنًا ، ثم نحصل على (وهذا صحيح!):

 Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @357246de at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281) at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:192) at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:185) at ProxyDemo.lambda$0(ProxyDemo.java:18) at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:28) 

كان أحد أهداف مشروع Jigsaw على وجه التحديد منع مثل هذه الاختراقات. لذا ، ما هو الحل الأفضل؟ هل هي؟

 import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Proxy; interface Duck { default void quack() { System.out.println("Quack"); } } public class ProxyDemo { public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { MethodHandles.lookup() .findSpecial( Duck.class, "quack", MethodType.methodType( void.class, new Class[0]), Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } } 

 Quack 

رائع ، هذا يعمل في Java 9 و 10 ، ولكن ماذا عن Java 8؟

 Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:25) Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface Duck, from ProxyDemo at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850) at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572) at java.lang.invoke.MethodHandles$Lookup.findSpecial(MethodHandles.java:1002) at ProxyDemo.lambda$0(ProxyDemo.java:18) ... 2 more 

هل تمزح معي؟

لذا ، لدينا حل (اختراق) يعمل في Java 8 ، ولكن ليس في 9 و 10 ، وهناك حل يعمل في 9 و 10 ، ولكن ليس في 8

بحث أعمق


حسنًا ، لقد حاولت تشغيل تعليمات برمجية مختلفة على JDKs مختلفة. يحاول الفصل التالي كل المجموعات المذكورة أعلاه. وهو متوفر أيضًا كجهاز GIST هنا .

جمّع الكود باستخدام JDK 9 أو 10 (لأن واجهة برمجة تطبيقات JDK 9+ مطلوبة: MethodHandles.privateLookupIn () ) ، لكنك تحتاج إلى تجميعها باستخدام الأمر أدناه لتشغيل الفصل على JDK 8:

 javac -source 1.8 -target 1.8 CallDefaultMethodThroughReflection.java 

 import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface PrivateInaccessible { default void quack() { System.out.println(" -> PrivateInaccessible.quack()"); } } public class CallDefaultMethodThroughReflection { interface PrivateAccessible { default void quack() { System.out.println(" -> PrivateAccessible.quack()"); } } public static void main(String[] args) { System.out.println("PrivateAccessible"); System.out.println("-----------------"); System.out.println(); proxy(PrivateAccessible.class).quack(); System.out.println(); System.out.println("PrivateInaccessible"); System.out.println("-------------------"); System.out.println(); proxy(PrivateInaccessible.class).quack(); } private static void quack(Lookup lookup, Class<?> type, Object proxy) { System.out.println("Lookup.in(type).unreflectSpecial(...)"); try { lookup.in(type) .unreflectSpecial(type.getMethod("quack"), type) .bindTo(proxy) .invokeWithArguments(); } catch (Throwable e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } System.out.println("Lookup.findSpecial(...)"); try { lookup.findSpecial(type, "quack", MethodType.methodType(void.class, new Class[0]), type) .bindTo(proxy) .invokeWithArguments(); } catch (Throwable e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } } @SuppressWarnings("unchecked") private static <T> T proxy(Class<T> type) { return (T) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { type }, (Object proxy, Method method, Object[] arguments) -> { System.out.println("MethodHandles.lookup()"); quack(MethodHandles.lookup(), type, proxy); try { System.out.println(); System.out.println("Lookup(Class)"); Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(type); quack(constructor.newInstance(type), type, proxy); } catch (Exception e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } try { System.out.println(); System.out.println("MethodHandles.privateLookupIn()"); quack(MethodHandles.privateLookupIn(type, MethodHandles.lookup()), type, proxy); } catch (Error e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } return null; } ); } } 

مخرجات البرنامج أعلاه:

جافا 8
 $ java -version java version "1.8.0_141" Java(TM) SE Runtime Environment (build 1.8.0_141-b15) Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode) $ java CallDefaultMethodThroughReflection PrivateAccessible ----------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface CallDefaultMethodThroughReflection$PrivateAccessible, from CallDefaultMethodThroughReflection Lookup(Class) Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() MethodHandles.privateLookupIn() -> class java.lang.NoSuchMethodError: java.lang.invoke.MethodHandles.privateLookupIn(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup; PrivateInaccessible ------------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from PrivateInaccessible/package Lookup.findSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from CallDefaultMethodThroughReflection Lookup(Class) Lookup.in(type).unreflectSpecial(...) -> PrivateInaccessible.quack() Lookup.findSpecial(...) -> PrivateInaccessible.quack() MethodHandles.privateLookupIn() -> class java.lang.NoSuchMethodError: java.lang.invoke.MethodHandles.privateLookupIn(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup; 

جافا 9
 $ java -version java version "9.0.4" Java(TM) SE Runtime Environment (build 9.0.4+11) Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode) $ java --illegal-access=deny CallDefaultMethodThroughReflection PrivateAccessible ----------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() Lookup(Class) -> class java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @30c7da1e MethodHandles.privateLookupIn() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() PrivateInaccessible ------------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from PrivateInaccessible/package (unnamed module @30c7da1e) Lookup.findSpecial(...) -> PrivateInaccessible.quack() Lookup(Class) -> class java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @30c7da1e MethodHandles.privateLookupIn() Lookup.in(type).unreflectSpecial(...) -> PrivateInaccessible.quack() Lookup.findSpecial(...) -> PrivateInaccessible.quack() 

جافا 10
 $ java -version java version "10" 2018-03-20 Java(TM) SE Runtime Environment 18.3 (build 10+46) Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode) $ java --illegal-access=deny CallDefaultMethodThroughReflection ...   ,   Java 9 

الخلاصة


لفهم كل هذا أمر معقد قليلاً.

  • في Java 8 ، أفضل طريقة عمل هي اختراق لاختراق JDK الداخلي من خلال الوصول إلى مُنشئ الحزمة الخاصة من فئة Lookup . هذه هي الطريقة الوحيدة لاستدعاء طرق الواجهة بطريقة متسقة مع كل من الوصول الخاص والوصول غير الخاص من أي فئة.
  • أفضل طريقة في Java 9 و 10 هي استخدام Lookup.findSpecial() (لا يعمل في Java 8) أو MethodHandles.privateLookupIn() (الطريقة غير موجودة في Java 8). يجب استخدام الأسلوب الأخير إذا كانت الواجهة في وحدة نمطية أخرى. يجب أن توفر هذه الوحدة واجهة للمكالمات الخارجية.

بصراحة ، هذا مربك قليلاً. ميمي مناسب لهذا:



قال Rafael Winterhalter (مؤلف ByteBuddy) أن الإصلاح "الحقيقي" سيكون في النسخة المنقحة من Proxy API:


الترجمة
لوكاس إيدر : "أنت لا تعرف سبب قرارك بعدم السماح بذلك بعد الآن؟ أو هل فاتته (على الأرجح لا)؟ "
رافائيل وينترهالتر : "لا يوجد سبب. هذا هو أحد الآثار الجانبية لنموذج أمان Java لفئة البحث من MethodHandle. من الناحية المثالية ، يجب أن تحتوي واجهات الوكيل على مثل هذا البحث المقدم كحجة ( منشئ - تقريبًا لكل. ) ، لكنهم لم يفكروا في ذلك. لقد قدمت دون جدوى امتدادًا مشابهًا لواجهة برمجة التطبيقات لتحويل ملف الفئة. "

لست متأكدًا من أن هذا سيحل جميع المشاكل ، ولكنك تحتاج حقًا إلى التأكد من أن المطور لا يقلق بشأن كل ما سبق.

ومن الواضح أن هذه المقالة ليست كاملة ، على سبيل المثال ، لم يتم اختبار ما إذا كانت هذه الأساليب ستعمل إذا تم استيراد Duck من وحدة نمطية أخرى:


الترجمة
JOOQ : رابط العنوان والمقالة
Rafael Winterhalter : "هل حاولت وضع Duck في وحدة نمطية تقوم بالتصدير ولكن لا تفتح حزمة واجهة؟ أراهن أن الحل الخاص بك لـ Java 9+ لن يعمل إذا كنت تستخدم مسار الوحدة. "

... وهذا سيكون موضوع المقال التالي.

باستخدام jOOR


إذا كنت تستخدم jOOR (مكتبتنا لواجهة برمجة التطبيقات للتفكير ، فهي هنا ) ، فإن الإصدار 0.9.8 سيتضمن إصلاحًا لهذا: github.com/jOOQ/jOOR/issues/49
يستخدم Fix ببساطة أسلوب الإختراق Reflection API في Java 8 أو MethodHandles.privateLookupIn () لـ Java 9+. يمكنك الكتابة:

 Reflect.on(new Object()).as(PrivateAccessible.class).quack(); Reflect.on(new Object()).as(PrivateInaccessible.class).quack(); 

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


All Articles