Всем привет. Сегодня хотим поделиться с вами переводом статьи, подготовленным специально для студентов курса «Разработчик Java».
В моей статье Specification Pattern (паттерн Спецификация) я специально не упомянул о лежащем в основе компоненте, который сильно помог в реализации. Здесь я подробнее расскажу о классе JavaBeanUtil, который я использовал, чтобы получить значение поля объекта. В том примере это был FxTransaction.

Конечно, вы скажете, что для получения того же результата можно использовать Apache Commons BeanUtils или одну из его альтернатив. Но мне было интересно покопаться в этом и то, что я изучил, работает намного быстрее любой библиотеки, построенной на основе широко известного Java Reflection.
Технологией, позволяющей избежать очень медленного reflection, является инструкция байткода
В двух словах, техника, которую я собираюсь описать ниже, использует LambdaMetafactory и MethodHandle для динамического создания реализации интерфейса Function. В Function есть единственный метод, который делегирует вызов фактическому целевому методу с кодом, определенным внутри лямбды.
В данном случае, целевой метод — это геттер, который имеет прямой доступ к полю, которое мы хотим прочитать. Также, я должен сказать, что если вы хорошо знакомы с нововведениями, которые появились в Java 8, то вы найдете приведенные ниже фрагменты кода довольно простыми. В противном случае, код может показаться сложным с первого взгляда.
Приведенный ниже метод
Для улучшения производительности я кэширую функцию, созданную динамически, которая в действительности и будет читать значение из поля с именем
Метод
Если посмотреть внимательнее на создание функции, то нам нужно пройтись по полям в
Вышеупомянутый метод
Создание кортежа выполняется с методом
В двух вышеупомянутых методах я использую константу с именем
Обратите внимание, что для сеттеров можно использовать аналогичный подход, используя BiFunction вместо Function.
Для измерения производительности я использовал замечательный инструмент JMH (Java Microbenchmark Harness), который, вероятно, будет частью JDK 12 (Примечание переводчика: да, jmh вошел в java 9). Как вы, возможно, знаете, результат зависит от платформы, поэтому для справки: я буду использовать
Для сравнения я выбрал библиотеку Apache Commons BeanUtils, широко известную большинству Java-разработчиков, и одну из ее альтернатив под названием Jodd BeanUtil, которая, как утверждается, работает почти на 20% быстрее.
Код бенчмарка выглядит следующим образом:
В бенчмарке определено четыре сценария для разных уровней вложенности поля. Для каждого поля JMH выполнит 5 итераций по 3 секунды для разогрева, а затем 5 итераций по 1 секунде для фактического измерения. Каждый сценарий будет повторяться 3 раза для получения более качественных измерений.
Давайте начнем с результатов, собранных для JDK

Oracle JDK 8u191
Самый худший сценарий, использующий подход
Теперь посмотрим, как работает тот же тест с

GraalVM EE 1.0.0-rc9
Полные результаты можно посмотреть здесь с красивым JMH Visualizer.
Такая большая разница получилась из-за того, что JIT-компилятор хорошо знает
Если вам интересно и вы хотите покопаться глубже, я призываю вас взять код из моего репозитория на Github. Имейте в виду, я не советую вам делать самодельный
На этом перевод подошел к концу, а всех желающих мы приглашаем 13 июня на бесплатный вебинар, в рамках которого рассмотрим, чем Docker может быть полезен Java-разработчику: как сделать docker-образ с java-приложением и как с ним взаимодействовать.
В моей статье Specification Pattern (паттерн Спецификация) я специально не упомянул о лежащем в основе компоненте, который сильно помог в реализации. Здесь я подробнее расскажу о классе JavaBeanUtil, который я использовал, чтобы получить значение поля объекта. В том примере это был FxTransaction.

Конечно, вы скажете, что для получения того же результата можно использовать Apache Commons BeanUtils или одну из его альтернатив. Но мне было интересно покопаться в этом и то, что я изучил, работает намного быстрее любой библиотеки, построенной на основе широко известного Java Reflection.
Технологией, позволяющей избежать очень медленного reflection, является инструкция байткода
invokedynamic
. Вкратце, проявление invokedynamic
(или «indy») было самым серьезным нововведением в Java 7, которое позволило проложить путь для реализации динамических языков поверх JVM, используя динамические вызовы методов (dynamic method invocation). Позже, в Java 8, это также позволило реализовать лямбда-выражения и ссылки на методы (method reference), а также улучшить конкатенацию строк в Java 9.В двух словах, техника, которую я собираюсь описать ниже, использует LambdaMetafactory и MethodHandle для динамического создания реализации интерфейса Function. В Function есть единственный метод, который делегирует вызов фактическому целевому методу с кодом, определенным внутри лямбды.
В данном случае, целевой метод — это геттер, который имеет прямой доступ к полю, которое мы хотим прочитать. Также, я должен сказать, что если вы хорошо знакомы с нововведениями, которые появились в 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
. Связывание функций друг с другом в цепочку можно представить в виде вложенных вызовов, похожих на 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);
}
Вышеупомянутый метод
createFunctions
для каждого поля fieldName
и класса, в котором он объявлен, вызывает метод createFunction
, который ищет нужный геттер, используя javaBeanClass.getDeclaredMethods()
. Как только геттер найден, он преобразуется в кортеж Tuple (Tuple из библиотеки Vavr), который содержит возвращаемый геттером тип, и динамически созданную функцию, которая будет вести себя так, как если бы она сама была геттером.Создание кортежа выполняется с методом
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. С ее помощью я могу создать прямую ссылку на метод (direct method handle), основанную на ранее найденном геттере. И, наконец, созданный MethodHandle передается методу createCallSite
, в котором для функции создается тело лямбды с использованием LambdaMetafactory. Оттуда, в конечном счете, мы можем получить экземпляр 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
.Для сравнения я выбрал библиотеку Apache Commons BeanUtils, широко известную большинству Java-разработчиков, и одну из ее альтернатив под названием 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 выполнит 5 итераций по 3 секунды для разогрева, а затем 5 итераций по 1 секунде для фактического измерения. Каждый сценарий будет повторяться 3 раза для получения более качественных измерений.
Результаты
Давайте начнем с результатов, собранных для JDK
8u191
:
Oracle JDK 8u191
Самый худший сценарий, использующий подход
invokedynamic
, намного быстрее, чем самый быстрый из двух других библиотек. Это огромная разница, и если вы сомневаетесь в результатах, вы можете всегда скачать исходный код и поиграться с ним, как вам нравится.Теперь посмотрим, как работает тот же тест с
GraalVM EE 1.0.0-rc9.

GraalVM EE 1.0.0-rc9
Полные результаты можно посмотреть здесь с красивым JMH Visualizer.
Наблюдения
Такая большая разница получилась из-за того, что JIT-компилятор хорошо знает
CallSite
и MethodHandle
и может их заинлайнить (inline), в отличие от подхода с reflection. Кроме того, вы можете увидеть, насколько перспективен GraalVM. Его компилятор делает действительно потрясающую работу, способную значительно повысить производительность reflection.Если вам интересно и вы хотите покопаться глубже, я призываю вас взять код из моего репозитория на Github. Имейте в виду, я не советую вам делать самодельный
JavaBeanUtil
, чтобы использовать его в продакшене. Моя цель — просто показать мой эксперимент и возможности, которые мы можем получить из invokedynamic
.На этом перевод подошел к концу, а всех желающих мы приглашаем 13 июня на бесплатный вебинар, в рамках которого рассмотрим, чем Docker может быть полезен Java-разработчику: как сделать docker-образ с java-приложением и как с ним взаимодействовать.