Уменьшаем количество рефлексии в коде

Уменьшаем количество рефлексии в коде

Поговорим о том, какие есть альтернативы вызовам методов через рефлексию и почему важно уменьшать количество рефлексивных вызовов в коде. Рассмотрим пример кодогенерации, а также использование механизма LambdaMetafactory. Также сделаем небольшой микробенчмарк, который позволит нам сравнить разные способы вызова метода класса.

Видео: https://youtu.be/O7tEuZihzDA

80eadbf0a221aaa4b764854df32fb781?s=128

Deep Refactoring

June 27, 2019
Tweet

Transcript

  1. Уменьшаем количество рефлексии в коде Беляев Андрей, Haulmont @belyaev_andrey

  2. Reflection везде • Dependency Injection • Proxy • Listeners •

    Aspects • Современная Java, скорее всего, не стала бы одним из самых распространенных языков Reflection
  3. Reflection – dark side • Сложнее отладка и поиск ошибки

    • Stacktrace • Типизация • Скорость выполнения уменьшается • AOT компиляция • 60-e в 2020-м • GraalVM • Excelsior (?)
  4. Reflection - что используется? • Большое количество Reflection вызовов –

    работа с метаинформацией • Вызов методов занимает значительную часть в статистике использования Reflection Challenges for Static Analysis of Java Reflection – Literature Review and Empirical Study, Davy Landman, Alexander Serebrenik, Jurgen J. Vinju Centrum Wiskunde & Informatica, Amsterdam, The Netherlands, Eindhoven University of Technology, Eindhoven, The Netherlands
  5. Reflection - как влияет? • Cтатические данные можно кэшировать •

    Вызовы методов все время проверяются JVM @CallerSensitive @ForceInline // to ensure Reflection.getCallerClass optimization @HotSpotIntrinsicCandidate public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, Modifier.isStatic(modifiers) ? null : obj.getClass(), modifiers); } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
  6. А вообще, что мы хотим-то? •Поддерживаемости •Скорости выполнения •AOT компиляции

  7. Что можно сделать?

  8. Ничего • Пусть JVM отдувается за вас • Можно использовать

    флаги • sun.reflect.inflationThreshold • sun.reflect.noInflation • ReflectionFactory • NativeMethodAccessorImpl • DelegatingMethodAccessorImpl • NativeConstructorAccessorImpl • DelegatingConstructorAccessingImpl
  9. Генерация кода в JVM if (++numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass()))

    { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); }
  10. Что мы получаем? • Поддерживаемость • Скорость выполнения(?) • AOT

    компиляцию
  11. Кодогенерация • Отличный способ убрать рефлексивные вызовы • Код работает

    быстро • Никаких проблем с AOT компиляцией
  12. Кодогенерация • Compile-time • Генерация вспомогательных классов (JAXB, QueryDSL) •

    Ресурсы, документация и т.д. • Дополнительный плагин для обработки байткода • Run-time • CGLIB • ByteBuddy • Javassist protected void enhanceSetters(CtClass ctClass) throws NotFoundException, CannotCompileException { for (CtMethod ctMethod : ctClass.getDeclaredMethods()) { ctMethod.addLocalVariable("__prev", setterParamType); ctMethod.addLocalVariable("__new", setterParamType); ctMethod.insertBefore( "__prev = this.get" + StringUtils.capitalize(fieldName) + "();" ); ctMethod.insertAfter( "__new = this.get" + StringUtils.capitalize(fieldName) + "();" + "if (!InstanceUtils.propertyValueEquals(__prev, __new)) {" + " this.propertyChanged(\"" + fieldName + "\", __prev, __new);" + "}" ); } }
  13. Project Lombok

  14. Кодогенерация • Код уже не ваш • Отладка по декомпилированным

    исходникам • Другой Stacktrace • Генерация – отдельная задача • Ошибки в генераторе могут дорого стоить • Нужно обновлять больше инструментов
  15. Кодогенерация I took a glance at the ASM generation and

    its pretty incredible. You guys are working with 3 or 4 code abstractions: javax.lang.model.**, ASM, and java.lang.reflect for some edge case. Two of those libraries uses visitor patterns along with your own visitor patters. Consequently it makes it pretty difficult to figure out whats going on. https://github.com/micronaut-projects/micronaut-core/issues/445
  16. Кодогенерация > Task :compileJava FAILED Note: Creating bean classes for

    1 type elements error: Unexpected error: Illegal name .$HelloControllerDefinition 1 error FAILURE: Build failed with an exception. import io.micronaut.http.annotation.*; @Controller("/hello") public class HelloController { @Get public String index() { return "Hello World"; } } HelloController needs to be defined to be in a package.
  17. Что мы получаем? • Поддерживаемость • Скорость выполнения • AOT

    компиляцию
  18. LambdaMetafactory • Java7 - invokedynamic The Well-Grounded Java Developer. Vital

    techniques of Java 7 and polyglot programming Benjamin J. Evans and Martijn Verburg Foreword by Dr. Heinz Kabutz July 2012
  19. MethodHandle и LambdaMetafactory • Новый API для динамического вызова методов

    • MethodHandle – типизированный указатель на метод • LambdaMetafactory позволяет «обернуть» вызов метода • Связка с методом делается один раз • Возможен inline кода лямбды
  20. LambdaMetafactory @FunctionalInterface interface PropertyChangeListener { void propertyChanged(PropertyChangeEvent e); } @Autowired

    EventProcessor bean; … Instance.PropertyChangeListener l = (e) -> {bean.processEvent(e);} … l.propertyChanged(event);
  21. LambdaMetafactory MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply",

    MethodType.methodType(BiFunction.class), MethodType.methodType(Object.class, Object.class, Object.class), caller.findVirtual( bean.getClass(), method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0])), MethodType.methodType(Object.class, bean.getClass(), method.getParameterTypes()[0])); MethodHandle factory = site.getTarget(); BiFunction listenerMethod = (BiFunction) factory.invoke();
  22. Преимущества LambdaMetafactory • Код проекта не меняется • Stacktrace –

    короче • В теории – должно быть быстрее public class ApplicationListenerMethodAdapter implements GenericApplicationListener { private final Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this.method.invoke(bean, event); handleResult(result); } } public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = funHandler.apply(bean, event); handleResult(result); } }
  23. Что мы получаем? • Поддерживаемость • Скорость выполнения – в

    теории • AOT компиляцию
  24. Проверяем теорию практикой • Будем использовать JMH • Ограничимся простыми

    вызовами get/set @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) public void nativeSetTest(){ user.setValue("name", "someName"); } @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) public Object nativeGetTest(){ return user.getValue("name"); }
  25. Проверяем теорию практикой public class User { private final MethodsCache

    methodCache; private final LambdaMethodsCache lambdaMethodsCache; private final MethodHandleCache methodHandleCache; private UUID id; private String name; public void setValue(String propertyName, Object propertyValue) { setValueNative(propertyName, propertyValue); } public Object getValue(String propertyName) { return getValueNative(propertyName); } //Classic reflection method access public void setValueReflectionCache(String propertyName, Object propertyValue) { methodCache.setValue(propertyName, propertyValue, this); } public Object getValueReflectionCache(String propertyName) { return methodCache.getValue(propertyName, this); } } public void setValueNative(String propertyName, Object propertyValue) { switch (propertyName) { case "name": this.setName((String) propertyValue); return; case "id": this.setId((UUID) propertyValue); return; default: { throw new RuntimeException()); } } }
  26. Проверяем теорию практикой public abstract class AbstractMethodsCache<G, S> { protected

    final Map<String, G> getters = new HashMap<>(); protected final Map<String, S> setters = new HashMap<>(); protected abstract G createGetter(Class clazz, Method method); protected abstract S createSetter(Class clazz, Method method); public abstract Object getValue(String property, Object object); public abstract void setValue(String property, Object value, Object object); } public class LambdaMethodsCache extends AbstractMethodsCache<Function, BiConsumer>{ }
  27. Проверяем теорию практикой GetValue Throughput (ops/us) Execution Time (us/op) lambdaCacheGetTest

    55 0.018 reflectionCacheGetTest 48 0.017 methodHandleCacheGetTest 33 0.019 nativeGetTest 160 0.004 SetValue Throughput (ops/us) Execution Time (us/op) lambdaCacheSetTest 67 0.013 reflectionCacheSetTest 59 0.025 methodHandleCacheSetTest 42 0.029 nativeSetTest 355 0.002
  28. Итого • Reflection будет жить ещё долго • Слишком много

    всего завязано на этом • Кодогенерация • Похоже, будет развиваться • AOT компиляция, serverless и прочее в тренде • LambdaMetafactory • Если пишете свой фреймворк и не хотите кодогенерацию и AOT • Не сложнее и быстрее Reflection • Использование MethodHandle не дает прироста производительности • Связывание с методом – долгая процедура • Можно попробовать статические MethodHandle • А можно просто пить пиво и надеяться на JVM
  29. Спасибо за внимание