Question: track field access
I'm trying to intercept read access to class fields happening from some of its own methods. While I can easily do something like that by intercepting their getters, not all parts of the code access the fields using getters.
I've found several snippets trying to do more than that, generally altering behavior in some way and generally incomplete enough for me to try a different variant. I want to keep all original functionality but simply track what fields were read with their name and value.
You will have to modify the classes that access the field for this. MemberSubstitution offers facilities for that. You could for example add a method call to a tracking method before any such access.
I got it to work with something like this
.transform((builder, type, classLoader, module, protectionDomain) ->
builder.visit(MemberSubstitution.strict().field(fieldType(Foo.class)).replaceWithChain(
MemberSubstitution.Substitution.Chain.Step.ForDelegation.to(intercept),
MemberSubstitution.Substitution.Chain.Step.OfOriginalExpression.INSTANCE
).on(any()))
)
and this interceptor
public class FooTracking {
public static Foo intercept(@MemberSubstitution.Origin Field field, @MemberSubstitution.This Object instance) {
System.out.println("Intercepted Foo: " + field.getName() );
return null;
}
}
As you can see I still make use of the original implementation and just return null from my intercept. Since I don't know whether a read or an assignment is happening that worked fine for me. However, it would be nice to be able to differentiate between them since I'd like to only capture read events. Rather than returning null I could also simply use reflection to return the proper return value and not use the original but the problem would once again be the differentiation of whether assignment or read is happening. I'm also not sure if that would be any more performant than the above. So my questions are:
- How to differentiate between assign and read when intercepting?
- What is the most performant way to do this?
I found there is an onRead() method
.transform((builder, type, classLoader, module, protectionDomain) ->
builder.visit(MemberSubstitution.strict().field(fieldType(Foo.class)).onRead().replaceWithChain(
MemberSubstitution.Substitution.Chain.Step.ForDelegation.to(intercept),
MemberSubstitution.Substitution.Chain.Step.OfOriginalExpression.INSTANCE
).on(any()))
So that solves that problem leaving mostly the one about performance. Given that I will probably be accessing the value anyways using reflection on my interceptor, I assume there is no gains from leaving the original expression as the final step in the chain.
You should be able to simply return void, the chain value is not used in your example. As long as you end with a call to the original expression.
Void seems to work but I also noticed some weird behavior with my setup. This is what I got
private static void setupParameterTracking(ClassLoader newClassLoader) throws NoSuchMethodException {
// Install Byte Buddy agent and set up instrumentation for the new classloader
ByteBuddyAgent.install(new ByteBuddyAgent.AttachmentProvider() {
@Override
public Accessor attempt() {
return Accessor.Simple.of(newClassLoader);
}
});
Method intercept = Tracking.class.getDeclaredMethods()[0];
new AgentBuilder.Default(new ByteBuddy().with(TypeValidation.DISABLED))
.disableClassFormatChanges()
.type(ElementMatchers.hasSuperType(named("some.ClassName")))
.transform((builder, type, classLoader, module, protectionDomain) ->
builder
.visit(
Advice.to(ParameterTrackingAdvice.class).withExceptionPrinting().on(
nameContains("execute").or(named("isDereferenceAfterExecution"))
)
)
.visit(MemberSubstitution.strict().field(fieldType(Foo.class)).onRead().replaceWithChain(
MemberSubstitution.Substitution.Chain.Step.ForDelegation.to(intercept),
MemberSubstitution.Substitution.Chain.Step.OfOriginalExpression.INSTANCE
).on(any()))
)
.installOnByteBuddyAgent();
}
I noticed that it randomly fails to actually do the member substitution. I don't see any warnings or errors but I can see that none of my break points are hit in my intercept method and that it ands up loading the classes much faster the first time when that happens. The advice, on the other hand, consistently works. This step uses the newClassLoader before anything else does anything with it and the newClassLoader deals with only classes that are not already in the System classLoader classpath. It varies but I'd say it fails more than 50% of the time.
I added
.with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
.with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
And started seeing this on failures:
java.lang.IllegalStateException: No argument with index 0 available for net.bytebuddy.asm.MemberSubstitution$Target$ForMember@15c0a8ad
at net.bytebuddy.asm.MemberSubstitution$Substitution$Chain$Step$ForDelegation$OffsetMapping$ForArgument$Resolved.apply(MemberSubstitution.java:3949)
at net.bytebuddy.asm.MemberSubstitution$Substitution$Chain$Step$ForDelegation.resolve(MemberSubstitution.java:3001)
at net.bytebuddy.asm.MemberSubstitution$Substitution$Chain.resolve(MemberSubstitution.java:1843)
at net.bytebuddy.asm.MemberSubstitution$Replacement$Binding$ForMember.make(MemberSubstitution.java:7426)
at net.bytebuddy.asm.MemberSubstitution$SubstitutingMethodVisitor.visitFieldInsn(MemberSubstitution.java:8285)
at net.bytebuddy.jar.asm.ClassReader.readCode(ClassReader.java:2443)
at net.bytebuddy.jar.asm.ClassReader.readMethod(ClassReader.java:1512)
at net.bytebuddy.jar.asm.ClassReader.accept(ClassReader.java:745)
This still happens if I change from on(any()) to on(isMethod()) or on(isMethod().and(not(isStatic())))
This is my tracking method:
public class Tracking {
public static void track(@MemberSubstitution.Origin Field field, @MemberSubstitution.This Object instance) {
System.out.println(field.getName()+" "+instance.toString());
}
}
I noticed also that when it fails with the above error for one if fails for all classes that would have been modified with the member substitution.
It seems like the annotations are not visible on the specified method. Could you check using reflection yourself?