Alternativa de reflexión Java más rápida

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); } /** * 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); } } 


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 8u191

El 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-rc9

Los 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.

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


All Articles