Oleg Shelajev wrote an excellent tutorial
post on writing JVM agents. These are bits of code which run before
your main()
method. Why do this? It permits some
interesting tricks, chiefly modifying classes as they are loaded,
but also estimating the actual memory used by Java objects.
I gave this a try myself, but rather than writing my agent in Java, I wrote it in Kotlin. It was straight-forward, with only one gotcha.
AgentX.kt
@file:JvmName("AgentX") package hm.binkley.labs.skratch.jvmagent import java.lang.instrument.Instrumentation fun premain(arguments: String?, instrumentation: Instrumentation) { println("Hello from AgentX 'premain'!") } fun main(args: Array<String>) { println("Hello from AgentX 'main'!") }
OK, the non-gotcha. You can declare functions at the package
level. This acts just like static methods in Java, with simpler syntax (no
potentially artificial wrapper class to hold the static method). The two
obvious examples in the above code are main()
and premain()
.
But when calling Kotlin from Java, you use a wrapping class name in the the fully-qualified method name. My Kotlin file is named "AgentX.kt", so the default class name for Java is "AgentXKt". I'm lazy, wanted to save some typing, so I used a Kotlin package-level annotation to name the wrapping class just "AgentX".
Output
The JVM requires an absolute path to any agent jar, and I'm running Cygwin, so a little help to get a full path. Similarly, I used the Maven shade plugin to build a single uber-jar holding my own classes, and those of my dependencies (the Kotlin standard library).
$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar) -jar target/skratch-0-SNAPSHOT.jar Hello from AgentX 'premain'! Hello from AgentX 'main'!
Project is here: https://github.com/binkley/skratch.
Gotcha
Enough preamble, now the gotcha. Unlike Java, Kotlin helps you protect yourself from null
s
without boilerplate code. So in premain()
for the "arguments"
parameter, you need to use String?
rather
than String
as the parameter type as the JVM may pass you a
null
. The first time I tried the code, I didn't realize this
and it blew up:
$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar) -cp target/skratch-0-SNAPSHOT.jar hm.binkley.labs.skratch.jvmagent.AgentX java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386) at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:401) Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method hm.binkley.labs.skratch.jvmagent.AgentX.premain, parameter arguments at hm.binkley.labs.skratch.jvmagent.AgentX.premain(AgentX.kt) ... 6 more FATAL ERROR in native method: processing of -javaagent failed
Interesting! Kotlin found the issue at runtime. It can't find it at compile time as the JVM API for "premain" is pure convention without an interface or class to inspect.
Let's try running the agent a different way. The command-line lets us pass options, and these become the "arguments" parameter:
$ java -javaagent:$(cygpath -m $PWD/target/skratch-0-SNAPSHOT.jar)= -cp target/skratch-0-SNAPSHOT.jar hm.binkley.labs.skratch.jvmagent.AgentX Hello from AgentX 'premain'! Hello from AgentX 'main'!
Sneaky. The mere presence of the "=" on the command line turns the
"arguments" parameter from null
to an empty string.