Заменить отражающий вызов динамически сгенерированным классом - PullRequest
1 голос
/ 10 февраля 2020

У меня есть интерфейс, подобный этому:

public interface Getter {
    Object get(Params params);
}

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

public class GetterImpl implements Getter {

    private final Object target;
    private final Method method; //doStuff method

    public GetterImpl(Object target, Method method) {
        this.target = target;
        this.method = method;
    }

    @Override
    public Object get(Params params) {
        //both the target and arguments depend on Params
        return method.invoke(chooseTarget(params), prepareArgs(params));
    }

    private Object chooseTarget(Params params) {
        if (params.getTargetOverride() != null) {
            return params.getTargetOverride();
        }
        return target;
    }

    private Object[] prepareArgs(Params params) {
        ...
    }
}

Возможно ли вместо этого сгенерировать реализацию класса Getter с эквивалентными логиками c но без отражения? По сути, такой класс:

public class GeneratedGetterImpl implements Getter {

    ...

    @Override
    public Object get(Params params) {
        //somehow call doStuff directly (different method for each generated impl)
        return target.doStuff(prepareArgs(params));
    }
}

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

Это явно не тривиальная задача, но можно ли это сделать с помощью Byte Buddy? Или другая библиотека?

ОБНОВЛЕНИЕ:

Вот моя лучшая попытка:

Target target = new Target();
Method method = Target.class.getMethod("doStuff", Book.class);

//Helper class that computes the new arguments based on the original
Prepare prepare = new Prepare();
Method doPrep = Prepare.class.getMethod("doPrep", Params.class);

Getter getter = (Getter) new ByteBuddy()
            .subclass(Object.class)
            .implement(Getter.class)
            .method(named("get")).intercept(
                    MethodCall.invoke(method).on(target)
                            .withMethodCall(
                                    MethodCall.invoke(doPrep).on(prepare).withAllArguments()
                            ))
            .make()
            .load(getClass().getClassLoader())
            .getLoaded()
            .newInstance();

public static class Prepare {

    public Book doPrep(Params params) {
        return new Book(params.getTitle());
    }
}

Это делает то, что я хочу, но только если целевой метод принимает 1 аргумент (Book в моем случае). Я пытаюсь выяснить, как заставить его возвращать массив, который я затем распространяю при вызове целевого метода.

Например,

public static class Prepare {
    //returns all the arguments
    public Object[] doPrep(Params params) {
        return new Object[] { new Book(params.getTitle()) };
    }
}

Ответы [ 2 ]

5 голосов
/ 10 февраля 2020

Такое средство уже существует в JRE, если мы ограничим его привязкой интерфейса к соответствующему целевому методу.

public static void main(String[] args) throws NoSuchMethodException {
    Function<Double,Double> f1 = create(Math.class.getMethod("abs", double.class));
    System.out.println(f1.apply(-42.0));

    Map<Double,Double> m = new HashMap<>();
    Function<Double,Double> f2 = create(Map.class.getMethod("get", Object.class), m);
    m.put(1.0, 123.0);
    System.out.println(f2.apply(1.0));
}

static Function<Double,Double> create(Method m) {
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodType t = MethodType.methodType(Double.class, Double.class);
    try {
        return (Function)LambdaMetafactory.metafactory(l, "apply",
                MethodType.methodType(Function.class), t.erase(), l.unreflect(m), t)
                .getTarget().invoke();
    } catch(Throwable ex) {
        throw new IllegalStateException(ex);
    }
}
static Function<Double,Double> create(Method m, Object target) {
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodType t = MethodType.methodType(Double.class, Double.class);
    try {
        return (Function)LambdaMetafactory.metafactory(l, "apply",
                MethodType.methodType(Function.class, m.getDeclaringClass()),
                t.erase(), l.unreflect(m), t)
                .getTarget().invoke(target);
    } catch(Throwable ex) {
        throw new IllegalStateException(ex);
    }
}
42.0
123.0

Это демонстрирует, что такие адаптации, как автобокс и приведение в виде необходимые для обобщенных c функций включены, но любые другие адаптации параметров или результатов невозможны и должны выполняться уже существующим кодом украшения. В частности, обработка varargs не включена.

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

1 голос
/ 10 февраля 2020

Используя Byte Buddy, вы можете создать экземпляр MethodCall, который представляет ваш прокси-метод, и использовать его в качестве реализации. Я предполагаю, что вы рассматривали делегирование, которое требует более статичной c модели:

MethodCall.invoke(SomeClass.class.getMethod("foo")).with(...)

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

Что касается вашего обновленного вопроса, я бы порекомендовал вам гибридный подход. Реализуйте некоторый контейнер:

class Builder<T> {
  Builder with<T>(T value);
  T[] toArray();
}

, и затем вы можете использовать Byte Buddy, чтобы вызвать его для создания значения результата:

MethodCall builder = MethodCall.construct(Builder.class.getConstructor());
for (SomeInfoObject info : ...) {
  builder = MethodCall.invoke(Builder.class.getMethod("with", Object.class))
                      .on(builder)
                      .with(toMethodCall(info));
}
builder = MethodCall.invoke(Builder.class.getMethod("toArray")).on(builder);

Цель Byte Buddy - сделать процесс создания кода простым, а не замените написание кода * stati c, который гораздо лучше, если у вас есть такая возможность.

...