Ускоряем java-рефлексию в 2023

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Введение

Привет, Хабр.

С момента выхода в свет предыдущей статьи прошёл год с хвостиком, и у меня наконец-то дошли руки до написания исправленной версии, учитывающей предыдущие косяки с замером времени вызова и несправедливо забытую тему доступа к полям классов.

Ну что же, поехали!

Постановка задачи

Имеем в наличии jdk 17, хотим вызывать методы класса по имени и таким же образом обращаться к полям.

Характеристики машины

Intel Core i5-9400f, 16 GB ОЗУ, Windows 11

Замеры различных способов вызова методов

Не будем повторяться, вновь вводя и разъясняя все используемые понятия, вспоминая историю развития рефлексии и периодически пугаясь от её неторопливости.

Поэтому тех читателей, кто не поймёт происходящее, или какую-либо его часть, попрошу обратиться к первоначальной статье, упомянутой в начале.

Что касается методов замера – ошибки повторять мы тоже не будем, поэтому в этот раз используем православный jmh.

Итак, встречайте! Бессмертная тройка претендентов на нашей бенчмарк-арене:

  • Рефлексия. Древний способ, появившийся вместе с сотворением мира выходом jdk 1.0. Чрезвычайно мощный и чрезвычайно медленный (или уже нет? интрига, однако :)).

  • Мета-лямбды. Встроенный в sdk способ, добавленный в один из релизов jdk 8. Довольно ограниченный, так как позволяет вызывать только методы с известной во время компиляции сигнатурой, зато самый быстрый из всех трёх.

  • Динамическое проксирование. Сторонний способ, предоставленный в нашем случае моей библиотекой jeflect. Повторяет функционал рефлективных вызовов за исключением того, что целевой метод должен быть виден относительно класс-лоадера, загружающего прокси. Проще говоря, приватные (или, например, package-private) методы он вызывать не сможет. Медленнее, чем мета-лямбды, но быстрее, чем рефлексия.

Издеваться над испытуемыми будем с помощью вот этого безобидного класса:

public final class Calculator {
    public int add(int left, int right) {
        return left + right;
    }

    public String about() {
        return "Your first project™";
    }
}

Вероятно, кому-то покажется, что двух методов слишком много, но на самом деле для выявления всех особенностей и слабых мест каждого способа их чрезвычайно мало.

По крайней мере, этого хватит, чтобы создать некоторое подобие справедливого соревнования.

Метод about универсально удобен для всех троих, а add добавит проблем рефлексии и динамическим прокси - им придётся потратить дополнительное время на упаковку/распаковку аргументов плюс заставит боксить примитивы.

Наконец, напишем бенчмарки для обоих методов.

Для about:

package com.github.romanqed;

import com.github.romanqed.jeflect.lambdas.Lambda;
import com.github.romanqed.jeflect.lambdas.LambdaFactory;
import com.github.romanqed.jeflect.meta.LambdaType;
import com.github.romanqed.jeflect.meta.MetaFactory;
import com.github.romanqed.jfunc.Exceptions;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup
public class NoArgumentBench {
    // Подготавливаем всё необходимое
    private static final Calculator CALC = new Calculator();
    private static final Method ABOUT = Exceptions.suppress(() -> Calculator.class.getDeclaredMethod("about"));
    private static final MetaFactory META_FACTORY = new MetaFactory();
    @SuppressWarnings("unchecked")
    private static final Function<Calculator, String> META_LAMBDA = META_FACTORY.packLambdaMethod(
            LambdaType.fromClass(Function.class),
            ABOUT
    );
    private static final LambdaFactory LAMBDA_FACTORY = new LambdaFactory();
    private static final Lambda LAMBDA = LAMBDA_FACTORY.packMethod(ABOUT);

    static {
        // Отключаем для рефлексии проверки доступа
        ABOUT.setAccessible(true);
    }

    // Обычный вызов для сравнения
    @Benchmark
    public void benchPlainCall(Blackhole blackhole) {
        blackhole.consume(CALC.about());
    }

    // Вызов через рефлексию
    @Benchmark
    public void benchReflection(Blackhole blackhole) throws Exception {
        blackhole.consume(ABOUT.invoke(CALC));
    }

    // Вызов с помощью мета-лямбд
    @Benchmark
    public void benchMetaLambdas(Blackhole blackhole) {
        blackhole.consume(META_LAMBDA.apply(CALC));
    }

    // Вызов с помощью динамических прокси
    @Benchmark
    public void benchProxies(Blackhole blackhole) throws Throwable {
        blackhole.consume(LAMBDA.invoke(CALC));
    }
}

И для add:

package com.github.romanqed;

