Acesso adequado aos métodos de interface padrão através da reflexão em Java 8, 9, 10

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ção
Lukas 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ção
JOOQ : 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/49
O 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(); 

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


All Articles