├── README.md ├── src └── main │ └── java │ └── com │ └── github │ └── carlosraphael │ └── javabeanutil │ ├── javabean │ ├── JavaBean.java │ └── NestedJavaBean.java │ ├── JavaBeanUtilBenchmark.java │ └── JavaBeanUtil.java ├── LICENSE └── pom.xml /README.md: -------------------------------------------------------------------------------- 1 | # javabeanutil-benchmark 2 | https://medium.com/free-code-camp/a-faster-alternative-to-java-reflection-db6b1e48c33e 3 | -------------------------------------------------------------------------------- /src/main/java/com/github/carlosraphael/javabeanutil/javabean/JavaBean.java: -------------------------------------------------------------------------------- 1 | package com.github.carlosraphael.javabeanutil.javabean; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | 6 | /** 7 | * @author carlos.raphael.lopes@gmail.com 8 | */ 9 | @Value @Builder 10 | public class JavaBean { 11 | 12 | private final String fieldA; 13 | private final NestedJavaBean nestedJavaBean; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/carlosraphael/javabeanutil/javabean/NestedJavaBean.java: -------------------------------------------------------------------------------- 1 | package com.github.carlosraphael.javabeanutil.javabean; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | 6 | /** 7 | * @author carlos.raphael.lopes@gmail.com 8 | */ 9 | @Value @Builder 10 | public class NestedJavaBean { 11 | 12 | private final String fieldA; 13 | private final NestedJavaBean nestedJavaBean; 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Carlos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/carlosraphael/javabeanutil/JavaBeanUtilBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.github.carlosraphael.javabeanutil; 2 | 3 | import com.github.carlosraphael.javabeanutil.javabean.JavaBean; 4 | import com.github.carlosraphael.javabeanutil.javabean.NestedJavaBean; 5 | import jodd.bean.BeanUtil; 6 | import org.apache.commons.beanutils.PropertyUtils; 7 | import org.openjdk.jmh.Main; 8 | import org.openjdk.jmh.annotations.*; 9 | import org.openjdk.jmh.runner.RunnerException; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Fork(3) 15 | @Warmup(iterations = 5, time = 3) 16 | @Measurement(iterations = 5, time = 1) 17 | @BenchmarkMode(Mode.AverageTime) 18 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 19 | @State(Scope.Thread) 20 | public class JavaBeanUtilBenchmark { 21 | 22 | @Param({ 23 | "fieldA", 24 | "nestedJavaBean.fieldA", 25 | "nestedJavaBean.nestedJavaBean.fieldA", 26 | "nestedJavaBean.nestedJavaBean.nestedJavaBean.fieldA" 27 | }) 28 | String fieldName; 29 | JavaBean javaBean; 30 | 31 | @Setup 32 | public void setup() { 33 | NestedJavaBean nestedJavaBean3 = NestedJavaBean.builder().fieldA("nested-3").build(); 34 | NestedJavaBean nestedJavaBean2 = NestedJavaBean.builder().fieldA("nested-2").nestedJavaBean(nestedJavaBean3).build(); 35 | NestedJavaBean nestedJavaBean1 = NestedJavaBean.builder().fieldA("nested-1").nestedJavaBean(nestedJavaBean2).build(); 36 | javaBean = JavaBean.builder().fieldA("fieldA").nestedJavaBean(nestedJavaBean1).build(); 37 | } 38 | 39 | @Benchmark 40 | public Object invokeDynamic() { 41 | return JavaBeanUtil.getFieldValue(javaBean, fieldName); 42 | } 43 | 44 | /** 45 | * Reference: http://commons.apache.org/proper/commons-beanutils/ 46 | */ 47 | @Benchmark 48 | public Object apacheBeanUtils() throws Exception { 49 | return PropertyUtils.getNestedProperty(javaBean, fieldName); 50 | } 51 | 52 | /** 53 | * Reference: https://jodd.org/beanutil/ 54 | */ 55 | @Benchmark 56 | public Object joddBean() { 57 | return BeanUtil.declared.getProperty(javaBean, fieldName); 58 | } 59 | 60 | public static void main(String... args) throws IOException, RunnerException { 61 | Main.main(args); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.github.carlosraphael.javabeanutil 8 | javabeanutil-benchmark 9 | 1.0-SNAPSHOT 10 | 11 | javabeanutil-benchmark 12 | https://github.com/carlosraphael/javabeanutil-benchmark 13 | 14 | 15 | UTF-8 16 | 8 17 | 18 | 19 | 20 | 21 | io.vavr 22 | vavr 23 | 0.10.0 24 | 25 | 26 | org.apache.commons 27 | commons-lang3 28 | RELEASE 29 | 30 | 31 | commons-beanutils 32 | commons-beanutils-core 33 | RELEASE 34 | 35 | 36 | org.jodd 37 | jodd-bean 38 | RELEASE 39 | 40 | 41 | org.openjdk.jmh 42 | jmh-core 43 | RELEASE 44 | 45 | 46 | org.openjdk.jmh 47 | jmh-generator-annprocess 48 | RELEASE 49 | 50 | 51 | org.projectlombok 52 | lombok 53 | true 54 | RELEASE 55 | 56 | 57 | junit 58 | junit 59 | RELEASE 60 | test 61 | 62 | 63 | 64 | 65 | javaBeanUtilsBenchmark 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | 3.8.0 71 | 72 | ${jdk.version} 73 | ${jdk.version} 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-shade-plugin 79 | 3.2.1 80 | 81 | 82 | package 83 | 84 | shade 85 | 86 | 87 | 88 | 90 | com.github.carlosraphael.javabeanutil.JavaBeanUtilBenchmark 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/com/github/carlosraphael/javabeanutil/JavaBeanUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.carlosraphael.javabeanutil; 2 | 3 | import io.vavr.Tuple; 4 | import io.vavr.Tuple2; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.lang.invoke.*; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.Modifier; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.function.Function; 15 | import java.util.regex.Pattern; 16 | import java.util.stream.Stream; 17 | 18 | /** 19 | * This class is meant to demonstrate a quite fast alternative to Java Reflection when reading values from a given 20 | * Java Bean object. 21 | * 22 | * It is built on top of {@link LambdaMetafactory} and {@link MethodHandle} in order to create a {@link CallSite} 23 | * so that "invokedynamic" bytecode instruction is generated to get most out of the JVM . Also, for optimal performance 24 | * it caches the dynamically created getters into a {@link ClassValue}. 25 | * 26 | * Benchmark shows this technique can by far outperform the well-known Apache BeanUtils library and one of its alternatives 27 | * called Jodd BeanUtil. 28 | * 29 | * NOTE: this class does not support array and collection/map fields. 30 | * 31 | * @author carlos.raphael.lopes@gmail.com 32 | */ 33 | @SuppressWarnings("unchecked") 34 | public class JavaBeanUtil { 35 | 36 | private static final Pattern FIELD_SEPARATOR = Pattern.compile("\\."); 37 | private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); 38 | private static final ClassValue> CACHE = new ClassValue>() { 39 | @Override 40 | protected Map computeValue(Class type) { 41 | return new ConcurrentHashMap<>(); 42 | } 43 | }; 44 | 45 | private JavaBeanUtil() {} 46 | 47 | public static T getFieldValue(Object javaBean, String fieldName) { 48 | return (T) getCachedFunction(javaBean.getClass(), fieldName).apply(javaBean); 49 | } 50 | 51 | private static Function getCachedFunction(Class javaBeanClass, String fieldName) { 52 | final Function function = CACHE.get(javaBeanClass).get(fieldName); 53 | if (function != null) { 54 | return function; 55 | } 56 | return createAndCacheFunction(javaBeanClass, fieldName); 57 | } 58 | 59 | private static Function createAndCacheFunction(Class javaBeanClass, String path) { 60 | return cacheAndGetFunction(path, javaBeanClass, 61 | createFunctions(javaBeanClass, path) 62 | .stream() 63 | .reduce(Function::andThen) 64 | .orElseThrow(IllegalStateException::new) 65 | ); 66 | } 67 | 68 | private static Function cacheAndGetFunction(String path, Class javaBeanClass, Function functionToBeCached) { 69 | Function cachedFunction = CACHE.get(javaBeanClass).putIfAbsent(path, functionToBeCached); 70 | return cachedFunction != null ? cachedFunction : functionToBeCached; 71 | } 72 | 73 | private static List createFunctions(Class javaBeanClass, String path) { 74 | List functions = new ArrayList<>(); 75 | Stream.of(FIELD_SEPARATOR.split(path)) 76 | .reduce(javaBeanClass, (nestedJavaBeanClass, fieldName) -> { 77 | Tuple2 getFunction = createFunction(fieldName, nestedJavaBeanClass); 78 | functions.add(getFunction._2); 79 | return getFunction._1; 80 | }, (previousClass, nextClass) -> nextClass); 81 | return functions; 82 | } 83 | 84 | private static Tuple2 createFunction(String fieldName, Class javaBeanClass) { 85 | return Stream.of(javaBeanClass.getDeclaredMethods()) 86 | .filter(JavaBeanUtil::isGetterMethod) 87 | .filter(method -> StringUtils.endsWithIgnoreCase(method.getName(), fieldName)) 88 | .map(JavaBeanUtil::createTupleWithReturnTypeAndGetter) 89 | .findFirst() 90 | .orElseThrow(IllegalStateException::new); 91 | } 92 | 93 | private static boolean isGetterMethod(Method method) { 94 | return method.getParameterCount() == 0 && 95 | !Modifier.isStatic(method.getModifiers()) && 96 | method.getName().startsWith("get") && 97 | !method.getName().endsWith("Class"); 98 | } 99 | 100 | private static Tuple2 createTupleWithReturnTypeAndGetter(Method getterMethod) { 101 | try { 102 | return Tuple.of( 103 | getterMethod.getReturnType(), 104 | (Function) createCallSite(LOOKUP.unreflect(getterMethod)).getTarget().invokeExact() 105 | ); 106 | } catch (Throwable e) { 107 | throw new IllegalArgumentException("Lambda creation failed for getterMethod (" + getterMethod.getName() + ").", e); 108 | } 109 | } 110 | 111 | private static CallSite createCallSite(MethodHandle getterMethodHandle) throws LambdaConversionException { 112 | return LambdaMetafactory.metafactory(LOOKUP, "apply", 113 | MethodType.methodType(Function.class), 114 | MethodType.methodType(Object.class, Object.class), 115 | getterMethodHandle, getterMethodHandle.type()); 116 | } 117 | } 118 | --------------------------------------------------------------------------------