import com.github.romanqed.jeflect.lambdas.Lambda;
import com.github.romanqed.jeflect.lambdas.LambdaFactory;
import com.github.romanqed.jeflect.meta.LambdaType;
import com.github.romanqed.jeflect.meta.MetaFactory;
import com.github.romanqed.jfunc.Exceptions;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup
public class TwoArgumentBench {
    // Лямбда-интерфейс для мета-лямбд
    public interface Adder {
        int add(Calculator calculator, int left, int right);
    }

    // Подготавливаем всё необходимое
    private static final Calculator CALC = new Calculator();
    private static final Method ADD = Exceptions.suppress(
            () -> Calculator.class.getDeclaredMethod("add", int.class, int.class)
    );
    private static final MetaFactory META_FACTORY = new MetaFactory();
    private static final Adder META_LAMBDA = META_FACTORY.packLambdaMethod(
            LambdaType.fromClass(Adder.class),
            ADD
    );
    private static final LambdaFactory LAMBDA_FACTORY = new LambdaFactory();
    private static final Lambda LAMBDA = LAMBDA_FACTORY.packMethod(ADD);

    static {
        // Отключаем для рефлексии проверки доступа
        ADD.setAccessible(true);
    }

    // Обычный вызов для сравнения
    @Benchmark
    public void benchPlainCall(Blackhole blackhole) {
        blackhole.consume(CALC.add(5, 6));
    }

    // Вызов через рефлексию
    @Benchmark
    public void benchReflection(Blackhole blackhole) throws Exception {
        blackhole.consume(ADD.invoke(CALC, 5, 6));
    }

    // Вызов с помощью мета-лямбд
    @Benchmark
    public void benchMetaLambdas(Blackhole blackhole) {
        blackhole.consume(META_LAMBDA.add(CALC, 5, 6));
    }

    // Вызов с помощью динамических прокси
    @Benchmark
    public void benchProxies(Blackhole blackhole) throws Throwable {
        blackhole.consume(LAMBDA.invoke(CALC, new Object[]{5, 6}));
    }
}

И да, в отличие от прошлой статьи бенчить рефлексию с включенными проверками доступа мы не будем, ибо грешно смеяться над инвалидами.

Момент истины, запускаем jmh (jmh version 1.36, vm version JDK 17.0.7, OpenJDK 64-Bit Server VM, 17.0.7+7-LTS)… *барабанная дробь*

Benchmark                          Mode  Cnt  Score   Error  Units
NoArgumentBench.benchMetaLambdas   avgt   25  0,388 ± 0,003  ns/op
NoArgumentBench.benchPlainCall     avgt   25  0,388 ± 0,002  ns/op
NoArgumentBench.benchProxies       avgt   25  0,388 ± 0,003  ns/op
NoArgumentBench.benchReflection    avgt   25  2,280 ± 0,075  ns/op
TwoArgumentBench.benchMetaLambdas  avgt   25  0,388 ± 0,003  ns/op
TwoArgumentBench.benchPlainCall    avgt   25  0,387 ± 0,003  ns/op
TwoArgumentBench.benchProxies      avgt   25  0,520 ± 0,005  ns/op
TwoArgumentBench.benchReflection   avgt   25  7,170 ± 0,038  ns/op

Ожидаемо, безоговорочная победа присуждается мета-лямбдам. Практически идентичны обычным вызовам (учитывая погрешность в виде возможных накладных расходов на работу jmh).

Следующими идут прокси, прекрасно показавшие себя на вызовах без параметров и слегка сдавшие позиции из-за танцев с боксингом. Медленнее в ~1.3 раза, при отсутствии параметров идентичны обычным вызовам.

Почётное третье место достаётся рефлексии – не помог даже допинг в виде интринсинков и многочисленных улучшений в JNI. Медленнее в ~5.9 – ~18.5 раз.

Итоги подведены, вопрос окончательно закрыт, и можно переходить к полям.

(Разумеется, существует ещё кодогенерация, но об этом, пожалуй, как-нибудь в другой раз).

Разговоры о полях

В отличие от методов, которым целиком посвящена не только моя предыдущая статья, но и много другого контента, поля часто обходят стороной.

Основная причина этому – пресловутые принципы ООП, благодаря которым редко когда за пределами класса общение с полем происходит напрямую.

В общем-то это замечательно, но иногда, особенно при создании хитрой библиотеки или фреймворка (di, например), рефлективный функционал для общения с полями по имени жизненно необходим.

Поэтому, раз уж мы убедились в важности (относительной, конечно) данного вопроса, давайте заглянем в sdk в поисках нужного инструмента.

Что же мы там видим? Field#get и Field#set. Немного, но это честная работа... Или нет?

