Alternativa mais rápida para reflexão de Java

Olá pessoal. Hoje, queremos compartilhar com você a tradução de um artigo preparado especialmente para os alunos do curso Java Developer .

No meu artigo Padrão de especificação , não mencionei especificamente o componente subjacente que ajudou muito na implementação. Aqui vou falar mais sobre a classe JavaBeanUtil que usei para obter o valor de um campo de objeto. Nesse exemplo, era FxTransaction .



Obviamente, você dirá que pode usar o Apache Commons BeanUtils ou uma de suas alternativas para obter o mesmo resultado. Mas eu estava interessado em investigar isso e o que aprendi funciona muito mais rápido do que qualquer biblioteca criada com base no conhecido Java Reflection .

Uma tecnologia que evita reflexões muito lentas é a invokedynamic bytecode invokedynamic . Em resumo, a manifestação de invokedynamic (ou "indy") foi a inovação mais séria no Java 7, que abriu o caminho para a implementação de linguagens dinâmicas na parte superior da JVM usando invocações de métodos dinâmicos. Posteriormente, no Java 8, isso também permitiu expressões lambda e referências a métodos, além de concatenação de cadeias aprimorada no Java 9.

Em poucas palavras, a técnica que vou descrever abaixo usa LambdaMetafactory e MethodHandle para criar dinamicamente uma implementação da interface Function . Function é o único método que delega uma chamada para o método de destino real com código definido dentro do lambda.

Nesse caso, o método target é um getter que tem acesso direto ao campo que queremos ler. Além disso, devo dizer que, se você conhece bem as inovações que apareceram no Java 8, encontrará os trechos de código abaixo bastante simples. Caso contrário, o código pode parecer complicado à primeira vista.

Dê uma olhada no JavaBeanUtil improvisado


O método getFieldValue é um método utilitário usado para ler valores de um campo JavaBean. É necessário um objeto JavaBean e um nome de campo. O nome do campo pode ser simples (por exemplo, fieldA ) ou aninhado, separado por pontos (por exemplo, nestedJavaBean.nestestJavaBean.fieldA ).

 private static final Pattern FIELD_SEPARATOR = Pattern.compile("\\."); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static final ClassValue<Map<String, Function>> CACHE = new ClassValue<Map<String, Function>>() { @Override protected Map<String, Function> computeValue(Class<?> type) { return new ConcurrentHashMap<>(); } }; public static <T> T getFieldValue(Object javaBean, String fieldName) { return (T) getCachedFunction(javaBean.getClass(), fieldName).apply(javaBean); } private static Function getCachedFunction(Class<?> javaBeanClass, String fieldName) { final Function function = CACHE.get(javaBeanClass).get(fieldName); if (function != null) { return function; } return createAndCacheFunction(javaBeanClass, fieldName); } private static Function createAndCacheFunction(Class<?> javaBeanClass, String path) { return cacheAndGetFunction(path, javaBeanClass, createFunctions(javaBeanClass, path) .stream() .reduce(Function::andThen) .orElseThrow(IllegalStateException::new) ); } private static Function cacheAndGetFunction(String path, Class<?> javaBeanClass, Function functionToBeCached) { Function cachedFunction = CACHE.get(javaBeanClass).putIfAbsent(path, functionToBeCached); return cachedFunction != null ? cachedFunction : functionToBeCached; } 


Para melhorar o desempenho, eu fieldName cache uma função criada dinamicamente que realmente lê um valor de um campo chamado fieldName . No método getCachedFunction , como você pode ver, existe um caminho "rápido" usando o ClassValue para armazenamento em cache e um caminho createAndCacheFunction "lento", que é executado se um valor não for encontrado no cache.

O método createFunctions chama um método que retorna uma lista de funções que serão encadeadas usando Function::andThen . As funções de vinculação entre si em uma cadeia podem ser representadas como chamadas aninhadas, semelhantes a getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA() . Depois disso, simplesmente colocamos a função no cache chamando o método cacheAndGetFunction .
Se você observar atentamente a criação da função, precisamos percorrer os campos no path seguinte maneira:

 private static List<Function> createFunctions(Class<?> javaBeanClass, String path) { List<Function> functions = new ArrayList<>(); Stream.of(FIELD_SEPARATOR.split(path)) .reduce(javaBeanClass, (nestedJavaBeanClass, fieldName) -> { Tuple2<? extends Class, Function> getFunction = createFunction(fieldName, nestedJavaBeanClass); functions.add(getFunction._2); return getFunction._1; }, (previousClass, nextClass) -> nextClass); return functions; } private static Tuple2<? extends Class, Function> createFunction(String fieldName, Class<?> javaBeanClass) { return Stream.of(javaBeanClass.getDeclaredMethods()) .filter(JavaBeanUtil::isGetterMethod) .filter(method -> StringUtils.endsWithIgnoreCase(method.getName(), fieldName)) .map(JavaBeanUtil::createTupleWithReturnTypeAndGetter) .findFirst() .orElseThrow(IllegalStateException::new); } 


