Anmerkung des Übersetzers: Die Entwicklung des CUBA-Frameworks bringt eine große Anzahl von F & E-Projekten hervor. Im Verlauf eines solchen Projekts stellte sich heraus, dass wir Standardschnittstellenmethoden aus Proxy-Klassen aufrufen müssen. Wir sind auf einen sehr nützlichen Artikel gestoßen. Es scheint mir, dass die darin präsentierten Erfahrungen zumindest interessant und höchstens für einen breiten Kreis von Entwicklern nützlich sein werden.Wenn es darum geht, über Reflektion auf Standardschnittstellenmethoden in Java zuzugreifen, hilft Google nicht viel. Beispielsweise funktioniert eine
Lösung in StackOverflow nur in bestimmten Situationen und nicht in allen Java-Versionen.
In diesem Artikel werden verschiedene Ansätze zum Aufrufen von Standardschnittstellenmethoden durch Reflektion erläutert. Dies kann beispielsweise beim Erstellen von Proxy-Klassen erforderlich sein.
TL; DR Wenn Sie nicht warten können, stehen
unter diesem Link alle in diesem Artikel beschriebenen Methoden zum Aufrufen der Standardmethoden zur Verfügung. Dieses Problem wurde bereits in unserer
jOOR- Bibliothek
behoben .
Proxying-Schnittstellen mit Standardmethoden
Die nützliche API java.lang.reflect.Proxy gibt es schon lange, mit der wir coole Dinge machen können, zum Beispiel:
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(); } }
Dieser Code gibt einfach Folgendes aus:
Quack
In diesem Beispiel haben wir eine Instanz eines Proxys erstellt, der die Duck-API mithilfe des
InvocationHandler implementiert. Dies ist im Grunde nur ein Lambda, das für jede Methode der Duck-Schnittstelle aufgerufen wird.
Der interessante Teil beginnt, wenn wir der Schnittstelle eine Methodenimplementierung hinzufügen und einen Aufruf an diese Methode delegieren möchten:
interface Duck { default void quack() { System.out.println("Quack"); } }
Höchstwahrscheinlich möchten Sie diesen Code schreiben:
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(); } }
Dies erzeugt jedoch nur einen langen Stapel verschachtelter Ausnahmen (und dies ist nicht mit dem Aufruf der Implementierung der Methode in der Schnittstelle verbunden, es ist einfach verboten, dies zu tun):
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
Nicht sehr hilfreich.
Verwenden der API für Methodenhandles
Eine Google-Suche sagt uns also, dass
wir die MethodHandles-API verwenden müssen . Lass es uns versuchen!
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(); } }
Cool, es scheint zu funktionieren!
Quack
... aber nein.
Aufruf einer Schnittstellenmethode mit nicht privatem Zugriff
Die Schnittstelle aus dem obigen Beispiel wurde sorgfältig hergestellt, so dass der aufrufende Code privaten Zugriff darauf hatte, d. H. Die Schnittstelle wurde in der aufrufenden Klasse verschachtelt. Aber was ist, wenn wir eine nicht verschachtelte Schnittstelle haben?
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(); } }
Fast der gleiche Code funktioniert nicht mehr. Holen Sie sich die 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
Bullshit kam heraus. Wenn Sie es noch googeln, finden Sie die folgende Lösung, die über Reflexion auf die Interna von
MethodHandles.Lookup zugreift.
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(); } }
Und Prost, wir bekommen:
Quack
Wir haben es auf JDK 8 geschafft. Wie wäre es mit JDK 9 oder 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
Überschuhe. Dies geschieht
standardmäßig . Wenn wir das Programm mit dem Flag
--illegal-access=deny
:
java --illegal-access=deny ProxyDemo
Na dann bekommen wir (und das zu Recht!):
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)
Eines der Ziele des Jigsaw-Projekts war es, solche Hacks genau zu verhindern. Welche Lösung ist also besser? Das?
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
Großartig, das funktioniert in Java 9 und 10, aber was ist mit 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
Willst du mich veräppeln?
Wir haben also eine Lösung (Hack), die in Java 8 funktioniert, aber nicht in 9 und 10, und es gibt eine Lösung, die in 9 und 10 funktioniert, aber nicht in 8
Tiefere Forschung
Nun, ich habe gerade versucht, unterschiedlichen Code auf verschiedenen JDKs auszuführen. Die nächste Klasse probiert alle oben genannten Kombinationen aus. Es ist hier auch als GIST erhältlich.
Kompilieren Sie den Code mit JDK 9 oder 10 (da die JDK 9+
-API erforderlich ist:
MethodHandles.privateLookupIn () ), aber Sie müssen ihn
mit dem folgenden Befehl kompilieren, um die Klasse unter JDK 8 auszuführen:
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; } ); } }
Die Ausgabe des obigen Programms:
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;
Java 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
Fazit
Das alles zu verstehen ist etwas kompliziert.
- In Java 8 ist der beste Arbeitsansatz ein Hack, der durch Zugriff auf den paketprivaten Konstruktor der
Lookup
Klasse in die JDK-Interna einbricht. Dies ist die einzige Möglichkeit, Schnittstellenmethoden auf konsistente Weise sowohl für den privaten als auch für den nicht privaten Zugriff einer Klasse aufzurufen. - In Java 9 und 10 verwenden Sie am besten
Lookup.findSpecial()
(funktioniert in Java 8 nicht) oder MethodHandles.privateLookupIn()
(die Methode ist in Java 8 nicht vorhanden). Der letztere Ansatz sollte verwendet werden, wenn sich die Schnittstelle in einem anderen Modul befindet. Dieses Modul sollte eine Schnittstelle für externe Anrufe bieten.
Ehrlich gesagt ist das ein bisschen verwirrend. Geeignetes Mem dafür:

