Nota do tradutor: O desenvolvimento da estrutura CUBA gera um grande número de projetos de P&D. No decorrer de um desses projetos, descobriu-se que precisamos chamar métodos de interface padrão de classes proxy. Nos deparamos com um artigo muito útil, parece-me que a experiência apresentada será pelo menos interessante e, no máximo, útil para um amplo círculo de desenvolvedores.Quando se trata de acessar os métodos de interface padrão em Java através da reflexão, o Google não ajuda muito. Por exemplo, uma
solução no StackOverflow funciona apenas em determinadas situações e não em todas as versões do Java.
Este artigo discutirá várias abordagens para chamar métodos de interface padrão por meio de reflexão; isso pode ser necessário, por exemplo, ao criar classes de proxy.
TL; DR Se você não puder esperar, todos os métodos de chamada de métodos padrão descritos neste artigo estão disponíveis
neste link , e esse problema já foi resolvido em nossa biblioteca
jOOR .
Proxy de interfaces com métodos padrão
A útil API java.lang.reflect.Proxy existe há muito tempo, com ela podemos fazer coisas legais, por exemplo:
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(); } }
Esse código simplesmente gera:
Quack
Neste exemplo, criamos uma instância de um proxy que implementa a API Duck usando o
InvocationHandler , que é basicamente apenas um lambda chamado para cada método da interface Duck.
A parte interessante começará quando queremos adicionar uma implementação de método à interface e delegar uma chamada a este método:
interface Duck { default void quack() { System.out.println("Quack"); } }
Provavelmente, você desejará escrever este código:
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(); } }
Mas isso gerará apenas uma longa pilha de exceções aninhadas (e isso não está relacionado à chamada da implementação do método na interface, é simplesmente proibido fazê-lo):
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
Não é muito útil.
Usando API de manipulação de métodos
Portanto, uma pesquisa no Google nos diz que
precisamos usar a API MethodHandles . Bem, vamos tentar!
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(); } }
Legal, parece funcionar!
Quack
... mas não.
Chamando um método de interface com acesso não privado
A interface do exemplo acima foi feita com cuidado para que o código de chamada tivesse acesso privado a ela, ou seja, a interface foi aninhada na classe de chamada. Mas e se tivermos uma interface não aninhada?
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(); } }
Quase o mesmo código não funciona mais. Obtenha a 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
Besteira saiu. Se você pesquisar no Google ainda, poderá encontrar a seguinte solução, que acessa os elementos internos do
MethodHandles.Lookup através da reflexão.
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(); } }
E, aplausos, temos:
Quack
Conseguimos fazer isso no JDK 8. E o 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
Overshoes. É o que acontece
por padrão . Se
--illegal-access=deny
o programa com a bandeira
--illegal-access=deny
:
java --illegal-access=deny ProxyDemo
Bem, então obtemos (e com razão!):
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)
Um dos objetivos do projeto Jigsaw era justamente impedir esses ataques. Então, qual solução é melhor? É isso?
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
Ótimo, isso funciona no Java 9 e 10, mas e o 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
Esta brincando comigo
Portanto, temos uma solução (hack) que funciona em Java 8, mas não em 9 e 10, e há uma solução que funciona em 9 e 10, mas não em 8
Pesquisa mais profunda
Bem, eu apenas tentei executar código diferente em JDKs diferentes. A próxima aula tenta todas as combinações acima. Também está disponível como GIST
aqui .
Compile o código usando o JDK 9 ou 10 (porque a API do JDK 9+ é necessária:
MethodHandles.privateLookupIn () ), mas você precisa compilá-lo
usando o comando abaixo para executar a classe no 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; } ); } }
A saída do programa acima:
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
Conclusão
Entender tudo isso é um pouco complicado.
- No Java 8, a melhor abordagem de trabalho é um hack que invade os internos do JDK por meio do acesso ao construtor privado de pacote da classe
Lookup
. Essa é a única maneira de chamar métodos de interface de maneira consistente com acesso privado e não privado de qualquer classe. - No Java 9 e 10, a melhor maneira é usar
Lookup.findSpecial()
(não funciona no Java 8) ou MethodHandles.privateLookupIn()
(o método não existe no Java 8). A última abordagem deve ser usada se a interface estiver em outro módulo. Este módulo deve fornecer uma interface para chamadas externas.
Honestamente, isso é um pouco confuso. Meme adequado para isso:

Rafael Winterhalter (autor do ByteBuddy) disse que a correção "real" estará na versão revisada da API Proxy:

TraduçãoLukas Eder : "Você não sabe o motivo pelo qual decidiu não permitir mais? Ou apenas perdeu (provavelmente não)? ”
Rafael Winterhalter : “Não há razão. Este é um efeito colateral do modelo de segurança Java para a classe Lookup do MethodHandle. Idealmente, as interfaces de proxy deveriam ter essa pesquisa fornecida como argumento ( construtor - aprox. Por. ), Mas elas não consideraram isso. Ofereci, sem sucesso, uma extensão semelhante para a API de transformação de arquivos de classe. ”
Não tenho certeza de que isso resolverá todos os problemas, mas você realmente precisa garantir que o desenvolvedor não se preocupe com tudo o que foi dito acima.
E está claro que este artigo não está completo, por exemplo, não foi testado se essas abordagens funcionariam se o Duck fosse importado de outro módulo:

TraduçãoJOOQ : título e link do artigo
Rafael Winterhalter : “Você tentou colocar o Duck em um módulo que exporta, mas não abre um pacote de interface? Aposto que sua solução para Java 9+ não funcionará se você usar o caminho do módulo. ”
... e esse será o tópico do próximo artigo.
Usando jOOR
Se você usar jOOR (nossa biblioteca para a API de reflexão, está
aqui ), a versão 0.9.8 incluirá uma correção para isso:
github.com/jOOQ/jOOR/issues/49O Fix simplesmente usa a abordagem de invasão da API de reflexão no Java 8 ou MethodHandles.privateLookupIn () para Java 9+. Você pode escrever:
Reflect.on(new Object()).as(PrivateAccessible.class).quack(); Reflect.on(new Object()).as(PrivateInaccessible.class).quack();