Reputation: 15459
I have an interface similar to this:
public interface Getter {
Object get(Params params);
}
that I implement using a reflective call to a different method:
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) {
...
}
}
Is it possible to instead generate a class implementing Getter
with equivalent logic but without reflection? Effectively a class like this:
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));
}
}
I'm looking into using Byte Buddy to generate such a class on the fly, but all the examples provide some sort of statically known method interceptor, and never delegate to a dynamically chosen target and method.
It's clearly not a trivial task, but can this be done with Byte Buddy? Or a different library?
UPDATE:
Here's my best attempt so far:
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());
}
}
This does what I want, but only if the targeted method takes 1 argument (Book
in my case). I'm struggling to figure out how to have it return an array that I then spread when calling the target method.
E.g.
public static class Prepare {
//returns all the arguments
public Object[] doPrep(Params params) {
return new Object[] { new Book(params.getTitle()) };
}
}
Upvotes: 2
Views: 708
Reputation: 44042
Using Byte Buddy, you can create a MethodCall
instance that represents your proxied method and use it as an implementation. I assume that you looked into delegation which requires a more static model:
MethodCall.invoke(SomeClass.class.getMethod("foo")).with(...)
You can also provide other method call instances as arguments to methods to achieve what you have in your example code.
As for your updated question, I'd recommend you a hybrid approach. Implement some container:
class Builder<T> {
Builder with<T>(T value);
T[] toArray();
}
and then you can use Byte Buddy to invoke it for creating your result value:
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's goal is to make weaving code easy, not to replace writing static code which is the much better option if you have the opportunity.
Upvotes: 1
Reputation: 298469
Such a facility does already exists in the JRE, if we restrict it to binding an interface to a matching target method.
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
This demonstrates that adaptations like auto-boxing and casting as required for generic functions are included, but any other adaptations of parameters or results are not possible and have to be performed by pre-existing decorating code. Most notably, varargs processing is not included.
The documentation is exhaustive. It’s strongly recommended to read it in all details before using the class. But the things you can do wrong here, are similar to the things you can do wrong when implementing your own bytecode generator.
Upvotes: 6