大家好 今天,我们想与您分享专门为
Java Developer课程的学生准备的文章的翻译。
在我的
规范模式文章中,我没有特别提到对实现有很大帮助的底层组件。 在这里,我将更多地讨论我用来获取对象字段值的
JavaBeanUtil类。 在该示例中,它是
FxTransaction 。

当然,您会说可以使用
Apache Commons BeanUtils或其替代方法之一来获得相同的结果。 但是我对深入研究很感兴趣,并且我学到的东西比任何基于著名的
Java Reflection构建的库都要快得多。
避免非常慢的反射的技术是
invokedynamic
字节码
invokedynamic
。 简而言之,
invokedynamic
(或“ indy”)的体现是Java 7中最重要的创新,它为使用动态方法调用在JVM之上实现动态语言铺平了道路。 后来,在Java 8中,它还允许使用
lambda表达式和方法引用,并在Java 9中改进了字符串连接。
简而言之,我将在下面描述的技术使用
LambdaMetafactory和
MethodHandle动态创建
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); } @Benchmark public Object apacheBeanUtils() throws Exception { return PropertyUtils.getNestedProperty(javaBean, fieldName); } @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编译器非常了解
CallSite
和
MethodHandle
并可
MethodHandle
联它们,与反射方法不同。 此外,您可以看到
GraalVM的前景如何。 它的编译器做的很棒,可以显着提高反射性能。
如果您好奇并想深入研究,建议您从我的
Github存储库中获取代码。 请记住,我不建议您制作一个自制的
JavaBeanUtil
在生产中使用它。 我的目标仅仅是展示我的实验以及我们从
invokedynamic
可以得到的可能性。
翻译已经结束,我们邀请所有人参加6月13日的
免费网络研讨会 ,其中我们将考虑Docker如何对Java开发人员有用:如何使用Java应用程序制作Docker映像以及如何与之交互。