【问题标题】:Starting Instrumentation Agent after VM StartupVM 启动后启动 Instrumentation Agent
【发布时间】:2018-01-23 21:42:00
【问题描述】:

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

我正在阅读 Java Agent Instrumentation,它说代理可以在 VM 启动后启动。因此,如果我想动态替换某个类(而不破坏应用程序),这就是我要使用 agent-main 的目的吗?还是我需要在这里做更多的事情?

我知道人们可能会问“你在谈论 JRebel” - 并不是因为我想做一些简单的事情,而 JRebel 太过分了。

仪器文档 - Java docs for Instrumentation

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

【问题讨论】:

  • 请注意,如果您可以访问目标应用程序代码,那么有更简单的方法可以动态加载代码。事实上,Java 的设计使得在运行时编译和加载一些类变得非常容易,你不需要代理(只需谷歌“java 动态加载类”),它涉及使用ClassLoader .但如果您无法访问目标代码,那么这可能是注入代理的好方法。
  • @Zabuza 你说的是动态加载而不重启应用程序吗?因为那是我想做的。在生产系统中,您永远不会(嗯,不应该)存在任何源代码,因此如果有的话,使用代理可能是最首选的方式
  • 代理允许您将代码注入其他进程,而无需更改目标代码中的内容。但是如果你在你的项目中并且想要稍后加载东西,那么你可以在那个时候使用ClassLoader.loadClass(...)。它使您能够在运行时动态加载类,而无需重新启动项目。优点是后者不需要对库进行任何摆弄,它默认存在并且非常易于使用。
  • @Zabuza 不,我只想用更新的类替换一个特定的类 - 想法是类加载器已经加载了该类,但是如果有代码更改,我想部署更改的类而不实际重新启动整个应用程序。看来代理是首选。
  • 啊,重载类也可以,不涉及代理的使用,还要ClassLoaders。让我再写一个答案。

标签: java instrumentation agent


【解决方案1】:

在运行时附加代理需要使用附加 API,该 API 在 Java 8 之前包含在 tools.jar 中,并且从 Java 9 开始包含在其自己的模块中。tools.jar 的位置及其类的名称依赖于系统(操作系统、版本、供应商),从 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);
  }
}

你就完成了。

【讨论】:

  • 感谢您发布答案,我个人了解了一个新库,它可能在未来有用,但您是否忘记了以 D 开头的东西?
  • D 喜欢免责声明?在这里没觉得有必要。
  • 您的回答对我来说有点像广告。我不会说这是必要的,但我认为这是一种很好的形式。做你认为合适的事,我不会举报什么的。
  • 好吧,我是 Byte Buddy 的作者,这是一个开源工具,这里有文档记录 ;)
【解决方案2】:

显然你想在运行时重新加载类。这样您的项目就可以对代码的更改做出反应而无需重新启动。

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

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


我发现this article 很有帮助,让我解释一下它是如何工作的。您可以使用ClassLoader.loadClass(...) 轻松加载类,也可以使用它重新加载类,非常简单。但是,在您编译代码时,您的代码类已经是某种硬连线了。因此,您的旧代码将继续创建旧类的实例,尽管您已经重新加载了类。

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


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

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

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


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

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

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

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


现在一些示例代码可以让您大致了解一下。

您要监控的对象的界面

public interface IExample {
    void example();
}

您要重建的真实类

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

您将实际使用的代理类

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

【讨论】:

  • 请注意,我没有测试过代码,但它应该会给你一个好主意。
  • 非常好的答案,这就是这个网站应该有的内容。
  • @Zabuza 感谢您的回答。由于使用工厂模式编写一个全新的应用程序是不切实际的 - 我已经采用了代理方法并且它有效。我能看到的唯一缺点是即使在agentmain() 成功执行之后,应用程序也不会释放代理。
  • 很高兴听到它有帮助。请注意,卸载似乎是可能的,但如下所示:Unloading a JVMTI agent at runtime?
【解决方案3】:

首先您的代理类需要指定一个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-文件(MANIFEST.MF)。它指向实现agentmain 方法的类。它可能看起来像:

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

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


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

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,对应类为Injector.java,整个项目有一个小Wiki

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

【讨论】:

  • 作为注释,我想添加一个小概述:有一个 jar,即 target,它当前正在运行。然后你有一个 againt-jar 你想注入到目标中。最后,您有一个将代理注入目标的应用程序,即 injector。注射器将具有上述方法。执行后,agent 被注入到 target 中,它的 agentmain 将被执行。此时两者共享同一个堆,因此您可以从 agent 内部操作来自 target 的内容。
  • 是的,这就是我所做的,但我只是想知道是否需要进行任何其他设置才能在 VM 之后启动。似乎这里一切都很好:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-07
  • 1970-01-01
  • 1970-01-01
  • 2020-08-26
  • 2018-02-08
  • 2013-08-08
  • 1970-01-01
相关资源
最近更新 更多