I am trying to run a javaagent
in a modular application, but I cannot make it work from command line. I have created the smallest repository possible:
.
├── Makefile
├── manifest
└── mods
├── main
│ ├── module-info.java
│ └── tsp
│ └── App.java
└── modifier
├── module-info.java
└── tsp
└── Agent.java
mods/main/module-info.java
module main {
}
mods/main/tsp/App.java
package tsp;
public class App {
public static void main(String[] args) {
}
}
mods/modifier/module-info.java
module modifier {
requires java.instrument;
}
mods/modifier/tsp/Agent.java
package tsp;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
}
}
Makefile
SHELL := /bin/bash
.PHONY: clean build_main build_agent run
build_agent: clean
echo -e "Premain-Class: tsp.Agent\nCan-Retransform-Classes: true\n" > manifest
javac --module-path mods/modifier -d output/modifier $$(find mods/modifier -name *.java) && \
jar --create --file output/modifier.jar --manifest manifest -C output/modifier .
build_main: clean
javac --module-path mods/main -d output/main $$(find mods/main -name *.java)
run: build_main build_agent
java -javaagent:output/modifier.jar --module-path output/main --module main/tsp.App
clean:
rm -rf output
The manifest
is created automatically from the Makefile
.
When I execute make run
, the output is:
rm -rf output
javac --module-path mods/main -d output/main $(find mods/main -name *.java)
echo -e "Premain-Class: tsp.Agent\nCan-Retransform-Classes: true\n" > manifest
javac --module-path mods/modifier -d output/modifier $(find mods/modifier -name *.java) && \
jar --create --file output/modifier.jar --manifest manifest -C output/modifier .
java -javaagent:output/modifier.jar --module-path output/main --module main/tsp.App
Exception in thread "main" java.lang.ClassNotFoundException: tsp.Agent
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:431)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)
*** java.lang.instrument ASSERTION FAILED ***: "result" with message agent load/premain call failed at open/src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 422
FATAL ERROR in native method: processing of -javaagent failed, processJavaStart failed
make: *** [Makefile:14: run] Aborted (core dumped)
Instead, when I change the run
target in the Makefile
to:
run: build_main build_agent
java -javaagent:output/modifier.jar --class-path output/main tsp.App
Everything works perfectly. I do not want to use build tools like Gradle or Maven because I would like to understand why it does not work from command line. I have read Loading agent classes and the modules/classes available to the agent class but it is not completely clear to me honestly. I have made many tries like using --add-modules output/modifier
but without success.
> java --version
openjdk 15.0.2 2021-01-19
OpenJDK Runtime Environment (build 15.0.2+7-27)
OpenJDK 64-Bit Server VM (build 15.0.2+7-27, mixed mode, sharing)
As said by the other answer, Java Agents are loaded into the Unnamed module. This hides the problem with your setup. When you run your modules with
java -javaagent:output/modifier.jar --add-modules modifier \
--module-path output/main:output/modifier.jar --module main/tsp.App
you’ll get
Error occurred during initialization of boot layer
java.lang.LayerInstantiationException: Package tsp in both module modifier and module main
Within one module layer, packages unambiguously belong to one module and it is an error to have two modules with the same package name.
The classes on the class path do not undergo such check. Instead, in your setup, the package tsp
has been associated with the main
module and the runtime did not even try to look for tsp.Agent
in the class path. Since the tsp
package has been associated with the main
module, it only looked in the output/main
location and did not find the class.
The takeaway is, you have to use different packages. When you use different package names, the command line
java -javaagent:output/modifier.jar --module-path output/main --module main/tsp.App
works, but the Agent will be loaded into the Unnamed module. The module-info
will be ignored. However, you can enforce the agent to be loaded as a module by adding it manually. This utilizes the behavior described above; when a module has been added to the environment, it’s package ownership takes precedence.
When I rename your agent’s package to agent
and use the command line from the beginning of my answer, I get
Exception in thread "main" java.lang.IllegalAccessException: class sun.instrument.InstrumentationImpl (in module java.instrument) cannot access class agent.Agent (in module modifier) because module modifier does not export agent to module java.instrument
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
at java.base/java.lang.reflect.Method.invoke(Method.java:560)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:491)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:503)
which proves that the agent has been loaded as a module. Unlike the Unnamed module, it does not export everything by default.
When I change the Agent’s module-info
module modifier {
requires java.instrument;
exports agent to java.instrument;
}
Mind that I renamed the agent’s package to agent
, so the agent main class is agent.Agent
It runs successfully.
Changing the Agent’s class to
package agent;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Agent started in " + Agent.class.getModule());
}
}
does then print
Agent started in module modifier
as intended.
As a side note, your approach to compile the modules is unnecessarily complicated. You can compile a complete module using, e.g.
javac --module-source-path mods -d output -m main
or even
javac --module-source-path mods -d output -m modifier,main
to compile both in one go.