Прежде чем бросаться сломя голову запускать 100500 бенчмарков, воспользуемся главным преимуществом открытого исходного кода и заглянем под капот метода get.

@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException {
    if (!override) {
        Class<?> caller = Reflection.getCallerClass();
        checkAccess(caller, obj);
    }
    return getFieldAccessor(obj).get(obj);
}

Что мы здесь видим? Во-первых, небезызвестный checkAccess, повышающий накладные расходы в среднем в два раза. Во-вторых, нельзя не заметить, что доступ к полю осуществляется с помощью какой-то реализации интерфейса FieldAccessor (jdk.internal.reflect.FieldAccessor), создаваемой в недрах рефлексии.

Воспользовавшись отладчиком, чтобы долго не разбираться в хитросплетениях индусского подкапотного кода, добираемся до UnsafeFieldAccessorFactory, всё из того же пакета (напомню, что в других реализациях JVM может вообще не быть этих классов). Преодолев очень много if-else (YandereDev, ты ли это?) и утилитных реализаций, добираемся до jdk.internal.misc.Unsafe и узнаем, как реализован рефлективный доступ к полю в Hotspot.

Итак, здесь тоже замешан JNI. В определенном смысле, конечно, надежда есть (на интринсинки и прочие оптимизации), но что-то подсказывает, что результат бенчмарка нам не понравится.

Кстати, вот он.

package com.github.romanqed;

import com.github.romanqed.jfunc.Exceptions;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup
public class FieldBench {
    // Класс с полем
    public static class FieldHolder {
        public final String helloWorld = "Hello, world!";
    }

    private static final FieldHolder HOLDER = new FieldHolder();

    private static final Field FIELD = Exceptions.suppress(() -> FieldHolder.class.getField("helloWorld"));

    static {
        // Отключаем проверки доступа
        FIELD.setAccessible(true);
    }

    // Обычный доступ к полю
    @Benchmark
    public void benchPlainGet(Blackhole blackhole) {
        blackhole.consume(HOLDER.helloWorld);
    }

    // Доступ к полю через рефлексию
    @Benchmark
    public void benchReflectGet(Blackhole blackhole) throws Exception {
        blackhole.consume(FIELD.get(HOLDER));
    }
}

Результаты же...

Benchmark                   Mode  Cnt  Score   Error  Units
FieldBench.benchPlainGet    avgt   25  0,389 ± 0,010  ns/op
FieldBench.benchReflectGet  avgt   25  2,345 ± 0,142  ns/op

Страшно, очень страшно, если бы мы знали, что это такое, мы не знаем, что это такое. В ~6 раз медленнее.

Кто виноват и что делать?

Насчёт первого вопроса всё не так однозначно, а вот второй даже не стоит – конечно же писать свою реализацию FieldAccessor'а!

Динамическая генерация accessor'а

С первого взгляда задача выглядит нетривиальной, однако на самом деле у нас к данному моменту остаётся всего два пути: кодогенерация и генерация прокси-классов "на лету".

Кодогенерация приводит нас к необходимости создания скрипта на питоне плагина для конкретного сборщика, а это последняя вещь, к которой вообще следует обращаться (что, если потенциальный пользователь нашей библиотеки использует другой сборщик? или вообще не использует?), поэтому остаётся только генерация прокси-классов.

Для тех, кто не читал предыдущую статью или не знает jvm ассемблер, напомню несколько важных вещей, что пригодятся дальше:

  • JVM – стековая машина, и все операции выполняет исходя из данных, расположенных на операнд-стеке.

  • Полный документированный каталог опкодов машины можно найти тут

  • Встроенного средства генерации байт-кода в языке не предусмотрено, поэтому используем эту библиотечку (или любую другую на ваш вкус, в крайнем случае можно сразу заполнять массив байтами).

По традиции, сначала напишем интерфейс, от которого и будем отталкиваться:

interface FieldAccessor {
    Object get(Object instance);

    void set(Object instance, Object value);
}

А теперь, чтобы было легче писать ассемблерный код, добавим ещё для себя простенькую реализацию:

final class AccessorImpl implements FieldAccessor {
    public AccessorImpl() {
        super();
    }

    @Override
    public Object get(Object instance) {
        return ((FieldOwner) instance).value;
    }

    @Override
    public void set(Object instance, Object value) {
        ((FieldOwner) instance).value = (FieldType) value;
    }
}

Внимательные читатели заметят, что в этом скетче отсутствует одна очень важная вещь, и будут совершенно правы. К этому ещё вернёмся, а пока реализуем искомый генератор.

package com.github.romanqed;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.reflect.Field;

public final class ProxyGenerator {
    private static final Type OBJECT = Type.getType(Object.class);
    private static final Loader LOADER = new Loader();

