Akses yang tepat ke metode antarmuka standar melalui refleksi di Java 8, 9, 10

Catatan Penerjemah: Pengembangan kerangka kerja CUBA memunculkan sejumlah besar proyek Litbang. Dalam satu proyek seperti itu, ternyata kita perlu memanggil metode antarmuka standar dari kelas proxy. Kami menemukan sebuah artikel yang sangat berguna, bagi saya pengalaman yang disajikan di dalamnya akan setidaknya menarik, dan paling bermanfaat bagi lingkaran pengembang yang luas.

Ketika datang untuk mengakses metode antarmuka standar di Jawa melalui refleksi, google tidak banyak membantu. Misalnya, solusi pada StackOverflow hanya berfungsi dalam situasi tertentu dan tidak pada semua versi Java.

Artikel ini akan membahas berbagai pendekatan untuk memanggil metode antarmuka standar melalui refleksi, ini mungkin diperlukan, misalnya, saat membuat kelas proxy.

TL; DR Jika Anda tidak bisa menunggu, maka semua metode memanggil metode default yang dijelaskan dalam artikel ini tersedia di tautan ini , dan masalah ini telah dipecahkan di perpustakaan kami.

Proxying interface dengan metode default


API java.lang.reflect.Proxy yang berguna telah ada sejak lama, dengan itu kita dapat melakukan hal-hal keren, misalnya:

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

Kode ini hanya menghasilkan:

 Quack 

Dalam contoh ini, kami membuat instance proxy yang mengimplementasikan Duck API menggunakan InvocationHandler , yang pada dasarnya hanya sebuah lambda yang dipanggil untuk setiap metode antarmuka Duck.

Bagian yang menarik akan dimulai ketika kita ingin menambahkan implementasi metode ke antarmuka dan mendelegasikan panggilan ke metode ini:

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

Kemungkinan besar, Anda ingin menulis kode ini:

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

Tapi ini hanya akan menghasilkan setumpuk pengecualian bersarang (dan ini tidak terhubung dengan memanggil implementasi metode di antarmuka, itu hanya dilarang melakukannya):

 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 

Tidak terlalu membantu.

Menggunakan Metode Menangani API


Jadi, pencarian Google memberi tahu kita bahwa kita perlu menggunakan API MethodHandles . Baiklah, mari kita coba!

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

Keren, sepertinya berhasil!

 Quack 

... tapi tidak.

Memanggil metode antarmuka dengan akses non-pribadi


Antarmuka dari contoh di atas dibuat dengan hati-hati sehingga kode panggilan memiliki akses pribadi ke sana, yaitu antarmuka bersarang di kelas panggilan. Tetapi bagaimana jika kita memiliki antarmuka yang tidak bersarang?

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

Hampir kode yang sama tidak lagi berfungsi. Dapatkan 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 

Omong kosong keluar. Jika Anda belum google, Anda dapat menemukan solusi berikut, yang mengakses internal MethodHandles.Lookup melalui refleksi.

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

Dan, tepuk tangan, kami mendapatkan:

 Quack 

Kami berhasil melakukannya di JDK 8. Bagaimana dengan JDK 9 atau 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 

Overshoes. Inilah yang terjadi secara default . Jika kita menjalankan program dengan flag --illegal-access=deny :

 java --illegal-access=deny ProxyDemo 

Baiklah, lalu kita dapatkan (dan memang begitu!):

 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) 

Salah satu tujuan dari proyek Jigsaw adalah untuk mencegah peretasan tersebut. Jadi, solusi mana yang lebih baik? Apakah itu

 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 

Bagus, ini berfungsi di Java 9 dan 10, tapi bagaimana dengan 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 

Apakah kamu bercanda?

Jadi, kami memiliki solusi (retas) yang berfungsi di Java 8, tetapi tidak di 9 dan 10, dan ada solusi yang bekerja di 9 dan 10, tetapi tidak di 8

Penelitian yang lebih mendalam


Yah, saya hanya mencoba menjalankan kode yang berbeda pada JDK yang berbeda. Kelas berikutnya mencoba semua kombinasi di atas. Ini juga tersedia sebagai GIST di sini .

Kompilasi kode menggunakan JDK 9 atau 10 (karena API JDK 9+ diperlukan: MethodHandles.privateLookupIn () ), tetapi Anda harus mengompilasinya menggunakan perintah di bawah ini untuk menjalankan kelas pada 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; } ); } } 

Output dari program di atas:

Java 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; 

Jawa 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() 

Java 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 

Kesimpulan


Untuk memahami semua ini agak rumit.

  • Di Java 8, pendekatan kerja terbaik adalah retas yang membobol internal JDK melalui akses ke konstruktor paket-pribadi dari kelas Lookup . Ini adalah satu-satunya cara untuk memanggil metode antarmuka secara konsisten dengan akses pribadi dan akses non-pribadi dari kelas apa pun.
  • Di Java 9 dan 10, cara terbaik adalah menggunakan Lookup.findSpecial() (tidak berfungsi di Java 8) atau MethodHandles.privateLookupIn() (metode ini tidak ada di Java 8). Pendekatan yang terakhir harus digunakan jika antarmuka berada dalam modul lain. Modul ini harus menyediakan antarmuka untuk panggilan eksternal.

Jujur saja, ini agak membingungkan. Meme yang cocok untuk ini:



Rafael Winterhalter (penulis ByteBuddy) mengatakan bahwa perbaikan "nyata" akan berada dalam versi Proxy API yang direvisi:


Terjemahan
Lukas Eder : "Anda tidak tahu alasan mengapa Anda memutuskan untuk tidak mengizinkannya lagi? Atau hanya melewatkannya (kemungkinan besar tidak)? ”
Rafael Winterhalter : β€œTidak ada alasan. Ini adalah efek samping dari model keamanan Java untuk kelas Pencarian dari MethodHandle. Idealnya, antarmuka proksi harus memiliki Pencarian seperti yang disediakan sebagai argumen ( konstruktor - sekitar. ), Tetapi mereka tidak mempertimbangkan ini. Saya tidak berhasil menawarkan ekstensi serupa untuk API transformasi file kelas. "

Saya tidak yakin ini akan menyelesaikan semua masalah, tetapi Anda benar-benar perlu memastikan bahwa pengembang tidak khawatir tentang semua hal di atas.

Dan jelas bahwa artikel ini tidak lengkap, misalnya, tidak diuji apakah pendekatan ini akan berhasil jika Bebek diimpor dari modul lain:


Terjemahan
JOOQ : tautan judul dan artikel
Rafael Winterhalter : β€œSudahkah Anda mencoba memasukkan Duck ke dalam modul yang mengekspor tetapi tidak membuka paket antarmuka? Saya yakin solusi Anda untuk Java 9+ tidak akan berfungsi jika Anda menggunakan jalur modul. "

... dan itu akan menjadi topik artikel berikutnya.

Menggunakan jOOR


Jika Anda menggunakan jOOR (pustaka kami untuk API refleksi, ada di sini ), maka versi 0.9.8 akan menyertakan perbaikan untuk ini: github.com/jOOQ/jOOR/issues/49
Perbaiki cukup menggunakan pendekatan retas API Refleksi di Java 8 atau MethodHandles.privateLookupIn () untuk Java 9+. Anda dapat menulis:

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

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


All Articles