Rafael Winterhalter (Autor von ByteBuddy) sagte, dass das "echte" Update in der überarbeiteten Version der Proxy-API enthalten sein wird:

ÜbersetzungLukas Eder : "Sie kennen den Grund nicht, warum Sie beschlossen haben, es nicht mehr zuzulassen? Oder haben Sie es einfach verpasst (höchstwahrscheinlich nicht)? “
Rafael Winterhalter : „Es gibt keinen Grund. Dies ist ein Nebeneffekt des Java-Sicherheitsmodells für die Lookup-Klasse von MethodHandle. Im Idealfall sollte für Proxy-Schnittstellen eine solche Suche als Argument bereitgestellt werden ( Konstruktor - ca. Per. ). Dies wurde jedoch nicht berücksichtigt. Ich habe erfolglos eine ähnliche Erweiterung für die API zur Transformation von Klassendateien angeboten. “
Ich bin nicht sicher, ob dies alle Probleme lösen wird, aber Sie müssen wirklich sicherstellen, dass sich der Entwickler nicht um all das kümmert.
Und es ist klar, dass dieser Artikel nicht vollständig ist. Beispielsweise wurde nicht getestet, ob diese Ansätze funktionieren würden, wenn Duck aus einem anderen Modul importiert würde:

ÜbersetzungJOOQ : Titel- und Artikellink
Rafael Winterhalter : „Haben Sie versucht, Duck in ein Modul zu integrieren, das exportiert, aber kein Schnittstellenpaket öffnet? Ich wette, Ihre Lösung für Java 9+ funktioniert nicht, wenn Sie den Modulpfad verwenden. “
... und das wird das Thema des nächsten Artikels sein.
Mit jOOR
Wenn Sie jOOR verwenden (unsere Bibliothek für die Reflection-API finden Sie
hier ), enthält Version 0.9.8 eine Korrektur für dieses
Problem :
github.com/jOOQ/jOOR/issues/49Fix verwendet einfach den Reflection API-Hack-Ansatz in Java 8 oder MethodHandles.privateLookupIn () für Java 9+. Sie können schreiben:
Reflect.on(new Object()).as(PrivateAccessible.class).quack(); Reflect.on(new Object()).as(PrivateInaccessible.class).quack();