VM 启动后启动 Instrumentation Agent

Starting Instrumentation Agent after VM Startup

我希望有人能解释这个项目,因为我可能理解错了:

我正在阅读有关 Java Agent Instrumentation 的文章,它说代理可以在 VM 启动后启动。因此,如果我想动态替换一些 class(不降低应用程序),这就是我要使用 agent-main 的目的吗?或者我需要在这里做更多的事情吗?

我知道人们可能会问 "Are you talking about JRebel" - 不是真的,因为我想做一些简单的事情,而 JRebel 太过分了。

仪器文档 - Java docs for Instrumentation

我了解所有检测覆盖,但我有点困惑如何在应用程序启动后使用 -agent 参数挂接此代理。

首先,您的代理 class 需要指定一个 agentmain 方法,例如:

public class MyAgent {
    public static void agentmain(final String args, final Instrumentation inst) {
        try {
            System.out.println("Agent loaded.");
        } catch (Exception e) {
            // Catch and handle every exception as they would
            // otherwise be ignored in an agentmain method
            e.printStackTrace();
        }
    }
}
例如,

编译它并将其打包到 jar 文件中。如果您选择 jar-变体,那么它必须在其 manifest[= 中指定 Agent-Class key 55=]-文件(MANIFEST.MF)。它指向 class 实现 agentmain 方法。它可能看起来像:

Manifest-Version: 1.0
Agent-Class: package1.package2.MyAgent

如果它位于那些包中,例如。


之后你可以通过VirtualMachine#loadAgent方法加载代理(documentation)。请注意,那些 classes 使用的机制是 Java 的 Attach library 的一部分。他们决定,因为大多数用户不需要它,所以不直接将它添加到系统路径中,但您可以直接添加它。它位于

pathToYourJDKInstallation\jre\bin\attach.dll

并且它需要在系统属性java.library.path指向的地方。例如,您可以将它复制到您的 .../Windows/System32 文件夹或调整 属性 或类似的东西。


举个例子,如果你想在另一个当前 运行 jar 中注入一个 agent-jar,你可以使用这样的方法:

public void injectJarIntoJar(final String processIdOfTargetJar,
        final String pathToAgentJar, final String[] argumentsToPass) {
    try {
        final VirtualMachine vm = VirtualMachine.attach(processIdOfTargetJar);
        vm.loadAgent(pathToAgentJar, argumentsToPass.toString());
        vm.detach();
    } catch (AttachNotSupportedException | AgentLoadException
            | AgentInitializationException | IOException e) {
        System.err.println("Unable to inject jar into target jar.");
    }
}

使用相同的技术,您可以将 dll 库(如果它们通过本机代理接口实现相应的代理方法)注入 jar。


实际上,如果这对您有帮助,我前段时间已经为此类内容编写了一些小型库。参见 Mem-Eater-Bug, the corresponding class is Injector.java and the whole project has a small Wiki

它有一个示例展示了如何使用该技术来操纵编写为 Java 应用程序的 SpaceInvaders 游戏。

显然您想在运行时重新加载 classes。这样您的项目就可以在不重新启动的情况下对代码的更改做出反应。

要实现这一点,您需要准备您的项目并编写一个非常干净的架构,它涉及使用 interfacesfactory-patternsproxy-patterns 和检查 updates 然后销毁和重建所有当前对象的例程。

不幸的是,这可能不是一件容易的事,但它是可行的,具体取决于项目的大小和应动态响应更改的代码量。


我发现 this article 很有帮助,让我解释一下它是如何工作的。您可以轻松地用 ClassLoader.loadClass(...) 加载 class,也可以使用它重新加载 class,非常简单。然而,在您编译代码时,classes 已经是某种硬连线。因此,尽管您已重新加载 class.

,但您的旧代码将继续创建 old classes 的实例

这就是为什么我们需要某种允许将旧 class 与新 class 交换的架构的原因。同样很明显,旧 class 的当前实例不能自动转移到新版本,因为一切都可能已经改变。因此,您还需要一个自定义方法来收集和重建这些实例。


文章中描述的方法首先使用 Interface 而不是实际的 class。这允许在不破坏使用该接口的代码的情况下轻松交换该接口后面的 class。

然后您需要一个工厂,您可以在其中请求 Interface 的实例。工厂现在可以检查底层 class 文件是否已更改,如果已更改,则重新加载它并获取对新 class 版本的引用。它现在可以始终创建使用最新 class.

的接口实例