O método createFunctions acima para cada campo fieldName e a classe em que é declarado chama o método createFunction , que procura o getter desejado usando javaBeanClass.getDeclaredMethods() . Depois que o getter é encontrado, ele é convertido em uma tupla de tupla (tupla da biblioteca Vavr ), que contém o tipo retornado pelo getter e uma função criada dinamicamente que se comportará como se fosse um próprio getter.
Uma tupla é criada com o método createTupleWithReturnTypeAndGetter em combinação com o método createCallSite seguinte maneira:

 private static Tuple2<? extends Class, Function> createTupleWithReturnTypeAndGetter(Method getterMethod) { try { return Tuple.of( getterMethod.getReturnType(), (Function) createCallSite(LOOKUP.unreflect(getterMethod)).getTarget().invokeExact() ); } catch (Throwable e) { throw new IllegalArgumentException("Lambda creation failed for getterMethod (" + getterMethod.getName() + ").", e); } } private static CallSite createCallSite(MethodHandle getterMethodHandle) throws LambdaConversionException { return LambdaMetafactory.metafactory(LOOKUP, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), getterMethodHandle, getterMethodHandle.type()); } 


Nos dois métodos acima, eu uso uma constante chamada LOOKUP , que é apenas uma referência a MethodHandles.Lookup . Com ele, posso criar um link direto para um método (manipulação direta de método) com base em um getter encontrado anteriormente. E, finalmente, o MethodHandle criado é passado para o método createCallSite , no qual o corpo lambda é criado para a função usando LambdaMetafactory . A partir daí, finalmente, podemos obter uma instância do CallSite , que é o "guardião" da função.
Observe que para setters você pode usar uma abordagem semelhante usando BiFunction em vez de Function .

Referência


Para medir o desempenho, usei a maravilhosa ferramenta JMH ( Java Microbenchmark Harness ), que provavelmente faz parte do JDK 12 ( nota do tradutor: sim, o jmh está incluído no java 9 ). Como você provavelmente sabe, o resultado depende da plataforma, portanto, para referência: usarei 1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9 .
Para comparação, escolhi a biblioteca BeanUtils do Apache Commons , amplamente conhecida pela maioria dos desenvolvedores de Java, e uma de suas alternativas chamada Jodd BeanUtil , que é alegadamente 20% mais rápida .

O código de referência é o seguinte:

 @Fork(3) @Warmup(iterations = 5, time = 3) @Measurement(iterations = 5, time = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class JavaBeanUtilBenchmark { @Param({ "fieldA", "nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.nestedJavaBean.fieldA" }) String fieldName; JavaBean javaBean; @Setup public void setup() { NestedJavaBean nestedJavaBean3 = NestedJavaBean.builder().fieldA("nested-3").build(); NestedJavaBean nestedJavaBean2 = NestedJavaBean.builder().fieldA("nested-2").nestedJavaBean(nestedJavaBean3).build(); NestedJavaBean nestedJavaBean1 = NestedJavaBean.builder().fieldA("nested-1").nestedJavaBean(nestedJavaBean2).build(); javaBean = JavaBean.builder().fieldA("fieldA").nestedJavaBean(nestedJavaBean1).build(); } @Benchmark public Object invokeDynamic() { return JavaBeanUtil.getFieldValue(javaBean, fieldName); } /** * Reference: http://commons.apache.org/proper/commons-beanutils/ */ @Benchmark public Object apacheBeanUtils() throws Exception { return PropertyUtils.getNestedProperty(javaBean, fieldName); } /** * Reference: https://jodd.org/beanutil/ */ @Benchmark public Object joddBean() { return BeanUtil.declared.getProperty(javaBean, fieldName); } public static void main(String... args) throws IOException, RunnerException { Main.main(args); } } 


A referência define quatro cenários para diferentes níveis de aninhamento do campo. Para cada campo, o JMH executará 5 iterações de 3 segundos para aquecer e, em seguida, 5 iterações de 1 segundo para a medição real. Cada cenário será repetido 3 vezes para obter melhores medições.

Resultados


Vamos começar com os resultados compilados para o JDK 8u191 :


Oracle JDK 8u191

O pior cenário possível usando a abordagem invokedynamic é muito mais rápido que o mais rápido das outras duas bibliotecas. Essa é uma diferença enorme e, se você duvida dos resultados, sempre pode baixar o código-fonte e brincar com ele como quiser.

Agora vamos ver como o mesmo teste funciona com o GraalVM EE 1.0.0-rc9.


GraalVM EE 1.0.0-rc9

Os resultados completos podem ser vistos aqui com o belo JMH Visualizer.

Observações


Essa grande diferença ocorreu devido ao fato de o compilador JIT conhecer bem o CallSite e o MethodHandle e poder incorporá-los, diferentemente da abordagem de reflexão. Além disso, você pode ver como o GraalVM é promissor. Seu compilador faz um trabalho realmente impressionante que pode melhorar significativamente o desempenho da reflexão.

Se você estiver curioso e quiser se aprofundar, encorajo você a pegar o código do meu repositório do Github . Lembre-se de que eu não aconselho que você faça um JavaBeanUtil fabricação própria para usá-lo na produção. Meu objetivo é simplesmente mostrar meu experimento e as possibilidades que podemos obter do invokedynamic .

A tradução chegou ao fim e convidamos todos a um seminário on- line gratuito em 13 de junho, no qual examinaremos como o Docker pode ser útil para um desenvolvedor Java: como criar uma imagem do docker com um aplicativo java e como interagir com ele.

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


All Articles