在VM启动后启动检测代理



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

我读到关于Java Agent Instrumentation的文章,它说代理可以在VM启动后启动。所以,如果我想动态替换某个类(而不需要关闭应用程序),这就是我使用代理main的目的吗?还是我需要在这里做更多的事情?

我知道人们可能会问"你在说JRebel吗"——并不是因为我想做一些简单的事情,而JRebel是一个过度的人。

Instrumentation docs.Instrumentation 的Java文档

我理解所有的指令插入重写,但我有点困惑,在应用程序启动后,如何用-agent参数挂接此代理。

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

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

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


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

pathToYourJDKInstallationjrebinattach.dll

它需要位于系统属性java.library.path所指向的位置。例如,您可以将它复制到.../Windows/System32文件夹中,或者调整属性或类似的东西。


例如,如果您想将代理jar注入另一个当前运行的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-Eeater Bug,对应的类是Injector.java,整个项目有一个小Wiki。

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

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

为了实现这一点,您需要准备您的项目并编写一个非常干净的体系结构,它包括使用接口工厂模式代理模式新,然后销毁和重建所有当前对象的例程。

不幸的是,这可能不是一项容易的任务,但它是可行的,这取决于项目的大小和应该对更改做出动态反应的代码量。


我觉得这篇文章很有帮助,让我解释一下它是如何工作的。您可以使用ClassLoader.loadClass(...)轻松地加载一个类,也可以使用它来重新加载一个类别,非常简单。然而,在编译时,您的代码类已经是某种硬连接了。因此,您的旧代码将继续创建旧类的实例,尽管您已经重新加载了该类。

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


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

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

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


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

很简单,您有一个代理类,它是您正在处理的实际对象。它拥有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();
}

在运行时附加代理需要使用附加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);
}
}

你就完了。

最新更新