Overwriting method with MethodDelegation silently ignored when applied from ByteBuddy agent
See original GitHub issueMy use case
I am looking into using ByteBuddy for weaving in some extra logic in one of our methods related to performing DB calls. Since the method is called very often, and the extra logic involves a ConcurrentHashMap
being accessed (and in the worst case, written to), we want to avoid doing this in the “real” production code.
Instead, we are trying to create a dynamic type which only gets used when we run our integration tests. We use this by utilizing the @BeforeSuite
annotation in testng
, to let us install the ByteBuddy Agent and our transformations at an early-enough stage.
The class we are trying to replace is DatabaseMediator
in the example below.
What works
ByteBuddyAgent.install();
new AgentBuilder.Default()
.ignore( none() )
.with( AgentBuilder.InitializationStrategy.NoOp.INSTANCE )
.with( AgentBuilder.TypeStrategy.Default.REBASE )
.with( AgentBuilder.RedefinitionStrategy.REDEFINITION )
.type( named( DatabaseMediator.class.getName() ) ).transform( new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform( DynamicType.Builder<?> builder, TypeDescription typeDescription, lassLoader classLoader, JavaModule javaModule ) {
return new ByteBuddy()
.redefine( DatabaseMediator.class )
.method( named( "getQuery" ) )
.intercept( FixedValue.value( "OVERWRITTEN VALUE BY BYTEBUDDY!" ) );
}
} )
.installOnByteBuddyAgent();
The approach above overwrites our private String getQuery( String queryId )
method with a new dummy method, returning the fixed string above. When running this, all DB queries fail since they try to execute OVERWRITTEN VALUE BY BYTEBUDDY!
as an SQL query.
Unfortunately though, this is not fully enough for our use case. 😉
What does not work
This is closer to what we actually need:
@BeforeSuite
public void beforeSuite() {
Builder<DatabaseMediator> databaseMediatorBuilder = new ByteBuddy()
.rebase( DatabaseMediator.class )
.method( named( "getQuery" ) )
.intercept( MethodDelegation.to( SeenQueriesInterceptor.class ) );
// Added to avoid simple errors in the definition (will be silently ignored by `ByteBuddyAgent`)
databaseMediatorBuilder.make();
ByteBuddyAgent.install();
new AgentBuilder.Default()
.ignore( none() )
.with( NoOp.INSTANCE )
.with( AgentBuilder.TypeStrategy.Default.REDEFINE )
.with( AgentBuilder.RedefinitionStrategy.REDEFINITION )
.type( named( DatabaseMediator.class.getName() ) ).transform( new Transformer() {
@Override
public Builder<?> transform( Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule ) {
return databaseMediatorBuilder;
}
} )
.installOnByteBuddyAgent();
}
The SeenQueriesInterceptor
looks like this:
public static class SeenQueriesInterceptor {
@RuntimeType
public static Object intercept( @SuperCall Callable<?> zuper, @AllArguments Object[] args ) throws Exception {
System.out.println( "Hello world from method interceptor" );
return "foo";
}
}
Unfortunately, whenever I install the transformation above, the transformation is completely ignored and no exceptions are thrown. The original getQuery()
method is executed instead.
Things tested thus far
Apart from the @BeforeSuite
example above, I’ve also tried calling my interceptor from a simpler example like this (note, no ByteBuddy Agent involved):
public static class Foo {
public String getQuery( String id ) {
return "value from Foo";
}
}
@BeforeSuite
public void beforeSuite() throws Exception {
DynamicType.Loaded<Foo> loaded = new ByteBuddy()
.subclass( Foo.class )
.method( isDeclaredBy( Foo.class ) )
.intercept( MethodDelegation.to( SeenQueriesInterceptor.class ) )
.make()
.load( Foo.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER );
Foo instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
String s = instance.getQuery( "foo" );
}
public static class SeenQueriesInterceptor {
@RuntimeType
public static Object intercept( @SuperCall Callable<?> zuper, @AllArguments Object[] args ) throws Exception {
System.out.println( "Hello world from method interceptor" );
return "foo";
}
}
The above works, but note an important difference here: the getQuery()
method is public; the real DatabaseMediator.getQuery()
method which I’m trying to replace is private
.
I feel quite stuck here. Any ideas on what could be the problem here?
One thing that is a bit frustrating in this is that whenever I try to do these transformations via the ByteBuddy Agent, it feels like all errors are silently disregarded. That’s why I move the actual transformation out of the transform()method and added the explicit
databaseMediatorBuilder.make(). This was helpful in making sure the
SeenQueriesInterceptor.intercept()` method has the correct method signature. 🙏
I guess there isn’t any way to enable more logging from the ByteBuddy Agent, if something goes wrong with a particular transformation? It’s much easier to debug errors which gets printed somewhere than just the dreaded “nothing works, and no exception is thrown”-type of errors. 😅
Thanks in advance for any help with this, appreciated.
Issue Analytics
- State:
- Created 2 years ago
- Comments:5 (5 by maintainers)
Just for reference, instead of:
you would write
The former does not what you expect as
WithErrorsOnly
does inherit all nested classes of its parent. You can also addwithTransformationsOnly
what will print errors and transformed classes.That the delegation example is working surprises me, but I assume that there’s something else going wrong.
The default behavior of a
ClassFileTransformer
does exactly that: disregard the error. You can register anAgentBuilder.Listener
to be notified of any errors, for example by printing them to the console.As for your setup, you would probably want rebasing if you were using a
MethodDelegation
. The original code would be copied to another method if you require aSuperCall
what retransformation does not allow. I assume this would be what the exception would be telling you, too.Rather then method delegation, you should probably use
Advice
which allows you to add logic to a method without requiring any additional methods. You should then also use.disableClassFormatChanges()
.