Class Retransformation with Bytebuddy Agent - java

I am writing a Java agent with ByteBuddy API. Therefore, I want to get in touch with the method delegation of classes that are alredy loaded using the retransformation capabilities of the Bytebuddy DSL. When I start the application with the -javaagent parameter everything works fine and the console output gets changed but when attaching the java agent at runtime the agentmain method is executed but the console output does not get changed. Maybe im missing some further ByteBuddy configuration. Any help would be appreciate!
Here is the agent code :
public class AgentMain {
private static final String CLASS = "testing.Test";
private static final String METHOD = "doSomething";
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain...");
new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.named(INSTRUMENTED_CLASS))
.transform(new AgentBuilder.Transformer() {
#Override
public DynamicType.Builder transform(
DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule javaModule
)
{
return builder.method(named(INTERCEPTED_METHOD))
.intercept(MethodDelegation.to(Interceptor.class));
}
}).installOn(instrumentation);
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("Running agentmain...");
new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.named(INSTRUMENTED_CLASS))
.transform(new AgentBuilder.Transformer() {
#Override
public DynamicType.Builder transform(
DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule javaModule
)
{
return builder.method(named(INTERCEPTED_METHOD))
.intercept(MethodDelegation.to(Interceptor.class));
}
}).installOn(instrumentation);
}
}
public class Interceptor {
public static void doSomething(String string) throws Exception {
System.out.println("Intercepted! ");
}
}
Here is the application code :
public class Main {
public static void main(String args[]) throws Exception {
while (true) {
Thread.sleep(1000);
String say = "Not intercepted!";
Test test = new Test();
test.doSomething(say);
}
}
}
Here is the code to attach the agent:
public class Attacher {
public static void attach(String jarFile, String pid) {
try {
ByteBuddyAgent.attach(new File(jarFile), pid);
}
catch (Exception e) {
e.printStackTrace();
}
}
}

You probably need to add .disableClassFormatChanges() as most JVMs do not support changing the shape of classes upon a retransformation.
Also, consider registering an AgentBuilder.Listener to see why a class cannot be transformed. The instrumentation API suppresses all errors otherwise.
Normally, when retransforming, the Advice API is better suited for transformation. It supports most features of the delegation API but works slightly different.

Related

How can I manage (or pass as an argument) my config json to start the testing of my vertx application?

