更快的Java反射替代

大家好 今天,我们想与您分享专门为Java Developer课程的学生准备的文章的翻译。

在我的规范模式文章中,我没有特别提到对实现有很大帮助的底层组件。 在这里,我将更多地讨论我用来获取对象字段值的JavaBeanUtil类。 在该示例中,它是FxTransaction



当然,您会说可以使用Apache Commons BeanUtils或其替代方法之一来获得相同的结果。 但是我对深入研究很感兴趣,并且我学到的东西比任何基于著名的Java Reflection构建的库都要快得多。

避免非常慢的反射的技术是invokedynamic字节码invokedynamic 。 简而言之, invokedynamic (或“ indy”)的体现是Java 7中最重要的创新,它为使用动态方法调用在JVM之上实现动态语言铺平了道路。 后来,在Java 8中,它还允许使用lambda表达式和方法引用,并在Java 9中改进了字符串连接。

简而言之,我将在下面描述的技术使用LambdaMetafactoryMethodHandle动态创建Function接口的实现。 函数是唯一使用lambda内部定义的代码将调用委派给实际目标方法的方法。

在这种情况下,目标方法是直接访问我们要读取的字段的getter。 另外,我必须说,如果您熟悉Java 8中出现的创新,那么您会发现下面的代码片段非常简单。 否则,代码乍一看似乎很复杂。

看一下临时的JavaBeanUtil


下面的getFieldValue方法是一种实用程序方法,用于从JavaBean字段读取值。 它带有一个JavaBean对象和一个字段名。 字段名称可以是简单的(例如, fieldA ),也可以是嵌套的,并用句点分隔(例如, 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; } 


为了提高性能,我缓存了一个动态创建的函数,该函数实际上将从一个名为fieldName的字段中读取一个值。 如您所见,在getCachedFunction方法中,有一个使用ClassValue进行缓存的“快速”路径,以及一个在缓存中未找到值时运行的“慢速” createAndCacheFunction路径。

createFunctions方法调用一个方法,该方法返回将使用Function::andThen链接的Function::andThen 。 链中彼此链接的函数可以表示为嵌套调用,类似于getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA() 。 之后,我们只需调用cacheAndGetFunction方法将函数放入缓存中。
如果仔细研究函数的创建,那么我们需要遍历path的字段,如下所示:

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


上面针对每个fieldName字段及其声明的类的createFunction方法调用createFunction方法,该方法使用javaBeanClass.getDeclaredMethods()搜索所需的getter。 一旦找到吸气剂,它将转换为一个Tuple元组( Vavr库中的Tuple),其中包含该吸气剂返回的类型,以及一个动态创建的函数,其行为就像它本身就是一个吸气剂一样。
使用createTupleWithReturnTypeAndGetter方法与createCallSite方法结合创建元组,如下所示:

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


在上述两种方法中,我使用了一个名为LOOKUP的常量,它只是对MethodHandles.Lookup的引用。 有了它,我可以基于先前找到的getter创建指向方法直接链接 (直接方法句柄)。 最后,将创建的MethodHandle传递给createCallSite方法,其中使用LambdaMetafactory为该函数创建lambda主体。 从那里,最终,我们可以得到CallSite的实例,该实例是函数的“托管人”。
请注意,对于二传手 ,您可以使用类似的方法使用BiFunction代替Function

基准测试


为了衡量性能,我使用了出色的JMH工具( Java Microbenchmark Harness ),它很可能是JDK 12的一部分( 译者注:是的,jmh包含在Java 9中 )。 您可能知道,结果取决于平台,因此仅供参考:我将使用1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9
为了进行比较,我选择了大多数Java开发人员都熟知的Apache Commons BeanUtils库 ,以及它的替代方案之一Jodd BeanUtil ,据称它快了将近20%

基准代码如下:

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


该基准针对字段的不同嵌套级别定义了四种方案。 对于每个字段,JMH将执行3秒钟的5次迭代以进行预热,然后进行1秒的5次迭代以进行实际测量。 每个场景将重复3次以获得更好的测量结果。

结果


让我们从为JDK 8u191编译的结果开始:


甲骨文JDK 8u191

使用invokedynamic方法的最坏情况比其他两个库中最快的情况要快得多。 这是一个巨大的差异,如果您对结果有疑问,可以随时下载源代码并随意使用它。

现在,让我们看看相同的测试如何与GraalVM EE 1.0.0-rc9.


GraalVM EE 1.0.0-rc9

美丽的JMH Visualizer可以在这里看到完整的结果。

观察结果


如此大的差异是由于JIT编译器非常了解CallSiteMethodHandle并可MethodHandle联它们,与反射方法不同。 此外,您可以看到GraalVM的前景如何。 它的编译器做的很棒,可以显着提高反射性能。

如果您好奇并想深入研究,建议您从我的Github存储库中获取代码。 请记住,我不建议您制作一个自制的JavaBeanUtil在生产中使用它。 我的目标仅仅是展示我的实验以及我们从invokedynamic可以得到的可能性。

翻译已经结束,我们邀请所有人参加6月13日的免费网络研讨会 ,其中我们将考虑Docker如何对Java开发人员有用:如何使用Java应用程序制作Docker映像以及如何与之交互。

Source: https://habr.com/ru/post/zh-CN455122/


All Articles