Андрей Беляев — Уменьшаем количество рефлексии в коде

3fc5b5eb32bd3b48d7810fd67b37f9a1?s=47 Moscow JUG
September 26, 2019

Андрей Беляев — Уменьшаем количество рефлексии в коде

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

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

3fc5b5eb32bd3b48d7810fd67b37f9a1?s=128

Moscow JUG

September 26, 2019
Tweet

Transcript

  1. Уменьшим количество рефлексии в коде! Зачем? Как? Какой ценой? Беляев

    Андрей, Haulmont @belyaev_andrey
  2. Давайте знакомиться • Долго работал над разными проектами в аутсорсе

    • Участвую в разработке фреймворка CUBA Platform • RnD • Developer Advocacy
  3. Reflection везде • Dependency Injection • Proxy • Listeners •

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

    переезжают в Runtime • Сложность рефакторинга • Нет AOT компиляции
  5. None
  6. Прежде, чем начинать искоренять рефлексию в своем приложении, сначала убедитесь,

    что это – корень ваших проблем и нет более простых способов эти проблемы исправить. Нужно точно понимать, что нужно конкретно вам, для этого обязательно производите замеры, профилирование, тестирование, опросы разработчиков и пользователей. Не руководствуйтесь слухами, общепринятым мнением, докладами с конференций и т.д. Помните, что все врут. WARNING
  7. Ради чего мы все это затеваем? • Скорость выполнения •

    Поддерживаемость • AOT компиляция
  8. Reflection вызовы в JVM Вызываем java.lang.reflect.Method

  9. java.lang.reflect.Method#invoke public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,

    InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
  10. java.lang.reflect.Method#invoke public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,

    InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
  11. Reflection вызовы в JVM • JNI Accessor • Быстрый старт

    • Медленно работает (Java -> JNI -> Java) • Pure Java Accessor • Долго создавать • Быстро работает class NativeMethodAccessorImpl extends MethodAccessorImpl { //Class body private static native Object invoke0(Method var0, Object var1, Object[] var2); }
  12. Inflation • Замена JNI на java method accessor • Можно

    использовать флаги • sun.reflect.inflationThreshold • sun.reflect.noInflation • ReflectionFactory • NativeMethodAccessorImpl • DelegatingMethodAccessorImpl • NativeConstructorAccessorImpl • DelegatingConstructorAccessorImpl
  13. Inflation - NativeMethodAccessorImpl 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); }
  14. Reflection в JVM - Inflation • Зачем? • Ускоряем вызовы

    методов • Как? • inflationThreshold • noInflation • Какой ценой? • Дольше время старта • Больше расход памяти
  15. Убираем Reflection вручную Воспользуемся LambdaMetafactory

  16. Убираем reflection с LMF • Зачем? • Скорость работы •

    Когда? • Design-time
  17. notifyBtn.addClickListener(new Button.ClickListener() { @Override public void buttonClick(Button.ClickEvent event) { //Event

    handling code } }); Case - Listeners • Нужно делать обработчики событий для UI • UI экраны не привязаны к Spring Lifecycle notifyBtn.addClickListener(e -> { //Show notification code }); notifyBtn.addClickListener(this::showNotification); private void showNotification(Button.ClickEvent event) { //Show notification code } @Subscribe("notifyBtn") private void showNotification(Button.ClickEvent event) { //Show notification code }
  18. Что там у Spring? public class ApplicationListenerMethodAdapter { private final

    Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this.method.invoke(bean, event); handleResult(result); } }
  19. MethodHandle vs Method • API для динамического вызова методов из

    Java 7 • MethodHandle – типизированный указатель на метод • LambdaMetafactory позволяет «обернуть» вызов метода • Связка с методом делается один раз • Возможен inline кода лямбды • В теории – должно быть быстрее
  20. Проверяем теорию практикой 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); } //More Setters and Getters here } https://github.com/cuba-rnd/entity-lambda-accessors-benchmark
  21. Проверяем теорию практикой - Getter @Override protected Function createGetter(Class clazz,

    Method getter) { MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), caller.findVirtual(clazz, getter.getName(), MethodType.methodType(getter.getReturnType())), MethodType.methodType(getter.getReturnType(), clazz)); MethodHandle factory = site.getTarget(); return (Function) factory.invoke(); } https://github.com/cuba-rnd/entity-lambda-accessors-benchmark (objectInstance) -> { clazz::getter(objectInstance) };
  22. Проверяем теорию практикой - скорость • Будем использовать 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"); } https://github.com/cuba-rnd/entity-lambda-accessors-benchmark
  23. Проверяем теорию практикой 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 https://github.com/cuba-rnd/entity-lambda-accessors-benchmark
  24. Что в итоге /** * Fire listeners for event type

    E. * * @param eventType event class * @param event event object * @param <E> type of event */ @SuppressWarnings("unchecked") public <E> void publish(Class<E> eventType, E event) { if (events != null) { Consumer[] eventListeners = events.get(eventType); if (eventListeners != null) { for (Consumer listener : eventListeners) { listener.accept(event); } TriggerOnce triggerOnce = eventType.getAnnotation(TriggerOnce.class); if (triggerOnce != null) { unsubscribe(eventType); } } }
  25. А как это можно сделать в Spring? @Component public class

    LambdaEventListenerFactory extends DefaultEventListenerFactory { @Override public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerLambdaMethodAdapter(beanName, type, method); } }
  26. ApplicationListenerLambdaMethodAdapter public ApplicationListenerLambdaMethodAdapter(String beanName, Class<?> targetClass, Method method) { super(beanName,

    targetClass, method); try { handler = createListenerHandler(targetClass, method); } catch (Throwable throwable) { throw new ApplicationContextException("Cannot create handler for a listener method: "+method.getName(), throwable); } } @Override protected Object doInvoke(Object... args) { log.info("Invoking Lambda method adapter"); Object bean = getTargetBean(); return handler.apply(bean, args[0]); }
  27. ApplicationListenerLambdaMethodAdapter public ApplicationListenerLambdaMethodAdapter(String beanName, Class<?> targetClass, Method method) { super(beanName,

    targetClass, method); try { handler = createListenerHandler(targetClass, method); } catch (Throwable throwable) { throw new ApplicationContextException("Cannot create handler for a listener method: "+method.getName(), throwable); } } @Override protected Object doInvoke(Object... args) { log.info("Invoking Lambda method adapter"); Object bean = getTargetBean(); return handler.apply(bean, args[0]); }
  28. Что ещё мы делаем с LMF • Getters и Setters

    в сущностях public void setValue(String name, Object value) { BiConsumer setter = getMethodsCache().getSetterNN(name); setter.accept(this, value); } public <T> T getValue(String name) { Function getter = getMethodsCache().getGetterNN(name); return (T) getter.apply(this); }
  29. Убираем Reflection вручную - LMF • Зачем? • Если нужно

    дешево получить прирост производительности при частых вызовах java.lang.reflect.Method • Как? • Заменить method на MethodHandle/LMF • Какой ценой? • Пишем немного дополнительного кода • На AOT можно (пока) не надеяться
  30. Кто самый быстрый? 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
  31. Кто самый быстрый? 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
  32. Убираем Reflection в исходниках Exterminate!

  33. Кодогенерация • Зачем? • Скорость работы • AOT • Когда?

    • Design-time • Build-time • Как? • Annotation Processing и разновидности • Генерация Bytecode
  34. Design-time генерация • Aвтоматизация рутины • Специфичный для проекта/фреймворка код

    • Генерация кода по внешним ресурсам notifyBtn.addClickListener(this::showNotification); private void showNotification(Button.ClickEvent event) { //Show notification code }
  35. Проблемы в Design-time • Сгенерированный код страшно трогать • Поменяем

    код – сломаем IDE • Мы начинаем зависеть от IDE • Появляются соглашения, которых надо придерживаться
  36. Кодогенерация в Design-time • Зачем? • Автоматизируйте рутину • Генерируйте

    boilerplate • Как? • Поддержка IDE • Какой ценой? • Сложно модифицировать сгенерированный код • При изменении шаблонов генерации – массовый рефакторинг руками • Готовьтесь писать и поддерживать свой IDE plugin
  37. Build-time генерация • Annotation Processing • Используется для написания проектных

    фреймворков • Убираем boilerplate • Cloud-native frameworks • Модное направление • Поддерживают AOT
  38. Annotation Processing • Механизм появился в Java 5 • Большое

    количество фреймворков • QueryDSL • Dagger • Lombok • Легко писать свой • КРОК – jXFW
  39. Annotation Processing • Для нужных аннотаций создаем Processor • При

    сборке: • Для каждой аннотации вызываем Processor • Processor генерирует новые файлы • Повторяем, пока генерируются новые файлы • Есть библиотеки для облегчения создания java исходников • JavaPoet • Picocog
  40. Пример public class Person { private int age; private String

    name; @BuilderProperty public void setAge(int age) { this.age = age; } @BuilderProperty public void setName(String name) { this.name = name; } //Getters here } public class PersonBuilder { private Person object = new Person(); public Person build() { return object; } public PersonBuilder setName(java.lang.String value) { object.setName(value); return this; } public PersonBuilder setAge(int value) { object.setAge(value); return this; } }
  41. Annotation Processing - цена • Есть ограничения – можем генерировать

    только новые файлы • Новый язык – новые шаблоны • Нет подсказок в Design-time • Если у вас нет поддержки в IDE • Увеличивается Build-Time
  42. А Lombok? • Нетрадиционный подход • Использует com.sun.tools API •

    Поэтому периодически ломается
  43. А теперь – Reflection! @ApplicationScoped public class AppLifecycleBean { private

    static final Logger LOGGER = LoggerFactory.getLogger("ListenerBean"); @Inject MyOtherBean bean; void onStart(@Observes StartupEvent ev) { LOGGER.info("The application is starting...{}", bean.hello()); } void onStop(@Observes ShutdownEvent ev) { LOGGER.info("The application is stopping... {}", bean.bye()); } } @ApplicationScoped public class MyOtherBean { public String hello() { return "hello"; } public String bye() { return "bye bye"; } }
  44. Cloud-native frameworks • Зачем? • Уменьшение расхода памяти • Уменьшение

    времени старта • Ahead of Time компиляция (AOT) • Как? • Минимальное использование Reflection • Кодогенерация везде, где возможно
  45. Cloud-native frameworks и AOT • Зачем? • Не нужно тащить

    JVM • Быстрый старт • Малый расход памяти • Недостатки? • Нет JIT компиляции • Substrate VM What Support Status Dynamic Class Loading / Unloading Not supported Reflection Supported (Requires Configuration) Dynamic Proxy Supported (Requires Configuration) Java Native Interface (JNI) Mostly supported Unsafe Memory Access Mostly supported InvokeDynamic Bytecode and Method Handles Not supported Finalizers Not supported References Mostly supported Security Manager Not supported JVMTI, JMX, other native VM interfaces Not supported
  46. Micronaut и Quarkus • Схожие подходы • Container-first • Основная

    область применения - микросервисы • GraalVM support • Micronaut • Поддерживается Object Computing • Свой набор аннотаций + CDI • Quarkus • Поддерживается RedHat • Полагаются на MicroProfile, CDI, JAX-RS
  47. 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
  48. Micronaut – hello world! > 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.
  49. Quarkus – event listener @ApplicationScoped public class AppLifecycleBean { @Inject

    MyOtherBean bean; void onStart(@Observes StartupEvent ev) { LOGGER.info("The application is starting...{}", bean.hello()); } void onStop(@Observes ShutdownEvent ev) { LOGGER.info("The application is stopping... {}", bean.bye()); } }
  50. Quarkus – event listener

  51. Quarkus – event listener public class AppLifecycleBean_Observer_onStart_fd71b5e0b implements InjectableObserverMethod {

    public AppLifecycleBean_Observer_onStart_fd71b5e0b (InjectableBean var1) { this.declaringProvider = var1; this.observedType = StartupEvent.class; } public void notify(EventContext var1) { InjectableBean var3 = this.declaringProvider; Object var2 = null; Object var4 = ((InjectableReferenceProvider)var3).get((CreationalContext)var2); var4 = ((ClientProxy)var4).arc_contextualInstance(); Object var5 = var1.getEvent(); ((AppLifecycleBean)var4).onStart((StartupEvent)var5); } }
  52. Убираем рефлексию в исходниках • Зачем? • Если нужна скорость

    исполнения • Если нужно AOT • Как? • Annotation Processing • Cloud-native Frameworks • Какой ценой? • Отлаживаете не то, что написали • Добавляется ещё один инструмент
  53. Убираем Reflection в байткоде

  54. Кодогенерация в байткоде • Зачем? • Скорость работы • Когда?

    • Compile-time • Runtime • Как? • ByteBuddy • Javassist • CGLib
  55. Кодогенерация в байткоде • Задача – оповещения об изменении значения

    • Решение – AOP или кодогенерация public void setName(String name) { Object oldValue = this.name; Object newValue = name; this.name = name; if (ObjectUtils.notEqual(oldValue, newValue)) { this.propertyChanged(“name", oldValue, newValue); } }
  56. Выбираем подход • AOP • Build-time weaver • Аспект –

    часть фреймворка • Кодогенерация • Часть build plugin • Можно менять без привязки к фреймворку
  57. Генерация байткода - пример 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);" + "}" ); } }
  58. Кодогенерация в байткоде • Зачем? • Если нужна скорость •

    Если объем генерируемого кода невелик • Если генерируемый код должен быть language-agnostic • Как? • Build Plugin • Какой ценой? • Отладка по декомпилированному коду • Сложно реализовывать логику
  59. Давайте избавимся от Reflection! • Зачем? • Для скорости •

    Для AOT • Как? • Вручную • Кодогенерацией • Какой ценой? • Писать больше кода • Изобретать и поддерживать свой инструментарий • Отлаживать не то, что написали
  60. Итого Поддерживаемость Скорость AOT Сложность реализации Reflection MethodHandle/LMF Annotation Processing

    Cloud-Native Frameworks Bytecode Generation
  61. Спасибо за внимание Теперь можно задавать вопросы, обсуждать подходы, троллить

    и т.д.