I have a small vertx application with an AppLauncher class that extend of VertxCommandLauncher and I set a appConfig.json with the typical config parameters :
public class AppLauncher extends VertxCommandLauncher implements VertxLifecycleHooks {
public static void main(String[] args) {
new AppLauncher().dispatch(args);
}
#Override
public void afterConfigParsed(JsonObject config) {
AppConfig.INSTANCE.setConfig(config);
}
To run my application in my IDE I put in edit configuration my main class (Applauncher.java) and the arguments :
run io.vertx.covid.verticle.MainVerticle -conf../vertx-application/src/main/resources/appConfig.json
This is my test class:
#BeforeAll
static void deployVerticles(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(BaseVerticle.class.getName(),testContext
.succeeding(id->testContext.completeNow()));
}
This is my BaseVerticle class that all my verticles extends from:
public abstract class BaseVerticle extends AbstractVerticle {
public static String CONTEXT_PATH = AppConfig.INSTANCE.getConfig().getString(Constants.CONTEXT_PATH);
}
And this is my AppConfig class :
public enum AppConfig {
INSTANCE;
private JsonObject config;
public JsonObject getConfig() {
return config;
}
public void setConfig(JsonObject config) {
this.config = config;
}
}
Everything works, but if I would like to test it in a separete way then I deploy my verticles but I have a Nullpointer in the CONTEXT_PATH (BaseVerticle class) because the config (suppose to be taken from appConfig.json) is null.
I haven't found a way to pass the arguments with my appConfig.json or should I call to the main method passing the arguments?
I like to do something that is similar to profiles in my vertx application.
If you set an environment variable with the key vertx-config-path before the vertx instance is initialized, you can control where vertx's config retriever (you might need to add vert-config to your gradle/maven dependencies) gets the configuration from.
In your launcher, you can do something like the following, which will give you the ability to add profile based config files to your resources folder conf/config-%s.json where %s is the profile name:
public class CustomLauncher extends Launcher {
public static final String ACTIVE_PROFILE_PROPERTY = "APP_ACTIVE_PROFILE";
private static final CLI cli = CLI.create("main")
.addOption(new Option()
.setShortName("p")
.setLongName("profile")
);
public static void main(String[] args) {
initDefaults(Arrays.asList(args));
new CustomLauncher().dispatch(args);
}
public static void executeCommand(String cmd, String... args) {
initDefaults(Arrays.asList(args));
new CustomLauncher().execute(cmd, args);
}
public static void initDefaults(List<String> args) {
System.setProperty(LoggerFactory.LOGGER_DELEGATE_FACTORY_CLASS_NAME, SLF4JLogDelegateFactory.class.getName());
CommandLine parse = cli.parse(args);
String profile = parse.getOptionValue("p");
if (profile != null && !profile.isEmpty()) {
System.setProperty(ACTIVE_PROFILE_PROPERTY, profile);
System.setProperty("vertx-config-path", String.format("conf/config-%s.json", profile));
}
}
}
Then in your test, instead of relaying on vertx test extension to inject vertx for you, you can initialize it by yourself and control the profile (aka which config file to load) like the following:
private static Vertx vertx;
#BeforeAll
public static void deployVerticles(VertxTestContext testContext) {
CustomLauncher.initDefaults(Arrays.asList("--profile", "test"))
vertx = Vertx.vertx();
ConfigRetriever.create(vertx).getConfig(asyncResult -> {
if (asyncResult.succeeded()) {
JsonObject config = asyncResult.result();
DeploymentOptions deploymentOptions = new DeploymentOptions()
.setConfig(config);
vertx.deployVerticle(BaseVerticle.class.getName(), deploymentOptions);
} else {
// handle failure
}
});
}
Then when you run your application, instead of providing -conf, you can use -p or --profile
I also highly recommend to get familiar with vertx-config as you can also get env variables, k8s config maps, and much more.
EDIT: I also highly recommend to move to Kotlin if possible, makes the async-code much easier to handle in an imperative way (with Coroutines). It's very hard to deal with libraries like Vert.x in Java compared to languages like Kotlin.
I solved my problem creating a verticle with the config stuffs (vertx-config documentation), here is my verticle config class:
public class ConfigVerticle extends AbstractVerticle {
protected static Logger logger = LoggerFactory.getLogger(ConfigVerticle.class);
public static JsonObject config;
#Override
public void start() throws Exception {
ConfigStoreOptions fileStore = new ConfigStoreOptions()
.setType("file")
.setOptional(true)
.setConfig(new JsonObject().put("path", "conf/appConfig.json"));
ConfigStoreOptions sysPropsStore = new ConfigStoreOptions().setType("sys");
ConfigRetrieverOptions options = new ConfigRetrieverOptions().addStore(fileStore).addStore(sysPropsStore);
ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
retriever.getConfig(ar -> {
if (ar.failed()) {
logger.info("Failed to retrieve config from appConfig.json");
} else {
config = ar.result();
vertx.deployVerticle(MainVerticle.class.getName(), new DeploymentOptions().setConfig(config));
}
});
}
}
And my MainVerticle.class I pass the new configuration like this:
public class MainVerticle extends AbstractVerticle {
#Override
public void start(){
vertx.deployVerticle(BackendVerticle.class.getName(), new DeploymentOptions().setConfig(config()));
}
}
Then, my simpleTests :
#ExtendWith(VertxExtension.class)
public class BaseCovidTest {
protected WebClient webClient;
#BeforeEach
void initWebClient(Vertx vertx){
webClient = WebClient.create(vertx);
}
#BeforeAll
static void deployVerticles(Vertx vertx, VertxTestContext vertxTestContext) {
vertx.deployVerticle(ConfigVerticle.class.getName() ,vertxTestContext
.succeeding(id-> {
try {
vertxTestContext.awaitCompletion(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
vertxTestContext.completeNow();
}));
}
}
And everything works, thanks #Tom that inspired me to fix it!

How to monitor the invocation of methods in abstract class using java agent and ASM?

What I want to do is to monitor the invocation of JUnit 4 test methods. The reason I must do this by myself is: I need to record the executed classes during the execution of each test method. So I need to insert some instructions to the test method so that I know when the test start/end and those recorded classes are executed by which test entity. So I need to filter the test methods on my own.
Actually, the reason why I am doing this is not relevant to the question, as I did not mention "JUnit", "test" in the title. The problem can still be a problem in other similar cases.
The case I have is like this:
public abstract class BaseTest {
#Test
public void t8() {
assert new C().m() == 1;
}
}
public class TestC extends BaseTest{
// empty
}
I have also modified Surefire's member argLine so that my agent will be attached (premain mode) when Surefire launch a new JVM process to execute tests.
In my agent class:
public static void premain(String args, Instrumentation inst){
isPreMain = true;
agentArgs = args;
log("args: " + args);
parseArgs(args);
inst.addTransformer(new TestTransformer(), true);
}
My transformer class:
public class TestTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
log("TestTransformer: transform: " + className);
...
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
RecordClassAdapter mca = new RecordClassAdapter(cw, className);
cr.accept(mca, 0);
return cw.toByteArray();
}
}
In my ClassVisitor adapter class:
class RecordClassAdapter extends ClassVisitor {
...
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
mv = new RecordMethodAdapter (...);
return mv;
}
}
In my MethodVisitor adapter class:
class RecordMethodAdapter extends MethodVisitor {
public void visitCode() {
mv.visitCode();
if (isTestMethod){
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(INVOKESTATIC, MyClass, "entityStarted",
"(Ljava/lang/String;)V", false);
}
}
}
Sadly, I found that the abstract class will not get into the transform method, thus I can not instrument the t8 method. TestC should be executed as a test class, but I can never monitor the invocation of TestC.t8.
There are several opportunities to inject logging into the test via the JUnit API. There is no need for instrumentation.
For a very simple setup:
public class BaseTest {
#Test
public void t8() {
System.out.println("Running test "+getClass().getName()+".t8() [BaseTest.t8()]");
}
#Test
public void anotherMethod() {
System.out.println("Running test "
+getClass().getName()+".anotherMethod() [BaseTest.anotherMethod()]");
}
}
public class TestC extends BaseTest {
#Rule
public TestName name = new TestName();
#Before
public void logStart() throws Exception {
System.out.println("Starting test "+getClass().getName()+'.'+name.getMethodName());
}
#After
public void logEnd() throws Exception {
System.out.println("Finished test "+getClass().getName()+'.'+name.getMethodName());
}
}
which will print
Starting test class TestC.t8
Running test TestC.t8() [BaseTest.t8()]
Finished test class TestC.t8
Starting test class TestC.anotherMethod
Running test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test class TestC.anotherMethod
You can also implement your own rule. E.g. ad-hoc:
public class TestB extends BaseTest {
#Rule
public TestRule notify = TestB::decorateTest;
static Statement decorateTest(Statement st, Description d) {
return new Statement() {
#Override public void evaluate() throws Throwable {
System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
st.evaluate();
System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
}
};
}
}
Or as a reusable rule that can be inserted via a single-liner into a test class
public class LoggingRule implements TestRule {
public static final LoggingRule INSTANCE = new LoggingRule();
private LoggingRule() {}
#Override
public Statement apply(Statement base, Description description) {
Logger log = Logger.getLogger(description.getClassName());
log.setLevel(Level.FINEST);
Logger.getLogger("").getHandlers()[0].setLevel(Level.FINEST);
String clName = description.getClassName(), mName = description.getMethodName();
return new Statement() {
#Override
public void evaluate() throws Throwable {
log.entering(clName, mName);
String result = "SUCCESS";
try {
base.evaluate();
}
catch(Throwable t) {
result = "FAIL";
log.throwing(clName, mName, t);
}
finally {
log.exiting(clName, mName, result);
}
}
};
}
}
used as simple as
public class TestB extends BaseTest {
#Rule
public LoggingRule log = LoggingRule.INSTANCE;
}
A different approach is implementing a custom test runner. This allows to apply a behavior to an entire test suite, as test suites are implemented via runners as well.
public class LoggingSuiteRunner extends Suite {
public LoggingSuiteRunner(Class<?> klass, RunnerBuilder builder)
throws InitializationError {
super(klass, builder);
}
#Override
public void run(RunNotifier notifier) {
notifier.addListener(LOG_LISTENER);
try {
super.run(notifier);
} finally {
notifier.removeListener(LOG_LISTENER);
}
}
static final RunListener LOG_LISTENER = new RunListener() {
public void testStarted(Description d) {
System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
}
public void testFinished(Description d) {
System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
}
public void testFailure(Failure f) {
Description d = f.getDescription();
System.out.println("Failed test "+d.getClassName()+"."+d.getMethodName()
+": "+f.getMessage());
};
};
}
This may get applied to an entire test suite, i.e. still inheriting test methods from BaseTest, you may use
#RunWith(LoggingSuiteRunner.class)
#SuiteClasses({ TestB.class, TestC.class })
public class TestA {}
public class TestB extends BaseTest {}
public class TestC extends BaseTest {}
which will print
Starting test TestB.t8
Running test TestB.t8() [BaseTest.t8()]
Finished test TestB.t8
Starting test TestB.anotherMethod
Running test TestB.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestB.anotherMethod
Starting test TestC.t8
Running test TestC.t8() [BaseTest.t8()]
Finished test TestC.t8
Starting test TestC.anotherMethod
Running test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestC.anotherMethod
These are only pointers, to suggest studying the API which allows even more. Another point to consider, is that depending on the method you’re using for launching the tests (you mentioned a maven plugin), there might be support for adding a global RunListener right there, without the need to alter the test classes.

