Java agents(代理)是一种特殊类型的类,通过使用Java Instrumentation API,它可以拦截JVM上运行的应用程序,修改它们的字节码。Java代理不是一项新技术。相反,它们从Java5开始就存在了。但即使在这段时间之后,许多开发人员仍然对这个特性有误解,其他人甚至不知道。
在本文中,我们通过向您提供有关Java agents的快速指南来纠正这种情况。您将了解什么是Java agents,使用它们的好处是什么,以及如何使用它们来评测Java应用程序。让我们开始吧。
定义Java Agents
Java Agents是Java Instrumentation API的一部分。所以要理解agents,我们需要理解什么是instrumentation
。
在软件上下文中,插装是一种用于更改现有应用程序、向其添加代码的技术。您可以手动和自动执行检测。您还可以在编译时和运行时执行此操作。
那么,仪器有什么好处呢?它的目的是允许您更改代码,更改其行为,而不必实际编辑其源代码文件。这可能非常强大,也非常危险。你能用它做什么就留给你了。可能性是无穷的。面向切面编程?突变测试?剖析?你说吧。
现在,让我们再次关注Java agent。这些是什么,它们与instrumentation有什么关系?
简而言之,Java agent代理只不过是一个普通的Java类。区别在于它必须遵循一些特定的惯例。第一个约定与agent代理的入口点有关。入口点由一个名为“premain
”的方法组成,该方法具有以下签名:
public static void premain(String agentArgs, Instrumentation inst)
如果代理类没有具有上面签名的“premain
”方法,那么它应该具有以下替代方法:
public static void premain(String agentArgs)
JVM一初始化,就调用每个代理的premain
方法。然后,它像往常一样调用Java应用程序的main
方法。每个premain
方法都必须正常恢复执行,应用程序才能进入启动阶段。
代理应该还有另一个名为“agentmain
”的方法。下面是该方法的两个可能签名:
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
当在JVM上调用这些方法时,在JVM上不使用这些方法。
如何编写Java Agent
实际上,java agent代理是一种特殊类型的.jar
文件。正如我们已经提到的,要创建这样的代理,我们必须使用Java Instrumentation API。正如我们前面提到的,这样的API并不新鲜。
我们需要创建代理的第一个要素是agent代理类。代理类只是一个普通的Java类。
要创建Java agent,我们需要一个示例项目。因此,我们将创建一个简单的应用程序,它只做一件事:打印斐波那契序列的前n个数字,n是用户提供的数字。一旦应用程序启动并运行,我们将使用一些Java工具来执行一些基本的评测。
构建我们的示例应用程序
对于这个项目,我将使用IntelliJ IDEA的免费社区版,但是可以随意使用您觉得最适合使用的IDE或代码编辑器。那么,让我们开始吧。
打开IDE并单击“创建新项目”,如下图所示:
在“创建新项目”窗口中,选择“Java”作为项目类型,然后单击“下一步”:
然后,在下一个屏幕上,标记“从模板创建项目”框,为应用程序选择“命令行应用程序”模板,然后再次单击“下一步”:
之后,唯一剩下的就是配置项目的名称和位置,然后单击“完成”:
创建项目后,让我们创建斐波那契逻辑。复制以下内容并粘贴到主类上:
package com.company;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("How many items do you want to print?");
int items, previous, next;
items = scanner.nextInt();
previous = 0;
next = 1;
for (int i = 1; i <= items; ++i)
{
System.out.println(previous);
int sum = previous + next;
previous = next;
next = sum;
}
}
}
应用程序非常简单。它开始询问用户希望打印的项目数。然后,它生成并打印Fibonacci
序列,其中包含的术语数量与用户通知的数量相同。
当然,这个应用程序非常幼稚。例如,它不会检查无效项目。另一个问题是,如果用户输入的值足够大,则会导致程序溢出int
的上限。您可以使用long
甚至biginger
类来处理较大的输入。不过,对于我们的示例来说,这些都不重要,所以如果您愿意,可以随意添加这些改进作为练习。
启动Java Agent
我们的示例应用程序已经启动并运行,因此我们已经准备好创建Java代理。重复创建新项目的过程。称之为“MyFirstAgentProject
”
进入File>new Java class
创建一个新类,如下图所示:
然后,将类命名为“MyFirstAgent
”,并按enter键。之后,将所创建文件的内容替换为以下内容:
package com.company;
import java.lang.instrument.Instrumentation;
public class MyFirstAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Start!");
}
}
现在我们必须创建一个自定义清单。让我们从为我们的项目添加Maven支持开始。右键单击“MyFirstAgentProject
”模块。然后,单击“添加框架支持”
在“添加框架支持”窗口中,选中“Maven”并单击OK。之后,IntelliJ将创建一个pom.xml
文件并打开它,以便您可以进行编辑。将以下内容添加到pom.xml
文件并保存:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
在上面的代码中,我们将“maven jar插件”插件添加到pom文件中,并为清单文件配置位置。现在我们需要创建它。为此,复制以下内容,将其粘贴到新文件上,并将其另存为“src/main/resources/META-INF/MANIFEST.MF
”
Manifest-Version: 1.0
Premain-Class: com.company.javaagent.helloworldagent.MyFirstAgent
Agent-Class: com.company.javaagent.helloworldagent.MyFirstAgent
我们快到了!清单创建完成后,现在让我们执行一个maven安装。在“Maven”工具窗口中,展开“Lifecycle”文件夹,右键单击install,然后选中“构建后执行”选项。
使用该设置,IDE将在每次构建应用程序时执行maven安装。那么,让我们来建造它吧!转到“生成”>“生成项目”,或使用CTRL+F9快捷键。如果一切顺利,您应该能够在“target”下找到结果jar文件
我们已经成功地为第一个Java代理创建了jar文件。现在,让我们测试一下!
加载Agent代理
我们现在要使用我们的代理,要做到这一点,我们需要加载它。加载Java代理有两种方法,它们称为静态加载和动态加载。静态加载发生在应用程序运行之前。它调用premain
方法,并在运行应用程序时使用-javaagent
选项激活它。另一方面,动态加载是在应用程序已经运行的情况下激活的,这是使用JavaAttachAPI完成的。
这里我们将使用静态加载。在IntelliJ IDEA中打开示例应用程序后,转到运行>编辑配置…,如下图所示:
将显示一个新窗口。顾名思义,您可以在那里配置许多与应用程序的运行和调试相关的不同选项。现在需要做的是将-javaagent
选项添加到vmoptions
字段,将代理的jar文件的路径作为参数传递给它。
配置路径后,可以单击OK,然后像往常一样运行项目。如果一切顺利,您应该看到的输出是:
如您所见,我们使用premain方法定义的消息“Start!”是在运行应用程序的主方法之前打印的。这意味着我们的代理已成功加载。
Start!
How many items do you want to print?
10
0
1
1
2
3
5
8
13
21
34
Process finished with exit code 0
小结
你可能会想,我们所看到的是不是麻烦太多而收效甚微。答案是肯定的“否”。首先,您必须记住,这里的示例相当于Java agents的“Hello world”。事情会变得越来越复杂。正如我们已经提到的,有一些非常复杂的工具使用Java Instrumentation API。
其次,请记住,可以使用许多额外的工具来真正地扩展java测试工具的能力,并允许您做像字节码操作这样的事情。有很多功能强大的工具,有不同的类型,可以满足几乎所有可能的评测需求。