【问题标题】:Java instrumentation with modules带有模块的 Java 检测
【发布时间】:2022-01-16 09:47:57
【问题描述】:

我正在尝试在模块化应用程序中运行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)

【问题讨论】:

    标签: java module instrumentation java-module javaagents


    【解决方案1】:

    代理不能模块化,它们会自动加载到系统类加载器的类路径中,加载到未命名的模块中。

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

    您是否在最近的 Java 11/17 上尝试过这个?我认为这可以通过删除代理的模块描述符来解决。

    【讨论】:

    • 感谢@RafaelWinterhalter,但我错过了一步:我将代理构建为一个模块,当我使用make run 而不是--module-path 时,它工作得非常好。如果我运行java --module ...java --class-path ... myclass,Java 代理是否会独立加载到未命名的模块中?
    • 我会假设 JVM 会忽略类路径组件的模块信息,因为根据定义它们将被放置在未命名的模块中,这与您在模块信息中定义的任何内容相矛盾。结果类似于在 JDK 8 上运行带有 Java 模块信息的库。它也可以工作,但当然没有 JMS 功能。
    • 感谢@kriegaex,所以它无法读取tsp.Agent 类,因为它会自动放入未命名的模块中,因此无法从命名模块访问?
    • 这不是我想说的。无论如何,我只是提供一个有根据的猜测。无论导致丑陋堆栈跟踪的确切原因是什么,我认为 JVM 应该做得比这更好。如果还没有 OpenJDK 问题,那么可能值得打开一个问题。我认为Rafael有打开一个的必要权限和解释它的技能,否则你必须先通过Oracle开始。
    • “断言失败”似乎总是在初始化 Java 代理失败时发生;这是一条红鲱鱼。 ClassNotFoundException 是这里唯一的问题,它是拆分包问题的直接结果。
    【解决方案2】:

    正如另一个答案所说,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
    

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

    类路径上的类不进行此类检查。相反,在您的设置中,包tsp 已与main 模块相关联,并且运行时甚至没有尝试在类路径中查找tsp.Agent。由于tsp 包已经与main 模块相关联,所以它只查看了output/main 位置,并没有找到该类。

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

    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)
    

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

    当我更改代理的module-info

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

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

    运行成功。

    将代理的类更改为

    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
    

    一次性编译。

    【讨论】:

    • 在模块中使用代理时需要注意的一个问题:转换类时必须在运行时手动添加读取边 - by default, only a read edge is added to the unnamed module of the system class loader and the unnamed module of the bootstrap class loader.
    • @JohannesKuhn 是的,这就是为什么我在答案中给了缺失的exports 这么多空间。请务必注意,使用显式模块会产生更多有关依赖管理的责任。
    • 该评论更倾向于转换一个类,因此它调用代理的方法 - 为此,包含调用的模块需要读取代理模块。
    • @JohannesKuhn “关于依赖管理的职责”是泛泛而谈的。由于代理可以检测代码以使用另一个模块的代码,而不一定是代理的代码,因此即使代理不在模块内也是如此。
    • 非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-08
    • 2014-10-04
    • 1970-01-01
    • 2021-08-20
    • 1970-01-01
    • 2021-10-14
    • 1970-01-01
    相关资源
    最近更新 更多