Bytebuddy - Intercept java.lang.RuntimeException constructor

Iam trying to intercept the constructor RuntimeException(String). Iam trying to use Advice as mentioned here and shown here. But the methods onEnter(String message) or onExit(String message). My instrumenting class (inside a different jar):
public class Instrumenting {
private static final String CLASS_NAME = "java.lang.RuntimeException";
public static void instrument(Instrumentation instrumentation) throws Exception {
System.out.println("[Instrumenting] starting to instrument '" + CLASS_NAME + "'");
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile("C:\\Users\\Moritz\\Instrumenting\\dist\\Instrumenting.jar"));
File temp = Files.createTempDirectory("tmp").toFile();
ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, instrumentation).inject(Collections.singletonMap(
new TypeDescription.ForLoadedType(RuntimeExceptionIntercept.class),
ClassFileLocator.ForClassLoader.read(RuntimeExceptionIntercept.class)));
new AgentBuilder.Default()
.ignore(ElementMatchers.none())
.with(new AgentBuilder.InjectionStrategy.UsingInstrumentation(instrumentation, temp))
.type(ElementMatchers.named(CLASS_NAME))
.transform((DynamicType.Builder<?> builder, TypeDescription td, ClassLoader cl, JavaModule jm) ->
builder
.visit(Advice.to(RuntimeExceptionIntercept.class)
.on(ElementMatchers.isConstructor())
)
).installOn(instrumentation);
System.out.println("[Instrumenting] done");
}
public static class RuntimeExceptionIntercept {
#Advice.OnMethodEnter
public static void onEnter(String message) throws Exception {
System.err.println("onEnter: " + message);
}
#Advice.OnMethodExit
public static void onExit(String message) throws Exception {
System.err.println("onExit: " + message);
}
}
}
How its called:
public class Main {
public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception {
Instrumenting.instrument(instrumentation);
}
public static void main(String[] args) throws MalformedURLException, IOException {
new RuntimeException("message");
}
}
Output:
[Instrumenting] starting to instrument 'java.lang.RuntimeException'
[Instrumenting] done
What am I doing wrong?
The class is already loaded when your agent is running and you have not specified for example RedefinitionStrategy.RETRANSFORM. Therefore, your agent will not reconsider already loaded classes. Note that you should set a more specific ignore matcher then that.
By the way, advice is inlined in the target class, your injection is not required.

