Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

Что там у 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); } }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Проверяем теорию практикой 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

Slide 21

Slide 21 text

Проверяем теорию практикой - 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) };

Slide 22

Slide 22 text

Проверяем теорию практикой - скорость • Будем использовать 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

Slide 23

Slide 23 text

Проверяем теорию практикой 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

Slide 24

Slide 24 text

Что в итоге /** * 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); } } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Что ещё мы делаем с 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); }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Кто самый быстрый? 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

Slide 31

Slide 31 text

Кто самый быстрый? 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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Пример 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; } }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

А теперь – 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"; } }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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.

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Quarkus – event listener

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Кодогенерация в байткоде • Задача – оповещения об изменении значения • Решение – 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); } }

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Генерация байткода - пример 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);" + "}" ); } }

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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