Note du traducteur: Le développement du cadre CUBA donne naissance à un grand nombre de projets de R&D. Au cours d'un de ces projets, il s'est avéré que nous devons appeler des méthodes d'interface par défaut à partir de classes proxy. Nous sommes tombés sur un article très utile, il me semble que l'expérience qui y est présentée sera au moins intéressante, et tout au plus utile à un large cercle de développeurs.Lorsqu'il s'agit d'accéder à des méthodes d'interface par défaut en Java par le biais de la réflexion, Google n'aide pas beaucoup. Par exemple, une
solution sur StackOverflow ne fonctionne que dans certaines situations et pas sur toutes les versions de Java.
Cet article décrit différentes approches pour appeler des méthodes d'interface par défaut par le biais de la réflexion, cela peut être nécessaire, par exemple, lors de la création de classes proxy.
TL; DR Si vous ne pouvez pas attendre, toutes les méthodes d'appel des méthodes par défaut décrites dans cet article sont disponibles
sur ce lien , et ce problème a déjà été résolu dans notre bibliothèque
jOOR .
Interfaces proxy avec méthodes par défaut
L'API utile java.lang.reflect.Proxy existe depuis longtemps, avec elle nous pouvons faire des choses sympas, par exemple:
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(); } }
Ce code génère simplement:
Quack
Dans cet exemple, nous avons créé une instance d'un proxy qui implémente l'API Duck à l'aide de
InvocationHandler , qui est essentiellement un lambda appelé pour chaque méthode de l'interface Duck.
La partie intéressante commencera lorsque nous voudrons ajouter une implémentation de méthode à l'interface et déléguer un appel à cette méthode:
interface Duck { default void quack() { System.out.println("Quack"); } }
Très probablement, vous voudrez écrire ce code:
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(); } }
Mais cela ne générera qu'une longue pile d'exceptions imbriquées (et cela n'est pas lié à l'appel de l'implémentation de la méthode dans l'interface, il est simplement interdit de le faire):
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
Pas très utile.
Utilisation de l'API Method Handles
Ainsi, une recherche Google nous indique que
nous devons utiliser l'API MethodHandles . Eh bien, essayons!
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, ça semble marcher!
Quack
... mais non.
Appel d'une méthode d'interface avec un accès non privé
L'interface de l'exemple ci-dessus a été soigneusement conçue pour que le code appelant y ait un accès privé, c'est-à-dire l'interface a été imbriquée dans la classe appelante. Mais que faire si nous avons une interface non imbriquée?
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(); } }
Presque le même code ne fonctionne plus. Obtenez l'exception 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
Des conneries sont sorties. Si vous le google encore, vous pouvez trouver la solution suivante, qui accède aux internes de
MethodHandles.Lookup par réflexion.
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(); } }
Et, bravo, nous obtenons:
Quack
Nous avons réussi à le faire sur JDK 8. Que diriez-vous de JDK 9 ou 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
Couvre-chaussures. C'est ce qui se passe
par défaut . Si nous
--illegal-access=deny
le programme avec le drapeau
--illegal-access=deny
:
java --illegal-access=deny ProxyDemo
Eh bien, nous obtenons (et à juste titre!):
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)
L'un des objectifs du projet Jigsaw était précisément d'empêcher de tels hacks. Alors, quelle solution est la meilleure? C'est ça?
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
Génial, cela fonctionne en Java 9 et 10, mais qu'en est-il de 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
Vous plaisantez?
Donc, nous avons une solution (hack) qui fonctionne en Java 8, mais pas en 9 et 10, et il y a une solution qui fonctionne en 9 et 10, mais pas en 8
Recherches approfondies
Eh bien, j'ai juste essayé d'exécuter du code différent sur différents JDK. La classe suivante essaie toutes les combinaisons ci-dessus. Il est également disponible sous forme de GIST
ici .
Compilez le code à l'aide de JDK 9 ou 10 (car l'API JDK 9+ est requise:
MethodHandles.privateLookupIn () ), mais vous devez le compiler
à l'aide de la commande ci-dessous pour exécuter la classe sur 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; } ); } }
La sortie du programme ci-dessus:
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
Conclusion
Comprendre tout cela est un peu compliqué.
- Dans Java 8, la meilleure approche de travail est un piratage qui s'introduit dans les internes JDK via l'accès au constructeur package-private de la classe
Lookup
. C'est le seul moyen d'invoquer des méthodes d'interface de manière cohérente avec un accès privé et un accès non privé à partir de n'importe quelle classe. - En Java 9 et 10, la meilleure façon est d'utiliser
Lookup.findSpecial()
(ne fonctionne pas en Java 8) ou MethodHandles.privateLookupIn()
(la méthode n'existe pas en Java 8). Cette dernière approche doit être utilisée si l'interface se trouve dans un autre module. Ce module devrait fournir une interface pour les appels externes.
Honnêtement, c'est un peu déroutant. Mème approprié pour cela:

Rafael Winterhalter (auteur de ByteBuddy) a déclaré que le "vrai" correctif sera dans la version révisée de l'API proxy:

La traductionLukas Eder : «Vous ne savez pas pourquoi vous avez décidé de ne plus l’autoriser? Ou tout simplement raté (probablement pas)? "
Rafael Winterhalter : «Il n'y a aucune raison. Il s'agit d'un effet secondaire du modèle de sécurité Java pour la classe Lookup de MethodHandle. Idéalement, les interfaces proxy devraient avoir une telle recherche fournie comme argument ( constructeur - environ Per. ), Mais ils ne l'ont pas considéré. J'ai en vain proposé une extension similaire pour l'API de transformation de fichiers de classe. »
Je ne suis pas sûr que cela résoudra tous les problèmes, mais vous devez vraiment vous assurer que le développeur ne se soucie pas de tout ce qui précède.
Et il est clair que cet article n'est pas complet, par exemple, il n'a pas été testé si ces approches fonctionneraient si Duck était importé d'un autre module:

La traductionJOOQ : lien titre et article
Rafael Winterhalter : «Avez-vous essayé de mettre Duck dans un module qui exporte mais n'ouvre pas un paquet d'interface? Je parie que votre solution pour Java 9+ ne fonctionnera pas si vous utilisez le chemin du module. »
... et ce sera le sujet du prochain article.
Utiliser jOOR
Si vous utilisez jOOR (notre bibliothèque pour l'API de réflexion, c'est
ici ), la version 0.9.8 inclura un correctif pour cela:
github.com/jOOQ/jOOR/issues/49Fix utilise simplement l'approche de piratage de l'API Reflection dans Java 8 ou MethodHandles.privateLookupIn () pour Java 9+. Vous pouvez écrire:
Reflect.on(new Object()).as(PrivateAccessible.class).quack(); Reflect.on(new Object()).as(PrivateInaccessible.class).quack();