How to register MemberSubstitution when using ByteBuddy Maven plug-in?

I'd like to use a MemberSubstitution to rewrite accesses to a particular field. For this I'd like to implement a Plugin to apply this change at compile time using the ByteBuddy Maven plug-in. How can I register the substitution in this case?
Update to give some more context:
Here's the class I'd like to modify:
public class Foo {
private final String FOO = "FOO!";
private final String BAR = "BAR!";
public String test() {
return FOO;
}
}
Here's my Plugin:
public class HookInstallingPlugin implements Plugin {
#Override
public boolean matches(TypeDescription target) {
return target.getName().equals("Foo");
}
#Override
public Builder<?> apply(Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
Field f = null;
try {
f = Class.forName("com.example.foo.Foo").getDeclaredField("BAR");
} catch (NoSuchFieldException | SecurityException | ClassNotFoundException e) {
e.printStackTrace();
}
builder = builder.visit(MemberSubstitution.strict()
.field(ElementMatchers.named("FOO"))
.onRead()
.replaceWith(f)
.on(named("test")));
return builder;
}
#Override
public void close() throws IOException {
}
I'd expect test() to return "BAR!", but it actually returns "FOO!", and this is confirmed when examining the byte code using javap. The type isn't altered, but I can canform that the apply() method was run.
A plugin has two methods:
matches determines what types to instrument.
apply instruments thos types. It provides a DynamicType.Builder to register your changes.
A MemberSubstitution is a decorator, not a replacement for methods. Thus it is registered via the visit method on the DynamicType.Builder:
builder = builder.visit(MemberSubstitution.strict().field(...).onWrite().stub().on(...));
This would for example remove all field writes of matched fields in all methods within the supplied matcher in the last argument.

How to advice constructor of original class after define a field using bytebuddy

I am trying to define a field to a class and use it with advice. I try this with normal methods but i can't use this with constructor.I try using
.constructor(ElementMatchers.any())
.intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(MethodListener.class)))
but then advice not running. I have code as follows..
Agent
new AgentBuilder.Default()
.with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
.type(ElementMatchers.nameContains("BalConnectorCallback"))
.transform((builder, typeDescription, classLoader, module) -> builder
.defineField("contextTimer", Timer.Context.class)
.method(ElementMatchers.any())
.intercept(Advice.to(MethodListener.class))
).installOn(instrumentation);
This is my advice
public class MethodListener {
#Advice.OnMethodEnter
public static void enter(#Advice.Origin String method,
#Advice.AllArguments Object[] para,
#Advice.FieldValue(value = "contextTimer", readOnly = false) Timer.Context contextTimer)
throws Exception {
if (getMethodName(method).equals("BalConnectorCallback")) {
contextTimer = metricServer.getResponsesTime().start();
}
}
#Advice.OnMethodExit
public static void exit(#Advice.Origin String method,
#Advice.FieldValue("contextTimer") Timer.Context contextTimer)
throws Exception {
if (getMethodName(method).equals("done")) {
contextTimer.stop();
}
}
}
How to advice constructor with define a field?
Hi got this problem corrected by using
.constructor(ElementMatchers.any())
.intercept(Advice.to(ConstructorAdvice.class))
and Created a another Advice for constructor as follows
public class ConstructorAdvice {
#Advice.OnMethodExit
public static void enter(#Advice.Origin String method,
#Advice.AllArguments Object[] para,
#Advice.FieldValue(value = "contextTimer", readOnly = false) Timer.Context contextTimer)
throws Exception {
if (getMethodName(method).equals("BalConnectorCallback")) {
contextTimer = metricServer.getResponsesTime().start();
}
}

Categories