据此,如果代码库已更改,工厂还能够收集所有创建的实例以便稍后交换它们。但是工厂应该使用 WeakReference (documentation) 来引用它们,否则你会有很大的内存泄漏,因为垃圾收集器将无法删除实例,因为工厂持有对它们的引用。


好的,现在我们总能获得 Interface 的最新实现。但是我们如何轻松交换现有实例。答案是使用 代理模式 (explanation).

很简单,您有一个 代理 class,这是您正在使用的实际对象。它具有 Interface 的所有方法,并且在调用方法时它只是转发到 真正的 class.

你的工厂,因为它有一个使用 WeakReference 的所有当前实例的列表,现在可以迭代代理列表并将它们的真实 class 与新的最新版本交换对象。

在你的项目中使用的现有代理现在将自动使用新的真实版本,因为代理本身没有改变,只是它对真实目标的内部引用发生了变化。


现在一些示例代码可以让您有个大概的了解。

您要监控的对象的接口

public interface IExample {
    void example();
}

您要重建的真实class

public class RealExample implements IExample {
    @Override
    public void example() {
        System.out.println("Hi there.");
    }
}

你实际会用到的代理class

public class ProxyExample implements IExample {
    private IExample mTarget;

    public ProxyExample(final IExample target) {
        this.mTarget = target;
    }

    @Override
    public void example() {
        // Forward to the real implementation
        this.mRealExample.example();
    }

    public void exchangeTarget(final IExample target) {
        this.mTarget = target;
    }
}

你将主要使用的工厂

public class ExampleFactory {
    private static final String CLASS_NAME_TO_MONITOR = "somePackage.RealExample";
    private final List<WeakReference<ProxyExample>> mInstances;
    private final URLClassLoader mClassLoader;

    public ExampleFactory() {
        mInstances = new LinkedList<>();

        // Classloader that will always load the up-to-date version of the class to monitor
        mClassLoader = new URLClassLoader(new URL[] {getClassPath()}) {
            public Class loadClass(final String name) {
                if (CLASS_NAME_TO_MONITOR.equals(name)) {
                    return findClass(name);
                }

                return super.loadClass(name);
            }
        };
    }

    private IExample createRealInstance() {
        return (IExample) this.mClassLoader.loadClass(CLASS_NAME_TO_MONITOR).newInstance();
    }

    public IExample createInstance() {
        // Create an up-to-date instance
        final IExample instance = createRealInstance();

        // Create a proxy around it
        final ProxyExample proxy = new ProxyExample(instance);
        // Add the proxy to the monitor
        this.mInstances.add(proxy);

        return proxy;
    }

    public void updateAllInstances() {
        // Iterate the proxies and update their references
        // Use a ListIterator to easily remove instances that have been cleared
        final ListIterator<WeakReference<ProxyExample>> instanceIter =
            this.mInstances.listIterator();
        while (instanceIter.hasNext()) {
            final WeakReference<ProxyExample> reference = instanceIter.next();
            final ProxyExample proxy = reference.get();

            // Remove the instance if it was already cleared,
            // for example by the garbage collector
            if (proxy == null) {
                instanceIter.remove();
                continue;
            }

            // Create an up-to-date instance for exchange
            final IExample instance = createRealInstance();
            // Update the target of the proxy instance
            proxy.exchangeTarget(instance);
        }
    }
}

最后如何使用它:

public static void main(final String[] args) {
    final ExampleFactory factory = new ExampleFactory();

    // Get some instances using the factory
    final IExample example1 = factory.createInstance();
    final IExample example2 = factory.createInstance();

    // Prints "Hi there."
    example1.example();

    // Update all instances
    factory.updateAllInstances();

    // Prints whatever the class now contains
    example1.example();
}

在运行时附加代理需要使用附加 API,它包含在 tools.jar 直到 Java 8 中,并包含在从 [=28= 开始的自己的模块中] 9. tools.jar 的位置及其 类 的名称取决于系统(OS、版本、供应商)并且从 Java 9 开始它不存在完全没有,但必须通过其模块解决。

如果您正在寻找访问此功能的简单方法,请尝试 Byte Buddy,它有一个子项目 byte-buddy-agent。按照您习惯的方式创建一个 Java 代理,但添加一个 Agent-Main 条目,您可以将 Pre-Main 放入清单中。另外,将输入方法命名为 agentmain,而不是 premain.

使用byte-buddy-agent,你可以写一个程序:

class AgentLoader {
  public static void main(String[] args) {
    String processId = ...
    File agentJar = ...
    ByteBuddyAgent.attach(processId, agentJar);
  }
}

大功告成。