Usando ByteBuddy, ¿puedo implementar un método de instancia llamando a otro y transformando el resultado?

Por ejemplo (ejemplo de juguete):

public abstract class Foo {
  public String bar() {
    return "bar";
  }

  public abstract int baz();
}

Dado lo anterior, ¿puedo implementar baz de manera que llame a bar() y devuelva la longitud de la cadena devuelta? Es decir, como si fuera:

public int baz() {
  return bar().length();
}

Ingenuamente, intenté lo siguiente:

Method bar = Foo.class.getDeclaredMethod("bar");
Method baz = Foo.class.getDeclaredMethod("baz");

Method length = String.class.getDeclaredMethod("length");

Foo foo = new ByteBuddy()
  .subclass(Foo.class)
  .method(ElementMatchers.is(baz))
  .intercept(
    MethodCall.invoke(bar)                 // call bar()...
      .andThen(MethodCall.invoke(length))  // ... .length()?
  ).make()
  .load(Foo.class.getClassLoader())
  .getLoaded()
  .newInstance();

System.out.println(foo.baz());

Sin embargo, parece que me equivoqué al pensar que andThen() se invoca en el valor de retorno de la primera invocación; parece que se invoca en la instancia generada.

Exception in thread "main" java.lang.IllegalStateException: 
  Cannot invoke public int java.lang.String.length() on class Foo$ByteBuddy$sVgjXXp9
    at net.bytebuddy.implementation.MethodCall$MethodInvoker$ForContextualInvocation
    .invoke(MethodCall.java:1667)

También probé un interceptor:

class BazInterceptor {
  public static int barLength(@This Foo foo) {
    String bar = foo.bar();
    return bar.length();
  }
}

Con:

Foo foo = new ByteBuddy()
  .subclass(Foo.class)
  .method(ElementMatchers.is(baz))
  .intercept(MethodDelegation.to(new BazInterceptor()))
  // ...etc.

Esto se ejecutó, pero produjo el resultado sin sentido 870698190, y establecer puntos de interrupción y / o agregar declaraciones de impresión en barLength() sugirió que nunca se llamaría; así que claramente no entiendo bien los interceptores o @This.

¿Cómo puedo hacer que ByteBuddy invoque un método y luego invoque otro en su valor de retorno?


Según la respuesta de k5_: BazInterceptor funciona si cualquiera :

  • delegamos en new BazInterceptor(), como arriba, pero hacemos de barLength() un método de instancia, o:
  • dejamos barLength() un método de clase, pero delegamos a BazInterceptor.class en lugar de a una instancia.

Sospecho que 870698190 estaba delegando a hashCode() de la instancia BazInterceptor, aunque en realidad no lo comprobé.

3
David Moles 5 jul. 2017 a las 23:42

2 respuestas

La mejor respuesta

Utiliza una instancia como interceptor, lo que significa que se prefieren los métodos de instancia (tal vez los métodos estáticos no se acepten en absoluto). Hay un método de instancia que coincide con la firma de su método int baz(), es int hashCode(). El número que obtiene es el código hash de la instancia new BazInterceptor().

Opciones que conozco:

  • Elimina static de barLength de esa manera se usará realmente para la interceptación.
  • Agrega la clase como interceptor .intercept(MethodDelegation.to(BazInterceptor.class))

Preferiría la segunda opción ya que no está utilizando ningún campo / estado de la instancia BazInterceptor.

1
k5_ 6 jul. 2017 a las 21:42

Actualmente, no existe una buena forma en Byte Buddy, pero sería una función fácil de agregar. Puede realizar un seguimiento del progreso en GitHub. Lo agregaré una vez que encuentre algo de tiempo.

Si desea implementar tales llamadas encadenadas hoy, puede implementarlas en código Java e insertar este código usando el componente Advice. Alternativamente, puede escribir el código de bytes de manera más explícita creando su propio ByteCodeAppender basado en MethodInvocation instancias donde tiene que cargar los argumentos manualmente.

2
Rafael Winterhalter 6 jul. 2017 a las 22:53