Faster Java Reflection Alternative

Original author: Carlos Raphael
  • Transfer
Hello. Today we want to share with you a translation of an article prepared especially for students of the Java Developer course .

In my Specification Pattern article, I did not specifically mention the underlying component that helped a lot with the implementation. Here I will talk more about the JavaBeanUtil class that I used to get the value of an object field. In that example, it was FxTransaction .



Of course, you will say that you can use Apache Commons BeanUtils to get the same result .or one of its alternatives. But I was interested in delving into this and what I learned works much faster than any library built on the basis of the well-known Java Reflection .

A technology that avoids very slow reflection is bytecode instruction invokedynamic. In short, the manifestation invokedynamic(or “indy”) was the most significant innovation in Java 7, which paved the way for implementing dynamic languages ​​on top of the JVM using dynamic method invocations. Later, in Java 8, it is also possible to implement lambda expressions and references to methods (method reference), as well as to improve string concatenation in Java 9.

In a nutshell, the technique I'm going to describe below uses LambdaMetafactory and MethodHandle to dynamically create an implementation of the Function interface . Function is the only method that delegates a call to the actual target method with code defined inside the lambda.

In this case, the target method is a getter that has direct access to the field we want to read. Also, I must say that if you are well acquainted with the innovations that appeared in Java 8, then you will find the code snippets below quite simple. Otherwise, the code may seem complicated at first sight.

Take a look at the makeshift JavaBeanUtil


The method below getFieldValueis a utility method used to read values ​​from a JavaBean field. It takes a JavaBean object and a field name. The field name can be simple (for example fieldA) or nested, separated by dots (for example, nestedJavaBean.nestestJavaBean.fieldA).

private static final Pattern FIELD_SEPARATOR = Pattern.compile("\\.");
    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
    private static final ClassValue> CACHE = new ClassValue>() {
        @Override
        protected Map computeValue(Class type) {
            return new ConcurrentHashMap<>();
        }
    };
    public static  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;
    }    


To improve performance, I cache a function created dynamically, which in reality will read the value from the field with the name fieldName. In the method getCachedFunction, as you can see, there is a “fast” path that uses ClassValue for caching, and a “slow” path createAndCacheFunctionthat runs if the value in the cache is not found.

The method createFunctionsis a method that returns a list of functions that will be converted to string using Function::andThen. Linking functions with each other in a chain can be represented as nested calls similar to getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA(). After that, we simply cache the function by calling the method cacheAndGetFunction.
If you look closely at the creation of the function, then we need to go through the fields in the pathfollowing way:

private static List createFunctions(Class javaBeanClass, String path) {
    List functions = new ArrayList<>();
    Stream.of(FIELD_SEPARATOR.split(path))
            .reduce(javaBeanClass, (nestedJavaBeanClass, fieldName) -> {
                Tuple2 getFunction = createFunction(fieldName, nestedJavaBeanClass);
                functions.add(getFunction._2);
                return getFunction._1;
            }, (previousClass, nextClass) -> nextClass);
    return functions;
}
private static Tuple2 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);
}


The above method createFunctionsfor each field fieldNameand class in which it is declared calls a method createFunctionthat searches for the desired getter using javaBeanClass.getDeclaredMethods(). Once the getter is found, it is converted to a Tuple tuple (Tuple from the Vavr library ), which contains the type returned by the getter, and a dynamically created function that will behave as if it were a getter itself.
Creating a tuple is performed with the method createTupleWithReturnTypeAndGetterin combination with the method createCallSiteas follows:

private static Tuple2 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());
}


In the two above methods, I use a constant with a name LOOKUPthat is just a reference to MethodHandles.Lookup . With it, I can create a direct link to a method (direct method handle) based on a previously found getter. And finally, the created MethodHandle is passed to the method createCallSitein which the lambda body is created for the function using LambdaMetafactory . From there, ultimately, we can get an instance of CallSite , which is the “custodian” of the function.
Note that for setters you can use a similar approach using BiFunction instead of Function .

Benchmark


To measure performance, I used the wonderful JMH tool ( Java Microbenchmark Harness ), which is likely to be part of JDK 12 ( Translator's note: yes, jmh is included in java 9 ). As you may know, the result is platform dependent, so for reference: I will use 1x6 i5-8600K 3,6 ГГц и Linux x86_64, а также Oracle JDK 8u191 и GraalVM EE 1.0.0-rc9.
For comparison, I chose the Apache Commons BeanUtils library , widely known to most Java developers, and one of its alternatives called Jodd BeanUtil , which is claimed to be nearly 20% faster .

The benchmark code is as follows:

@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);
    }
}


The benchmark defines four scenarios for different levels of nesting of the field. For each field, JMH will perform 5 iterations of 3 seconds to warm up, and then 5 iterations of 1 second for the actual measurement. Each scenario will be repeated 3 times to obtain better measurements.

results


Let's start with the results compiled for the JDK 8u191:


Oracle JDK 8u191

The worst-case scenario using the approach invokedynamicis much faster than the fastest of the other two libraries. This is a huge difference, and if you doubt the results, you can always download the source code and play with it as you like.

Now let's see how the same test works with GraalVM EE 1.0.0-rc9.


GraalVM EE 1.0.0-rc9

Full results can be seen here with the beautiful JMH Visualizer.

Observations


Such a big difference was due to the fact that the JIT compiler knows CallSitewell MethodHandleand can inline them, in contrast to the reflection approach. In addition, you can see how promising GraalVM is . Its compiler does really awesome work that can significantly improve reflection performance.

If you're curious and want to delve deeper, I encourage you to grab the code from my Github repository . Keep in mind, I do not advise you to make home-made JavaBeanUtilto use it in production. My goal is simply to show my experiment and the possibilities that we can get from invokedynamic.

On this the translation came to an end, and we invite everyone to June 13 toA free webinar , in which we will consider how a Docker can be useful for a Java developer: how to make a docker image with a java application and how to interact with it.

Also popular now: