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测试工具的能力,并允许您做像字节码操作这样的事情。有很多功能强大的工具,有不同的类型,可以满足几乎所有可能的评测需求。