    public interface FieldAccessor {
        Object get(Object instance);

        void set(Object instance, Object value);
    }

    static class Loader extends ClassLoader {
        public Class<?> define(String name, byte[] buffer) {
            return defineClass(name, buffer, 0, buffer.length);
        }
    }

    public static FieldAccessor createAccessor(Field field) {
        // Генерируем имя будущего прокси
        var name = "Accessor" + (field.getDeclaringClass().getName() + field.getName()).hashCode();
        // Проверяем, а не загружено ли уже такое прокси
        Class<?> clazz;
        try {
            clazz = LOADER.loadClass(name);
        } catch (Exception e) {
            clazz = null;
        }
        // Если загружено, то просто создаем экземпляр
        if (clazz != null) {
            try {
                return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        // А если нет, то начинаем генерировать.
        // Создаём генератор будущего прокси-класса,
        // задаём режим автоматического вычисления максимальных размеров стека и локальных переменных
        var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // Объявляем заголовок класса
        // public final class AccessorImpl implements FieldAccessor {
        writer.visit(Opcodes.V11,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                name,
                null,
                OBJECT.getInternalName(),
                new String[]{Type.getInternalName(FieldAccessor.class)});
        // Объявляем пустой конструктор
        // public AccessorImpl()
        var ctor = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null);
        // {
        ctor.visitCode();
        // Загружаем super
        ctor.visitVarInsn(Opcodes.ALOAD, 0);
        // Вызываем его
        ctor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                OBJECT.getInternalName(),
                "<init>",
                "()V",
                false);
        // Не забываем обязательный return; в конце, который компилятор обычно добавляет за нас
        ctor.visitInsn(Opcodes.RETURN);
        // }
        ctor.visitMaxs(0, 0);
        ctor.visitEnd();
        // Имплементируем метод get
        // @Override public Object get(Object instance)
        var get = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "get",
                Type.getMethodDescriptor(OBJECT, OBJECT),
                null,
                null);
        // {
        get.visitCode();
        // Загружаем объект, содержащий поле
        // 1 индекс, потому что 0 индекс у виртуального метода ведет на this
        get.visitVarInsn(Opcodes.ALOAD, 1);
        // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
        get.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        // Получаем поле
        get.visitFieldInsn(Opcodes.GETFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Возвращаем его
        get.visitInsn(Opcodes.ARETURN);
        // }
        get.visitMaxs(0, 0);
        get.visitEnd();
        // Имплементируем метод set
        // @Override public void set(Object instance, Object value)
        var set = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "set",
                Type.getMethodDescriptor(Type.VOID_TYPE, OBJECT, OBJECT),
                null,
                null);
        // {
        set.visitCode();
        // Загружаем объект, содержащий поле
        set.visitVarInsn(Opcodes.ALOAD, 1);
        // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
        set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        // Загружаем будущее значение поля
        set.visitVarInsn(Opcodes.ALOAD, 2);
        // Приводим его к нужному типу
        set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getType()));
        // Обновляем поле
        set.visitFieldInsn(Opcodes.PUTFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Не забываем return;
        set.visitInsn(Opcodes.RETURN);
        // }
        set.visitMaxs(0, 0);
        set.visitEnd();
        // Получаем наш сгенерированный класс в виде массива байтов
        var bytes = writer.toByteArray();
        // Загружаем байт-код в JVM
        clazz = LOADER.define(name, bytes);
        try {
            return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Проверяем:

package com.github.romanqed;

public class Main {
    public static class Data {
        public String value;
    }

    public static void main(String[] args) throws NoSuchFieldException {
        var accessor = ProxyGenerator.createAccessor(Data.class.getField("value"));
        var obj = new Data();
        accessor.set(obj, "hello");
        System.out.println(accessor.get(obj));
    }
}

Удивительно, оно работает! А теперь представим, что нам нужно не строковое поле, а целочисленное...

Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    Accessor865096057.get(Ljava/lang/Object;)Ljava/lang/Object; @7: areturn
  Reason:
    Type integer (current frame, stack[0]) is not assignable to reference type
  Current Frame:
    bci: @7
    flags: { }
    locals: { 'Accessor865096057', 'java/lang/Object' }
    stack: { integer }
  Bytecode:
    0000000: 2bc0 000e b400 12b0 

Упс. Мистер программист? Мистер боксинг примитивов передаёт привет *БАХ*

Чтобы это исправить, понадобится немного шаманской магии. Конкретно в этом случае с int'ом, его необходимо паковать в Integer следующим образом:

// Упаковка
Integer.valueOf(int);
// Распаковка
integer.intValue();

Добавим это в наш генератор - упаковку в get, а распаковку в set.

package com.github.romanqed;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

public final class ProxyGenerator {
    private static final Type OBJECT = Type.getType(Object.class);
    private static final Loader LOADER = new Loader();

    public interface FieldAccessor {
        Object get(Object instance);

        void set(Object instance, Object value);
    }

    static class Loader extends ClassLoader {
        public Class<?> define(String name, byte[] buffer) {
            return defineClass(name, buffer, 0, buffer.length);
        }
    }

    private static Class<?> wrap(Class<?> c) {
        return MethodType.methodType(c).wrap().returnType();
    }

    public static FieldAccessor createAccessor(Field field) {
        // Генерируем имя будущего прокси
        var name = "Accessor" + (field.getDeclaringClass().getName() + field.getName()).hashCode();
        // Проверяем, а не загружено ли уже такое прокси
        Class<?> clazz;
        try {
            clazz = LOADER.loadClass(name);
        } catch (Exception e) {
            clazz = null;
        }
        // Если загружено, то просто создаем экземпляр
        if (clazz != null) {
            try {
                return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        // А если нет, то начинаем генерировать.
        // Создаём генератор будущего прокси-класса,
        // задаём режим автоматического вычисления максимальных размеров стека и локальных переменных
        var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // Объявляем заголовок класса
        // public final class AccessorImpl implements FieldAccessor {
        writer.visit(Opcodes.V11,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                name,
                null,
                OBJECT.getInternalName(),
                new String[]{Type.getInternalName(FieldAccessor.class)});
        // Объявляем пустой конструктор
        // public AccessorImpl()
        var ctor = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null);
        // {
        ctor.visitCode();
        // Загружаем super
        ctor.visitVarInsn(Opcodes.ALOAD, 0);
        // Вызываем его
        ctor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                OBJECT.getInternalName(),
                "<init>",
                "()V",
                false);
        // Не забываем обязательный return; в конце, который компилятор обычно добавляет за нас
        ctor.visitInsn(Opcodes.RETURN);
        // }
        ctor.visitMaxs(0, 0);
        ctor.visitEnd();
        // Имплементируем метод get
        // @Override public Object get(Object instance)
        var get = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "get",
                Type.getMethodDescriptor(OBJECT, OBJECT),
                null,
                null);
        // {
        get.visitCode();
        // Загружаем объект, содержащий поле
        // 1 индекс, потому что 0 индекс у виртуального метода ведет на this
        get.visitVarInsn(Opcodes.ALOAD, 1);
        // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
        get.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        // Получаем поле
        get.visitFieldInsn(Opcodes.GETFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Проверяем, если вдруг тип поля примитив
        var retType = field.getType();
        if (retType.isPrimitive()) {
            // Получаем оберточный тип, синонимичный примитиву (например, int -> Integer)
            var wrapper = Type.getType(wrap(retType));
            // Вызываем его статический метод valueOf
            get.visitMethodInsn(Opcodes.INVOKESTATIC,
                    wrapper.getInternalName(),
                    "valueOf",
                    Type.getMethodDescriptor(wrapper, Type.getType(retType)),
                    false);
        }
        // Возвращаем его
        get.visitInsn(Opcodes.ARETURN);
        // }
        get.visitMaxs(0, 0);
        get.visitEnd();
        // Имплементируем метод set
        // @Override public void set(Object instance, Object value)
        var set = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "set",
                Type.getMethodDescriptor(Type.VOID_TYPE, OBJECT, OBJECT),
                null,
                null);
        // {
        set.visitCode();
        // Загружаем объект, содержащий поле
        set.visitVarInsn(Opcodes.ALOAD, 1);
        // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
        set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        // Загружаем будущее значение поля
        set.visitVarInsn(Opcodes.ALOAD, 2);
        var type = field.getType();
        // Проверяем, если вдруг тип поля примитив
        var toCast = type.isPrimitive() ? wrap(type) : type;
        // Приводим его к нужному типу
        set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(toCast));
        // Распаковываем если надо примитив
        if (type.isPrimitive()) {
            set.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                    Type.getInternalName(toCast),
                    type.getName() + "Value",
                    Type.getMethodDescriptor(Type.getType(type)),
                    false);
        }
        // Обновляем поле
        set.visitFieldInsn(Opcodes.PUTFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Не забываем return;
        set.visitInsn(Opcodes.RETURN);
        // }
        set.visitMaxs(0, 0);
        set.visitEnd();
        // Получаем наш сгенерированный класс в виде массива байтов
        var bytes = writer.toByteArray();
        // Загружаем байт-код в JVM
        clazz = LOADER.define(name, bytes);
        try {
            return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Повторная проверка показывает, что всё сделано правильно.

Усложним задачу – теперь поле становится статическим.

package com.github.romanqed;

public class Main {
    public static class Data {
        public static String value;
    }

    public static void main(String[] args) throws NoSuchFieldException {
        var accessor = ProxyGenerator.createAccessor(Data.class.getField("value"));
        accessor.set(null, "1");
        System.out.println(accessor.get(null));
    }
}

Босс, мы упали:

Exception in thread "main" java.lang.IncompatibleClassChangeError: Expected non-static field com.github.romanqed.Main$Data.value
	at Accessor865096057.set(Unknown Source)
	at com.github.romanqed.Main.main(Main.java:10)

Ничего страшного, просто добавим проверку на наличие модификатора static:

package com.github.romanqed;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public final class ProxyGenerator {
    private static final Type OBJECT = Type.getType(Object.class);
    private static final Loader LOADER = new Loader();

    public interface FieldAccessor {
        Object get(Object instance);

        void set(Object instance, Object value);
    }

    static class Loader extends ClassLoader {
        public Class<?> define(String name, byte[] buffer) {
            return defineClass(name, buffer, 0, buffer.length);
        }
    }

    private static Class<?> wrap(Class<?> c) {
        return MethodType.methodType(c).wrap().returnType();
    }

    public static FieldAccessor createAccessor(Field field) {
        // Генерируем имя будущего прокси
        var name = "Accessor" + (field.getDeclaringClass().getName() + field.getName()).hashCode();
        // Проверяем, а не загружено ли уже такое прокси
        Class<?> clazz;
        try {
            clazz = LOADER.loadClass(name);
        } catch (Exception e) {
            clazz = null;
        }
        // Если загружено, то просто создаем экземпляр
        if (clazz != null) {
            try {
                return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        var isStatic = Modifier.isStatic(field.getModifiers());
        // А если нет, то начинаем генерировать.
        // Создаём генератор будущего прокси-класса,
        // задаём режим автоматического вычисления максимальных размеров стека и локальных переменных
        var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // Объявляем заголовок класса
        // public final class AccessorImpl implements FieldAccessor {
        writer.visit(Opcodes.V11,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                name,
                null,
                OBJECT.getInternalName(),
                new String[]{Type.getInternalName(FieldAccessor.class)});
        // Объявляем пустой конструктор
        // public AccessorImpl()
        var ctor = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null);
        // {
        ctor.visitCode();
        // Загружаем super
        ctor.visitVarInsn(Opcodes.ALOAD, 0);
        // Вызываем его
        ctor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                OBJECT.getInternalName(),
                "<init>",
                "()V",
                false);
        // Не забываем обязательный return; в конце, который компилятор обычно добавляет за нас
        ctor.visitInsn(Opcodes.RETURN);
        // }
        ctor.visitMaxs(0, 0);
        ctor.visitEnd();
        // Имплементируем метод get
        // @Override public Object get(Object instance)
        var get = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "get",
                Type.getMethodDescriptor(OBJECT, OBJECT),
                null,
                null);
        // {
        get.visitCode();
        if (!isStatic) {
            // Загружаем объект, содержащий поле
            // 1 индекс, потому что 0 индекс у виртуального метода ведет на this
            get.visitVarInsn(Opcodes.ALOAD, 1);
            // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
            get.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        }
        // Получаем поле
        get.visitFieldInsn(isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Проверяем, если вдруг тип поля примитив
        var retType = field.getType();
        if (retType.isPrimitive()) {
            // Получаем оберточный тип, синонимичный примитиву (например, int -> Integer)
            var wrapper = Type.getType(wrap(retType));
            // Вызываем его статический метод valueOf
            get.visitMethodInsn(Opcodes.INVOKESTATIC,
                    wrapper.getInternalName(),
                    "valueOf",
                    Type.getMethodDescriptor(wrapper, Type.getType(retType)),
                    false);
        }
        // Возвращаем его
        get.visitInsn(Opcodes.ARETURN);
        // }
        get.visitMaxs(0, 0);
        get.visitEnd();
        // Имплементируем метод set
        // @Override public void set(Object instance, Object value)
        var set = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "set",
                Type.getMethodDescriptor(Type.VOID_TYPE, OBJECT, OBJECT),
                null,
                null);
        // {
        set.visitCode();
        if (!isStatic) {
            // Загружаем объект, содержащий поле
            set.visitVarInsn(Opcodes.ALOAD, 1);
            // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
            set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        }
        // Загружаем будущее значение поля
        set.visitVarInsn(Opcodes.ALOAD, 2);
        var type = field.getType();
        // Проверяем, если вдруг тип поля примитив
        var toCast = type.isPrimitive() ? wrap(type) : type;
        // Приводим его к нужному типу
        set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(toCast));
        // Распаковываем если надо примитив
        if (type.isPrimitive()) {
            set.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                    Type.getInternalName(toCast),
                    type.getName() + "Value",
                    Type.getMethodDescriptor(Type.getType(type)),
                    false);
        }
        // Обновляем поле
        set.visitFieldInsn(isStatic ? Opcodes.PUTSTATIC : Opcodes.PUTFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Не забываем return;
        set.visitInsn(Opcodes.RETURN);
        // }
        set.visitMaxs(0, 0);
        set.visitEnd();
        // Получаем наш сгенерированный класс в виде массива байтов
        var bytes = writer.toByteArray();
        // Загружаем байт-код в JVM
        clazz = LOADER.define(name, bytes);
        try {
            return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

И... Это всё. Совсем. Теперь этот код учитывает все возможные случаи, кроме, конечно наличия модификатора final, при котором было бы неплохо вообще не имплементировать метод set:

package com.github.romanqed;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public final class ProxyGenerator {
    private static final Type OBJECT = Type.getType(Object.class);
    private static final Loader LOADER = new Loader();

    public interface FieldAccessor {
        Object get(Object instance);

        default void set(Object instance, Object value) {
            throw new IllegalStateException("Field is final");
        }
    }

    static class Loader extends ClassLoader {
        public Class<?> define(String name, byte[] buffer) {
            return defineClass(name, buffer, 0, buffer.length);
        }
    }

    private static Class<?> wrap(Class<?> c) {
        return MethodType.methodType(c).wrap().returnType();
    }

    public static FieldAccessor createAccessor(Field field) {
        // Генерируем имя будущего прокси
        var name = "Accessor" + (field.getDeclaringClass().getName() + field.getName()).hashCode();
        // Проверяем, а не загружено ли уже такое прокси
        Class<?> clazz;
        try {
            clazz = LOADER.loadClass(name);
        } catch (Exception e) {
            clazz = null;
        }
        // Если загружено, то просто создаем экземпляр
        if (clazz != null) {
            try {
                return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        var isStatic = Modifier.isStatic(field.getModifiers());
        // А если нет, то начинаем генерировать.
        // Создаём генератор будущего прокси-класса,
        // задаём режим автоматического вычисления максимальных размеров стека и локальных переменных
        var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // Объявляем заголовок класса
        // public final class AccessorImpl implements FieldAccessor {
        writer.visit(Opcodes.V11,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                name,
                null,
                OBJECT.getInternalName(),
                new String[]{Type.getInternalName(FieldAccessor.class)});
        // Объявляем пустой конструктор
        // public AccessorImpl()
        var ctor = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null);
        // {
        ctor.visitCode();
        // Загружаем super
        ctor.visitVarInsn(Opcodes.ALOAD, 0);
        // Вызываем его
        ctor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                OBJECT.getInternalName(),
                "<init>",
                "()V",
                false);
        // Не забываем обязательный return; в конце, который компилятор обычно добавляет за нас
        ctor.visitInsn(Opcodes.RETURN);
        // }
        ctor.visitMaxs(0, 0);
        ctor.visitEnd();
        // Имплементируем метод get
        // @Override public Object get(Object instance)
        var get = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "get",
                Type.getMethodDescriptor(OBJECT, OBJECT),
                null,
                null);
        // {
        get.visitCode();
        if (!isStatic) {
            // Загружаем объект, содержащий поле
            // 1 индекс, потому что 0 индекс у виртуального метода ведет на this
            get.visitVarInsn(Opcodes.ALOAD, 1);
            // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
            get.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
        }
        // Получаем поле
        get.visitFieldInsn(isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD,
                Type.getInternalName(field.getDeclaringClass()),
                field.getName(),
                Type.getDescriptor(field.getType()));
        // Проверяем, если вдруг тип поля примитив
        var retType = field.getType();
        if (retType.isPrimitive()) {
            // Получаем оберточный тип, синонимичный примитиву (например, int -> Integer)
            var wrapper = Type.getType(wrap(retType));
            // Вызываем его статический метод valueOf
            get.visitMethodInsn(Opcodes.INVOKESTATIC,
                    wrapper.getInternalName(),
                    "valueOf",
                    Type.getMethodDescriptor(wrapper, Type.getType(retType)),
                    false);
        }
        // Возвращаем его
        get.visitInsn(Opcodes.ARETURN);
        // }
        get.visitMaxs(0, 0);
        get.visitEnd();
        if (!Modifier.isFinal(field.getModifiers())) {
            // Имплементируем метод set
            // @Override public void set(Object instance, Object value)
            var set = writer.visitMethod(Opcodes.ACC_PUBLIC,
                    "set",
                    Type.getMethodDescriptor(Type.VOID_TYPE, OBJECT, OBJECT),
                    null,
                    null);
            // {
            set.visitCode();
            if (!isStatic) {
                // Загружаем объект, содержащий поле
                set.visitVarInsn(Opcodes.ALOAD, 1);
                // Приводим объект к его типу (сейчас из-за сигнатуры метода он стал Object'ом)
                set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(field.getDeclaringClass()));
            }
            // Загружаем будущее значение поля
            set.visitVarInsn(Opcodes.ALOAD, 2);
            var type = field.getType();
            // Проверяем, если вдруг тип поля примитив
            var toCast = type.isPrimitive() ? wrap(type) : type;
            // Приводим его к нужному типу
            set.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(toCast));
            // Распаковываем если надо примитив
            if (type.isPrimitive()) {
                set.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                        Type.getInternalName(toCast),
                        type.getName() + "Value",
                        Type.getMethodDescriptor(Type.getType(type)),
                        false);
            }
            // Обновляем поле
            set.visitFieldInsn(isStatic ? Opcodes.PUTSTATIC : Opcodes.PUTFIELD,
                    Type.getInternalName(field.getDeclaringClass()),
                    field.getName(),
                    Type.getDescriptor(field.getType()));
            // Не забываем return;
            set.visitInsn(Opcodes.RETURN);
            // }
            set.visitMaxs(0, 0);
            set.visitEnd();
        }
        // Получаем наш сгенерированный класс в виде массива байтов
        var bytes = writer.toByteArray();
        // Загружаем байт-код в JVM
        clazz = LOADER.define(name, bytes);
        try {
            return (FieldAccessor) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

А теперь то, ради чего мы делали всё это.

Замеры различных способов доступа к полю

Уже знакомый код бенчмарка с новым участником:

package com.github.romanqed;

import com.github.romanqed.jfunc.Exceptions;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup
public class FieldBench {
    // Класс с полем
    public static class FieldHolder {
        public final String helloWorld = "Hello, world!";
    }

    // Подготавливаем всё
    private static final FieldHolder HOLDER = new FieldHolder();

    private static final Field FIELD = Exceptions.suppress(() -> FieldHolder.class.getField("helloWorld"));

    private static final ProxyGenerator.FieldAccessor ACCESSOR = ProxyGenerator.createAccessor(FIELD);

    static {
        // Отключаем проверки доступа
        FIELD.setAccessible(true);
    }

    // Обычный доступ
    @Benchmark
    public void benchPlainGet(Blackhole blackhole) {
        blackhole.consume(HOLDER.helloWorld);
    }

    // Рефлективный доступ
    @Benchmark
    public void benchReflectGet(Blackhole blackhole) throws Exception {
        blackhole.consume(FIELD.get(HOLDER));
    }

    // Доступ с помощью только что написанного генератора accessor'ов
    @Benchmark
    public void benchCustomAccessor(Blackhole blackhole) {
        blackhole.consume(ACCESSOR.get(HOLDER));
    }
}

Результаты не могут не радовать:

Benchmark                       Mode  Cnt  Score   Error  Units
FieldBench.benchCustomAccessor  avgt   25  0,518 ± 0,017  ns/op
FieldBench.benchPlainGet        avgt   25  0,390 ± 0,009  ns/op
FieldBench.benchReflectGet      avgt   25  2,319 ± 0,076  ns/op

Accessor оказался медленнее всего в ~1.32 раза!

Выводы

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

Также в который раз напомню прописную истину – иногда ничего из вышеупомянутого не нужно. Совсем.

Быстрого вам кода.

Спасибо за внимание!

Источник: https://habr.com/ru/articles/758664/


Интересные статьи

Интересные статьи

Только что вышла IntelliJ IDEA 2023.2. В этом релизе в IDE появилась куча интересных фичей и важных улучшений.Вы можете скачать последнюю сборку с официального сайта, или из бесплатного приложени...
Из новостей: в Стиме на странице игры теперь не более 2-х видео перед скриншотами, выручка с продаж Hogwarts Legacy в ритейле на конец марта превысила миллиард долларов, Unity сообщила об увольнении...
Статистика не устаёт повторять нам про устойчивую корреляцию между падением скорости загрузки страниц сайта и ростом частоты отказов со снижением конверсии. Я не открою Америку, если ...
Привет, Хабр! Представляю вашему вниманию перевод статьи «Interview with Weld’s main contributor: accelerating numpy, scikit and pandas as much as 100x with Rust and LLVM». Проработав нескольк...
Автор материала, перевод которого мы сегодня публикуем, говорит, что уверен в том, что многие JavaScript-разработчики пользуются, в основном, такими типами данных, как Number, String, Object, Arr...