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); } @Benchmark public Object apacheBeanUtils() throws Exception { return PropertyUtils.getNestedProperty(javaBean, fieldName); } @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 8u191O 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-rc9Os 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.