Your first javaagent
The JVM is a great platform, it provides anything you need to write applications but also to instrument them. There are multiple ways to do that, and generally your IoC framework does that but the most generic way to instrument any code - including the JVM itself - is to write a javaagent.
What does it look like?
If you never used a javaagent here is what its user interface looks like:
java \
-javaagent:/path/to/agent.jar \
-cp *.jar \
the.main.ClassName
It is a standard java command but we added a -javaagent option. You can even use multiple agents at once now and they will be executed in the order they are defined:
java \
-javaagent:/path/to/agent1.jar \
-javaagent:/path/to/agent2.jar \
-cp *.jar \
the.main.ClassName
What is a javaagent?
A javaagent can be seen as a callback before the main. There are actually two way to "attach" - link to the JVM - a javaagent:
- Before the actual application entry point (main) is executed,
- Once the application starts using the programmatic API.
Both options are pretty much the same in terms of API but the previous samples using the java option -javaagent will trigger the first case automatically. The last option can be interesting for frameworks but is a bit less powerful because you don't know what was executed before your agent runs and therefore it can happen you run too late to actually let your agent be useful. Typically, if you want to change the way a class behaves but the class is already loaded, you will need to reload the class changing its code. It is doable but has some limitations (think in terms of class relationships) and therefore is less reliable.
Side note: this post is not about the reloadable javaagent but this is actually a feature of the JVM.
If you want to see in a very trivial mode what a javaagent is, you can modelize it this way - note this is just pseudo code to help you understand and will likely not compile:
public class Main {
public static void main(String... args) {
runAgent(getAgentArgs(), getJVM());
runApplication(args);
}
}
This snippet shows that the agent is intended to run before the actual application code, has its own configuration (the agent args are not the JVM args) and has access to some part of the JVM.
When using a javaagent?
A javaagent being a callback in the JVM, it has a lot of usages. However the two most known cases are:
- Changing the executed bytecode of the classes to add monitoring data - tracing, metrics, ..., debugging information etc....
- Starting a server not directly linked to the JVM - for monitoring purposes. Prometheus was, for instance, using this implementation coupled with JMX as a fallback in cases the application is not able to expose a prometheus exporter directly. In this case, it is recommended to use a shutdown hook to stop the server properly - and potentially flush some data if it requires some communication with a third party remote server.
If you take JavaEE/JakartaEE, everything is based on either a contract (interface, class) or annotations, so a javaagent could read your application code and replace it by the actual "implementation". You can see it as a "runtime generation" or "bytecode generation" (by opposition to source generation).
Similarly, most monitoring framework will instrument your code to add metrics or tracing on methods and render the result in a UI enabling you to profile the application easily. Java Mission Control, the APM included now in the JVM, is doing that for instance.
Another example of bytecode instrumentation is JaCoCo which is used to estimate the code coverage of your test suite. JaCoCo is enabled by bytecode instrumentation - and generally using its javaagent even if in some cases it does not like in Arquillian case. The additional code will check methods - instructions actually - are ran during the test suite or not enabling to report which part of the source code is not tested.
So at the end, even if the usages are pretty different, you still end up either modifying the executed code or just using the lifecycle of the agent to start/stop some feature.
How to write a javaagent?
As mentionned before, an agent can be attached - configured to be started - to the JVM either before the main entry point or after. In the first case it is called "pre main" and in the last case it is called "agent main". Both cases have their own entrypoint/callback which looks like a main but with a different signature:
- First parameter is the agent arguments,
- Second parameter is the Instrumentation instance of the JVM.
Let's write a first javaagent. To start we need a class, let's call it MyAgent. It is a normal java class. We will add a premain method with the two mentionned parameters:
public class MyAgent {
public void premain(final String agentArgs, final Instrumentation instrumentation) {
// starting the agent
}
}
To activate the javaagent you must configure it in the META-INF/MANIFEST.MF - exactly as your main sets up Main-Class entry:
Premain-Class: com.company.MyAgent
If you prefer to attach the agent programmatically you will need Agent-Class entry and the agentmain method:
public class MyAgent {
public void agentmain(final String agentArgs, final Instrumentation instrumentation) {
// starting the agent
}
}
Agent-Class: com.company.MyAgent
However, in this last case, you will not use -javaagent option but some code to attach your agent:
import com.sun.tools.attach.VirtualMachine;
// in some class
final String agentJar = "/path/to/agent.jar"; // can be found from the classloader if present
final String pid = ProcessHandle.current().pid(); // java 9, if you run on java 8 use ManagementFactory.getRuntimeMXBean().getName().split('@')[0]
VirtualMachine.attach(pid).loadAgent(agentJar);
Two points are interesting in this snippet:
- The attach API is in com.sun namespace and will likely be done by reflection since some JVM can not provide it,
- The attach API - last line - is quite simple but it depends on the PID and agent path. As mentionned in the comment, there is now a standard API to get the PID but if you run in java 8 there is an undocumented workaround to use. Getting the agent path is also doable if the agent is in the classpath (you get the agent class resource - so MyAgent.class.replace('.','/') + ".class" - and then unwrap the URL to get back the file path.
FInally, to be complete on the way to configure a javaagent, there are two other manifest entries you can be interested in:
Can-Redefine-Classes: true
Can-Retransform-Classes: true
These two options will enable to mutate a class definition at runtime (rechanging the bytecode and redefining the class at runtime). However if you rely on that feature, ensure to fully understand what it means. In practise, it is rarely needed except for hot reloading solutions.
How to configure a javaagent
In previous part we saw that the agent entry point gets args. However we said these args are not the JVM args so where are they coming from? From the javaagent line itself. Javaagent args can be passed after the javaagent path and the separator =. All the string after this separator is passed to the agent entry point and the agent interpretes its completely freely. In other words, you can pass properties, JSON, YAML, plain string etc...
Here are some example:
-javaagent:/path/agent.jar=somestring
-javaagent:/path/agent.jar=key1=val1|key2=val2|....
-javaagent:/path/agent.jar={"foo":"bar","dummy":0}
There is no standard there so it mainly depends on the way you will parse it and the configuration you expose. However note that the javaagent classes are generally visible from the application so to avoid conflicts you either must shade them or just avoid to use dependencies when possible.
Javaagent sample
To illustrate what can do a javaagent I will write a simple javaagent which takes as parameter a whitelist and a blacklist of classes and will throw in the static block of any class matching this set an exception.
What can it be used for? Prevent some class you don't control to be loaded without the usage of a Security Manager which has other side effects on other parts of the application - not only the classloading.
The first thing for this implementation is to parse the parameters since we need to handle a blacklist and a whitelist. We will use the following format:
arg1=val1|arg2=val2
To parse this kind string we can use this code:
private static Map<String, String> parseArgs(final String agentArgs) {
return Stream.of(agentArgs.split("\\|"))
.map(String::trim)
.filter(it -> !it.isEmpty())
.map(it -> {
final int eq = it.indexOf("=");
if (eq > 0) {
return new AbstractMap.SimpleEntry<>(it.substring(0, eq), it.substring(eq + 1));
}
return new AbstractMap.SimpleEntry<>(it, "true");
})
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
This code simply splits the args (on |) then split each pairs to create a map Entry and finally we aggregate all parameters in a map. At this point we can get a Map<String, String> representing the parameters which is more than enough to start.
Then we need to convert two parameters (include for the whitelist and exclude for the blacklist) to a list of String of list of Predicate<String> - the predicate enabling to handle more than just a direct exact value:
private Collection<Predicate<String>> toPredicateCollection(final String raw) {
return raw == null ? emptySet() : Stream.of(raw.split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.map(v -> (Predicate<String>) n -> n.startsWith(v))
.collect(toSet());
}
This is a trivial implementation but you can find a more complex - justifying the predicate usage - on my github.
I will not detail how to deal with the include-exclude or exclude-include preference here (i.e. what happens when both conditions are not exclusive) but this is generally solved with another parameter forcing a winner when it happens. Here, we will implement the isHandled logic with this code:
private Predicate<String> isHandledPredicate(final Map<String, String> parsedArgs) {
final Collection<Predicate<String>> exclude = toPredicateCollection(parsedArgs.get("exclude"));
final Collection<Predicate<String>> include = toPredicateCollection(parsedArgs.get("include"));
return (Predicate<String>) name -> {
return include.stream().anyMatch(it -> it.test(name)) || exclude.stream().noneMatch(it -> it.test(name));
};
}
Now we know how to detect the classes we must instrument to make their loading fail, let's write the magic class which will do the bytecode replacement. This is done through the second parameter of the javaagent, the Instrumentation. It enables to add/remove ClassFileTransformer which are called each time a class is loaded and provide a way to replace the class bytecode before the Class object is actually defined.
Warning: at this point you can think you can throw an exception in the transformer and don't need to change the class bytecode at all. However a failing transformer is ignored by the JVM so it would just slow down class loading if you do it this way.
Here is the signature of the ClassFileTranformer:
public byte[] transform(final ClassLoader loader, final String className,
final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain,
final byte[] classfileBuffer) throws IllegalClassFormatException;
As you can see, the interesting parameters are for us the className which is actually the class name in resource format so org.foo.Bar will be represented as org/foo/Bar and the classfileBuffer which is the class bytecode before its loading. The returned instance is the bytecode to load. This is why a ClassFileTransformer enable to change the bytecode on the fly.
Warning: if it fine to change all the bodies of a class but take a lot of careness if you start changing the shape of the class (method, signatures, ....) because some frameworks - typically the ones using a claspath scanner like JavaEE specifications, Spring, ... - are checking the class shape from its resource and not only from a reflection point of view, at least partially.
At this point the question for us is: how to change the bytecode to make the static block failing. There are multiple options. You can, indeed, return an almost static byte[] - almost being the classname which must be adjusted. However a cleaner solution is to visit the bytecode, if there is already a static block, change it to throw an exception, and if there is no static block, add one throwing an exception.
This implementation option enables me to introduce asm library - which can be replaced by any bytecode library. ASM can be added to any project with the dependency: org.ow2.asm:asm:jar:7.1. It provides a ClassReader which enables to visit a class thanks to a ClassVisitor and a ClassWriter which enables to write a class. The standard pattern here is to visit the class, depending some trigger (static block for us) force some code generation and replace the input buffer by the writer one. Here is a skeleton doing that:
final ClassReader reader = new ClassReader(classfileBuffer);
final ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
reader.accept(new MyClassVisitor(ASM7, writer), ClassReader.SKIP_FRAMES);
return writer.toByteArray();
This is a very minimal skeleton, in most implementation the reader is extended to avoid some class analysis issue:
new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES) {
@Override
protected String getCommonSuperClass(final String type1, final String type2) {
Class<?> c, d;
try {
c = findClass(loader, type1.replace('/', '.'));
d = findClass(loader, type2.replace('/', '.'));
} catch (final Exception e) {
throw new RuntimeException(e.toString());
} catch (final ClassCircularityError e) {
return "java/lang/Object";
}
if (c.isAssignableFrom(d)) {
return type1;
}
if (d.isAssignableFrom(c)) {
return type2;
}
if (c.isInterface() || d.isInterface()) {
return "java/lang/Object";
}
do {
c = c.getSuperclass();
} while (!c.isAssignableFrom(d));
return c.getName().replace('.', '/');
}
private Class<?> findClass(final ClassLoader tccl, final String name) throws ClassNotFoundException {
try {
return className.equals(name) ? Object.class : Class.forName(name, false, tccl);
} catch (final ClassNotFoundException e) {
return Class.forName(className, false, getClass().getClassLoader());
}
}
}
This common implementation handled the inheritance links between classes more properly. The complexity can come for framework having to load the classes to analyze them before instrumenting them - check out the forName in previous code - which can lead to loading classes before instrumenting them with current JVM handling of the transformers.
Now how to know we have or not a static block. In the bytecode the static block is a specific method named <clinit> - for class initialization. So we can visit all method and if there is one called this way then we know we have a static block:
new ClassVisitor(ASM7, writer) {
private boolean hasStaticInit = false;
@Override
public MethodVisitor visitMethod(final int access, final String name, final String descriptor,
final String signature, final String[] exceptions) {
final MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("<clinit>".equals(name)) {
hasStaticInit = true;
throwException(visitor);
return null;
}
return visitor;
}
@Override
public void visitEnd() {
if (!hasStaticInit) {
throwException(super.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null));
}
super.visitEnd();
}
}, ClassReader.SKIP_FRAMES);
This snippet exactly does that, when it visits a <clinit> method, it generates a specific body (throwException) and tracks it was generated (hasStaticInit boolean) and if it was not visited after all the methods (visitEnd) then it enforces the class to get this method which is static (ACC_STATIC) and has no parameter and returns void (()V descriptor).
Now how to generate the body - throwException method? The code to generate the line:
throw new IllegalStateException("Forbidden class '" + classname + "')
Is - except the concatenation is precomputed:
visitor.visitCode();
visitor.visitTypeInsn(NEW, "java/lang/IllegalStateException");
visitor.visitInsn(DUP);
visitor.visitLdcInsn("Forbidden class '" + fqn + "'");
visitor.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "(Ljava/lang/String;)V", false);
visitor.visitInsn(ATHROW);
visitor.visitMaxs(0, 0);
visitor.visitEnd();
It can looks complicated because it manipulates the stack, splits one like java code in multiple operations and goes through operations we don't see in java sources (visitMaxs(0,0) which will trigger frame computation since we created the writer - visitor in this snippet - in COMPUTE_FRAMES mode).
There is however a trick to generate such statements: ASMifier. This is a class provided by ASM which has a main taking class (.class, the bytecode file, not java one) as parameter. When executed it will dump the code to generate with a writer the class you passed as parameter. So if you write a class with the code you want to generate - or almost it - then it is generally trivial to adapt it. However you can need to take care to the way the stack is built when going generic and if you need to handle generic return types - the instruction can change.
Tip: if it looks too complicated, you can have a look to some libraries like ByteBuddy which try to encapsulate the bytecode manipulation and expose a simpler user API. Depending the task you do it is worth it or not so give it a try if you doubt.
So at that time we have our transformer:
class FilteringTransformer implements ClassFileTransformer {
private final Predicate<String> matcher;
FilteringTransformer(final Map<String, String> configuration) {
matcher = name -> /* code shared earlier */;
}
@Override
public byte[] transform(final ClassLoader loader, final String className,
final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain,
final byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null || matcher == null) {
return classfileBuffer;
}
final String fqn = className.replace('/', '.'); // use fqn instead of resource name
if (matcher.test(fqn)) {
final ClassReader reader = new ClassReader(classfileBuffer);
final ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES) {
@Override
protected String getCommonSuperClass(final String type1, final String type2) {
// code as before
}
};
reader.accept(new ClassVisitor(ASM7, writer) {
// previous code
}, ClassReader.SKIP_FRAMES);
return writer.toByteArray();
}
return classfileBuffer;
}
}
Once you have a transformer you can add it to the JVM runtime through Instrumentation in our premain callback and using the injected agent configuration:
public static void premain(final String agentArgs, final Instrumentation instrumentation) {
final Map<String, String> config = parseArgs(agentArgs);
instrumentation.addTransformer(new FilteringTransformer(config));
}
Now if you create a jar from that code - don't forget the MANIFEST.MF - and add it on a JVM you will be able to make any class loading fail.
The last step is to shade ASM in our agent to have a single jar to add on the JVM - and avoid to use Instrumentation API to enrich the JVM classpath which can have side effects. Personally I use Maven shade plugin but any solution will work while it supports relocation - changing packages:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<createSourcesJar>true</createSourcesJar>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>shaded</shadedClassifierName>
<createDependencyReducedPom>true</createDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/reduced-pom.xml</dependencyReducedPomLocation>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>${project.groupId}.__shaded__.asm</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
This will create a -shaded jar containing ASM and enabling you to run this artifact as javaagent without any dependency issue.
Side note: the relocation is trivial to do with ASM since it provides visitor primitives to do that very easily, if you are interested, and now you know the ASM base API, have a look to their ClassRemapper.
Finally, here is an usage example to prevent maven to run:
MAVEN_OPTS=-javaagent:myagent.jar=include=org.apache.maven.cli.MavenCli mvn compile
This will prevent Maven cli main to load and therefore to run. In current state the failure will not be very "sexy" but since you now own all the JVM bytecode you can fix that without my help ;).
Conclusion
What is important to keep in mind here is that writing a javaagent is probably not the easiest task you can have to do in your java developer life. However it is a really helpful solution to instrument a JVM/application you don't own but need to work on. It can enable to extract precious information from the application without having to redeliver it and even enable to add all the production needs - metrics, tracing, authentication, ... - without having to care in dev in an almost unified way!
Don't abuse it but don't be scared to use it.
The example I used in this post is available on github in my filtering-agent repository.
From the same author:
In the same category: