Search code examples
javamoduleinstrumentationjava-modulejavaagents

Java instrumentation with modules


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)

Solution

  • 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.