📜 ⬆️ ⬇️

Faster alternative to Java Reflection

Hello. Today we want to share with you a translation of an article prepared specifically for students of the Java Developer course.

In my article, Specification Pattern (Pattern Specification), I did not specifically mention the underlying component, which greatly helped in 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 say that you can use Apache Commons BeanUtils or one of its alternatives to get the same result. But it was interesting for me to delve into it and what I learned works much faster than any library built on the basis of the well-known Java Reflection .
')
The technology to avoid very slow reflection is the invokedynamic bytecode invokedynamic . In short, invokedynamic (or "indy") was the most serious innovation in Java 7, which allowed us to pave the way for implementing dynamic languages ​​on top of the JVM using dynamic method invocation. Later, in Java 8, it also allowed for lambda expressions and method reference references, as well as improved string concatenation in Java 9.

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

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

Let's take a look at self-made JavaBeanUtil


The getFieldValue method getFieldValue is a utility method used to read values ​​from a JavaBean field. It accepts 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<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; } 


To improve performance, I cache a function created dynamically, which in fact will read the value from the field named fieldName . In the getCachedFunction method, as you can see, there is a “fast” path using ClassValue for caching, and a “slow” path createAndCacheFunction , which is executed if the value in the cache is not found.

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

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


The above createFunctions method for each fieldName field and the class in which it is declared calls the createFunction method, which 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 itself a getter.
Creating a tuple is done with the createTupleWithReturnTypeAndGetter method in combination with the createCallSite method as follows:

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


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

Benchmark


To measure performance, I used a great Java Microbenchmark Harness JMH tool , which will probably be part of JDK 12 ( Translator’s Note: yes, jmh entered java 9 ). As you may know, the result depends on the platform, 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 by most Java developers, and one of its alternatives called Jodd BeanUtil , which is said to work almost 20% faster .

The benchmark code looks like this:

 @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 fields. For each field, the JMH will perform 5 iterations of 3 seconds to warm up, and then 5 iterations of 1 second to actually measure. Each scenario will be repeated 3 times to get better measurements.

results


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


Oracle JDK 8u191

The worst scenario using the invokedynamic approach is 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 viewed here with the beautiful JMH Visualizer.

Observations


Such a big difference is due to the fact that the JIT compiler knows CallSite and MethodHandle well and can inline them (inline), unlike the approach with reflection. In addition, you can see how promising GraalVM is . His compiler does a really awesome job that can significantly increase the performance of reflection.

If you are interested and you want to delve deeper, I urge you to take the code from my repository on Github . Keep in mind, I do not advise you to make a custom JavaBeanUtil to use in production. My goal is simply to show my experiment and the possibilities that we can get from invokedynamic .

This is the end of the translation, and we invite everyone to join the free webinar on June 13, where we will consider how the Docker can be useful for a Java developer: how to make a docker image with a java application and how to interact with it.

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


All Articles