Hola a todos Hoy queremos compartir con ustedes una traducción de un artículo preparado especialmente para estudiantes del curso
Java Developer .
En mi artículo sobre el
Patrón de especificación , no mencioné específicamente el componente subyacente que ayudó mucho con la implementación. Aquí hablaré más sobre la clase
JavaBeanUtil que utilicé para obtener el valor de un campo de objeto. En ese ejemplo, fue
FxTransaction .

Por supuesto, dirá que puede usar
Apache Commons BeanUtils o una de sus alternativas para obtener el mismo resultado. Pero estaba interesado en profundizar en esto y lo que aprendí funciona mucho más rápido que cualquier biblioteca construida sobre la base de la conocida
Reflexión de Java .
Una tecnología que evita la reflexión muy lenta es la
invokedynamic
bytecode
invokedynamic
. En resumen, la manifestación de
invokedynamic
(o "indy") fue la innovación más seria en Java 7, que allanó el camino para implementar lenguajes dinámicos en la parte superior de la JVM utilizando invocaciones de métodos dinámicos. Más tarde, en Java 8, también permitió
expresiones lambda y referencias de métodos, así como una concatenación de cadenas mejorada en Java 9.
En pocas palabras, la técnica que voy a describir a continuación utiliza
LambdaMetafactory y
MethodHandle para crear dinámicamente una implementación de la interfaz
Function . La función es el
único método que delega una llamada al método de destino real con código definido dentro de la lambda.
En este caso, el método de destino es un captador que tiene acceso directo al campo que queremos leer. Además, debo decir que si está familiarizado con las innovaciones que aparecieron en Java 8, encontrará los fragmentos de código a continuación bastante simples. De lo contrario, el código puede parecer complicado a primera vista.
Echa un vistazo a la improvisada JavaBeanUtil
El
getFieldValue
método
getFieldValue
es un método de utilidad utilizado para leer valores de un campo JavaBean. Toma un objeto JavaBean y un nombre de campo. El nombre del campo puede ser simple (por ejemplo,
fieldA
) o anidado, separado por puntos (por ejemplo,
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 mejorar el rendimiento, guardo en caché una función creada dinámicamente que realmente leerá un valor de un campo llamado
fieldName
. En el método
getCachedFunction
, como puede ver, hay una ruta "rápida" que utiliza
ClassValue para el almacenamiento en caché, y una ruta
createAndCacheFunction
"lenta", que se ejecuta si no se encuentra un valor en la memoria caché.
El método
createFunctions
llama a un método que devuelve una lista de funciones que se encadenarán usando
Function::andThen
. Las funciones de
getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA()
entre sí en una cadena se pueden representar como llamadas anidadas, de forma similar a
getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA()
. Después de eso, simplemente ponemos la función en la memoria caché llamando al método
cacheAndGetFunction
.
Si observa de cerca la creación de la función, entonces debemos recorrer los campos en la
path
siguiente manera:
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); }
El método
createFunctions
anterior para cada campo
fieldName
y la clase en la que se declara llama al método
createFunction
, que busca el getter deseado utilizando
javaBeanClass.getDeclaredMethods()
. Una vez que se encuentra el captador, se convierte en una tupla Tupla (Tupla de la biblioteca
Vavr ), que contiene el tipo devuelto por el captador, y una función creada dinámicamente que se comportará como si fuera un captador.
Se crea una tupla con el método
createTupleWithReturnTypeAndGetter
en combinación con el método
createCallSite
siguiente manera:
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()); }
En los dos métodos anteriores, uso una constante llamada
LOOKUP
, que es solo una referencia a
MethodHandles.Lookup . Con él, puedo crear un
enlace directo a un método (identificador de método directo) basado en un captador encontrado previamente. Y finalmente, el
MethodHandle creado se pasa al método
createCallSite
, en el que se crea el cuerpo lambda para la función utilizando
LambdaMetafactory . A partir de ahí, en última instancia, podemos obtener una instancia de
CallSite , que es el "custodio" de la función.
Tenga en cuenta que para los setters puede usar un enfoque similar usando
BiFunction en lugar de
Function .
Punto de referencia
Para medir el rendimiento, utilicé la maravillosa herramienta JMH (
Java Microbenchmark Harness ), que probablemente sea parte de JDK 12 (
Nota del traductor: sí, jmh vino en Java 9 ). Como probablemente sepa, el resultado depende de la plataforma, por lo que para referencia:
1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9
.
Para comparar, elegí la
biblioteca Apache Commons BeanUtils , ampliamente conocida por la mayoría de los desarrolladores de Java, y una de sus alternativas llamada
Jodd BeanUtil , que se afirma que es
casi un 20% más rápida .
El código de referencia es el siguiente:
@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); } }
El punto de referencia define cuatro escenarios para diferentes niveles de anidamiento de campo. Para cada campo, JMH realizará 5 iteraciones de 3 segundos para calentar, y luego 5 iteraciones de 1 segundo para la medición real. Cada escenario se repetirá 3 veces para obtener mejores mediciones.
Resultados
Comencemos con los resultados compilados para el JDK
8u191
:
Oracle JDK 8u191El peor de los casos que utiliza el enfoque
invokedynamic
es mucho más rápido que el más rápido de las otras dos bibliotecas. Esta es una gran diferencia, y si duda de los resultados, siempre puede descargar el
código fuente y jugar con él a su gusto.
Ahora veamos cómo funciona la misma prueba con
GraalVM EE 1.0.0-rc9.
GraalVM EE 1.0.0-rc9Los resultados completos se pueden ver
aquí con el hermoso visualizador JMH.
Observaciones
Esa gran diferencia se debe al hecho de que el compilador JIT conoce
CallSite
y
MethodHandle
bien y puede
MethodHandle
, a diferencia del enfoque de reflexión. Además, puede ver lo prometedor que
es GraalVM . Su compilador hace un trabajo realmente increíble que puede mejorar significativamente el rendimiento de la reflexión.
Si tienes curiosidad y quieres profundizar, te animo a que tomes el código de mi repositorio de
Github . Tenga en cuenta que no le aconsejo que haga un
JavaBeanUtil
hecho a sí mismo para usarlo en la producción. Mi objetivo es simplemente mostrar mi experimento y las posibilidades que podemos obtener de la
invokedynamic
.
La traducción ha llegado a su fin, e invitamos a todos a un
seminario web gratuito el 13 de junio, en el que consideraremos cómo Docker puede ser útil para un desarrollador de Java: cómo hacer una imagen de Docker con una aplicación Java y cómo interactuar con ella.