Java 模块检测

Java instrumentation with modules

我正在尝试在模块化应用程序中 运行 一个 javaagent,但我无法通过命令行使其工作。我创建了尽可能小的存储库:

.
├── 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) {
    }
}

生成文件

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

manifest 是从 Makefile 自动创建的。


当我执行make run时,输出是:

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)

相反,当我将 Makefile 中的 run 目标更改为:

run: build_main build_agent
    java -javaagent:output/modifier.jar --class-path output/main tsp.App

一切正常。我不想使用像 Gradle 或 Maven 这样的构建工具,因为我想了解为什么它不能从命令行运行。我读过 Loading agent classes and the modules/classes available to the agent class 但老实说,我并不完全清楚。我做了很多尝试,比如使用 --add-modules output/modifier 但没有成功。

> 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)

代理不能模块化,它们会自动加载到系统 class 加载程序的 class 路径,进入未命名的模块。

我假设 JVM 没有正确涵盖代理是模块化的并且在尝试调用它时在内部崩溃的情况。

你用最近的 Java 11/17 试过这个吗?我认为这可以通过删除代理的模块描述符来解决。

正如其他答案所说,Java 代理被加载到未命名模块中。这隐藏了您的设置问题。当你 运行 你的模块

java -javaagent:output/modifier.jar --add-modules modifier \
  --module-path output/main:output/modifier.jar --module main/tsp.App

你会得到

Error occurred during initialization of boot layer
java.lang.LayerInstantiationException: Package tsp in both module modifier and module main

在一个模块层内,包明确属于一个模块,两个模块具有相同的包名是错误的。

class 路径上的 classes 不进行此类检查。相反,在您的设置中,包 tsp 已与 main 模块相关联,并且 运行time 甚至没有尝试在 [=67= 中查找 tsp.Agent ] 小路。由于 tsp 包已经与 main 模块相关联,它只在 output/main 位置查找并没有找到 class.

要点是,您必须使用不同的包。当你使用不同的包名时,命令行

java -javaagent:output/modifier.jar --module-path output/main --module main/tsp.App

有效,但代理将加载到未命名模块中。 module-info 将被忽略。但是,您可以通过手动添加来强制将代理加载为模块。这利用了上述行为;当一个模块被添加到环境中时,它的包所有权优先。

当我将您代理的包重命名为 agent 并从我的回答开始使用命令行时,我得到

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)

这证明代理已作为模块加载。与未命名模块不同,默认情况下它不会导出所有内容。

当我更改 Agent 的 module-info

module modifier {
    requires java.instrument;
    exports agent to java.instrument;
}

注意我把代理的包重命名为agent,所以代理主class是agent.Agent

运行成功了。

将代理的 class 更改为

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());
    }
}

然后打印

Agent started in module modifier

符合预期。

附带说明一下,您编译模块的方法不必要地复杂。您可以编译一个完整的模块,例如

javac --module-source-path mods -d output -m main

甚至

javac --module-source-path mods -d output -m modifier,main

一次编译。