$30 off During Our Annual Pro Sale. View Details »

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

Moscow JUG
September 26, 2019

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

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

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

Moscow JUG

September 26, 2019
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

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

    View Slide

  2. Давайте знакомиться
    • Долго работал над разными проектами в аутсорсе
    • Участвую в разработке фреймворка CUBA Platform
    • RnD
    • Developer Advocacy

    View Slide

  3. Reflection везде
    • Dependency Injection
    • Proxy
    • Listeners
    • Aspects
    • Современная Java, скорее всего, не стала бы
    одним из самых распространенных языков
    Reflection

    View Slide

  4. Reflection – dark side
    • Скорость выполнения уменьшается
    • Ошибки переезжают в Runtime
    • Сложность рефакторинга
    • Нет AOT компиляции

    View Slide

  5. View Slide

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

    View Slide

  7. Ради чего мы все это затеваем?
    • Скорость выполнения
    • Поддерживаемость
    • AOT компиляция

    View Slide

  8. Reflection вызовы в JVM
    Вызываем
    java.lang.reflect.Method

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. Inflation
    • Замена JNI на java method
    accessor
    • Можно использовать флаги
    • sun.reflect.inflationThreshold
    • sun.reflect.noInflation
    • ReflectionFactory
    • NativeMethodAccessorImpl
    • DelegatingMethodAccessorImpl
    • NativeConstructorAccessorImpl
    • DelegatingConstructorAccessorImpl

    View Slide

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

    View Slide

  14. Reflection в JVM - Inflation
    • Зачем?
    • Ускоряем вызовы методов
    • Как?
    • inflationThreshold
    • noInflation
    • Какой ценой?
    • Дольше время старта
    • Больше расход памяти

    View Slide

  15. Убираем Reflection
    вручную
    Воспользуемся
    LambdaMetafactory

    View Slide

  16. Убираем reflection с LMF
    • Зачем?
    • Скорость работы
    • Когда?
    • Design-time

    View Slide

  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
    }

    View Slide

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

    View Slide

  19. MethodHandle vs Method
    • API для динамического вызова методов из Java 7
    • MethodHandle – типизированный указатель на метод
    • LambdaMetafactory позволяет «обернуть» вызов метода
    • Связка с методом делается один раз
    • Возможен inline кода лямбды
    • В теории – должно быть быстрее

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  24. Что в итоге
    /**
    * Fire listeners for event type E.
    *
    * @param eventType event class
    * @param event event object
    * @param type of event
    */
    @SuppressWarnings("unchecked")
    public void publish(Class 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);
    }
    }
    }

    View Slide

  25. А как это можно сделать в Spring?
    @Component
    public class LambdaEventListenerFactory extends DefaultEventListenerFactory {
    @Override
    public ApplicationListener>
    createApplicationListener(String beanName, Class> type, Method method) {
    return new ApplicationListenerLambdaMethodAdapter(beanName, type, method);
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  28. Что ещё мы делаем с LMF
    • Getters и Setters в сущностях
    public void setValue(String name, Object value) {
    BiConsumer setter = getMethodsCache().getSetterNN(name);
    setter.accept(this, value);
    }
    public T getValue(String name) {
    Function getter = getMethodsCache().getGetterNN(name);
    return (T) getter.apply(this);
    }

    View Slide

  29. Убираем Reflection вручную - LMF
    • Зачем?
    • Если нужно дешево получить прирост производительности при частых
    вызовах java.lang.reflect.Method
    • Как?
    • Заменить method на MethodHandle/LMF
    • Какой ценой?
    • Пишем немного дополнительного кода
    • На AOT можно (пока) не надеяться

    View Slide

  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

    View Slide

  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

    View Slide

  32. Убираем Reflection в
    исходниках
    Exterminate!

    View Slide

  33. Кодогенерация
    • Зачем?
    • Скорость работы
    • AOT
    • Когда?
    • Design-time
    • Build-time
    • Как?
    • Annotation Processing и разновидности
    • Генерация Bytecode

    View Slide

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

    View Slide

  35. Проблемы в Design-time
    • Сгенерированный код страшно трогать
    • Поменяем код – сломаем IDE
    • Мы начинаем зависеть от IDE
    • Появляются соглашения, которых надо придерживаться

    View Slide

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

    View Slide

  37. Build-time генерация
    • Annotation Processing
    • Используется для написания проектных фреймворков
    • Убираем boilerplate
    • Cloud-native frameworks
    • Модное направление
    • Поддерживают AOT

    View Slide

  38. Annotation Processing
    • Механизм появился в Java 5
    • Большое количество фреймворков
    • QueryDSL
    • Dagger
    • Lombok
    • Легко писать свой
    • КРОК – jXFW

    View Slide

  39. Annotation Processing
    • Для нужных аннотаций создаем Processor
    • При сборке:
    • Для каждой аннотации вызываем Processor
    • Processor генерирует новые файлы
    • Повторяем, пока генерируются новые файлы
    • Есть библиотеки для облегчения создания java исходников
    • JavaPoet
    • Picocog

    View Slide

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

    View Slide

  41. Annotation Processing - цена
    • Есть ограничения – можем генерировать только новые файлы
    • Новый язык – новые шаблоны
    • Нет подсказок в Design-time
    • Если у вас нет поддержки в IDE
    • Увеличивается Build-Time

    View Slide

  42. А Lombok?
    • Нетрадиционный подход
    • Использует com.sun.tools API
    • Поэтому периодически ломается

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  46. Micronaut и Quarkus
    • Схожие подходы
    • Container-first
    • Основная область применения - микросервисы
    • GraalVM support
    • Micronaut
    • Поддерживается Object Computing
    • Свой набор аннотаций + CDI
    • Quarkus
    • Поддерживается RedHat
    • Полагаются на MicroProfile, CDI, JAX-RS

    View Slide

  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

    View Slide

  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.

    View Slide

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

    View Slide

  50. Quarkus – event listener

    View Slide

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

    View Slide

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

    View Slide

  53. Убираем Reflection в
    байткоде

    View Slide

  54. Кодогенерация в байткоде
    • Зачем?
    • Скорость работы
    • Когда?
    • Compile-time
    • Runtime
    • Как?
    • ByteBuddy
    • Javassist
    • CGLib

    View Slide

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

    View Slide

  56. Выбираем подход
    • AOP
    • Build-time weaver
    • Аспект – часть фреймворка
    • Кодогенерация
    • Часть build plugin
    • Можно менять без привязки к фреймворку

    View Slide

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

    View Slide

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

    View Slide

  59. Давайте избавимся от Reflection!
    • Зачем?
    • Для скорости
    • Для AOT
    • Как?
    • Вручную
    • Кодогенерацией
    • Какой ценой?
    • Писать больше кода
    • Изобретать и поддерживать свой инструментарий
    • Отлаживать не то, что написали

    View Slide

  60. Итого
    Поддерживаемость Скорость AOT Сложность реализации
    Reflection
    MethodHandle/LMF
    Annotation Processing
    Cloud-Native
    Frameworks
    Bytecode Generation

    View Slide

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

    View Slide