在本文中,我们将讨论Java Instrumentation API。它提供了向现有编译的Java类添加字节码的能力。

我们还将讨论java代理以及如何使用它们为代码提供工具。

我们将使用工具构建一个应用程序。

我们的应用程序将包括两个模块:

1. 允许我们取款的ATM应用程序

2. 还有一个Java代理,它允许我们通过测量投入的时间和花费的金钱来测量ATM的性能

Java agent将修改ATM字节码,允许我们测量取款时间,而无需修改ATM应用程序。

我们的项目将具有以下结构:

<groupId>com.baeldung.instrumentation</groupId>
<artifactId>base</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
    <module>agent</module>
    <module>application</module>
</modules>

什么是Java Agent

通常,java agent只是一个精心编制的jar文件。它利用JVM提供的 Instrumentation API来改变JVM中加载的现有字节码。

为了让代理工作,我们需要定义两种方法:

  • premain–将在JVM启动时使用-javaagent参数静态加载代理
  • agentmain–将使用Java附加API将代理动态加载到JVM中

要记住的一个有趣的概念是,JVM实现,如Oracle、OpenJDK等,可以提供一种动态启动代理的机制,但这不是一种要求。

首先,让我们看看如何使用现有的Javaagent。

之后,我们将看看如何从头开始创建一个,以便在字节码中添加所需的功能。

加载Java agent

为了能够使用Java agent,我们必须首先加载它。

我们有两种类型的负载:

  • static–使用-javaagent选项使用premain加载代理
  • dynamic–使用agentmain,使用Java附加API将代理加载到JVM中

接下来,我们将查看每种类型的负载,并解释其工作原理。

Static Load 静态加载

在应用程序启动时加载Java agent称为Static load静态加载。静态加载在启动时修改字节码,然后再执行任何代码。

请记住,静态加载Static load使用premain方法,该方法将在任何应用程序代码运行之前运行,以使其运行,我们可以执行:

java -javaagent:agent.jar -jar application.jar

需要注意的是,我们应该始终将–javaagent参数放在–jar参数之前。

以下是我们命令的日志:

22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

我们可以看到premain方法何时运行,MyAtm类何时转换。我们还看到了两个ATM取款交易日志,其中包含每个操作完成所需的时间。

请记住,在最初的应用程序中,我们没有事务的这个完成时间,它是由Java agent添加的。

Dynamic Load 动态加载

将Java agent加载到已经运行的JVM中的过程称为动态加载。代理使用Java Attach API进行连接。

更复杂的情况是,我们的ATM应用程序已经在生产环境中运行,并且我们希望动态地增加事务的总时间,而不让应用程序停机。

让我们编写一小段代码来实现这一点,我们将这个类称为AgentLoader。为了简单起见,我们将把这个类放在应用程序jar文件中。因此,我们的应用程序jar文件既可以启动我们的应用程序,也可以将我们的代理连接到ATM应用程序:

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

现在我们有了AgentLoader,我们启动应用程序,确保在事务之间的十秒钟暂停时间内,我们将使用AgentLoader动态地附加Java agent。

我们还要添加glue,让我们可以启动应用程序或加载agent。

我们将调用这个类启动器,它将是我们的主要jar文件类:

public class Launcher {
    public static void main(String[] args) throws Exception {
        if(args[0].equals("StartMyAtmApplication")) {
            new MyAtmApplication().run(args);
        } else if(args[0].equals("LoadAgent")) {
            new AgentLoader().run(args);
        }
    }
}

启动应用程序:

java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Attach Java Agent:

在第一次操作之后,我们将java agent连接到JVM:

java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully

检查应用日志:

现在我们将agent连接到JVM,我们将看到第二个ATM取款操作的总完成时间。

这意味着我们在应用程序运行时动态添加了功能:

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

创建Java Agent

在学习了如何使用agent之后,让我们看看如何创建agent。我们将研究如何使用Javassist来更改字节码,并将其与一些instrumentation API方法相结合。

关于javassist的介绍参考这篇文章:https://javakk.com/2239.html

由于java agent使用java Instrumentation API,在深入创建agent之前,让我们先看看此API中最常用的一些方法,并简要介绍它们的作用:

  • addTransformer–将transformer添加到仪表引擎
  • getAllLoadedClasses–返回JVM当前加载的所有类的数组
  • retransformClasses–通过添加字节码,简化了已加载类的检测
  • removeTransformer–注销提供的transformer
  • redefineClasses–使用提供的类文件重新定义提供的类集,这意味着该类将被完全替换,而不是像重传FormClass那样进行修改

创建Premain和Agentmain方法

我们知道每个Java agent都至少需要一个premainagentmain方法。后者用于动态加载,而前者用于将javaagent静态加载到JVM中。

让我们在代理中定义这两个,以便能够静态和动态加载此agent:

public static void premain(
  String agentArgs, Instrumentation inst) {
 
    LOGGER.info("[Agent] In premain method");
    String className = "com.baeldung.instrumentation.application.MyAtm";
    transformClass(className,inst);
}
public static void agentmain(
  String agentArgs, Instrumentation inst) {
 
    LOGGER.info("[Agent] In agentmain method");
    String className = "com.baeldung.instrumentation.application.MyAtm";
    transformClass(className,inst);
}

在每个方法中,我们声明要更改的类,然后使用transformClass方法向下挖掘以转换该类。

下面是transformClass方法的代码,我们定义它来帮助我们转换MyAtm类。

在这个方法中,我们找到要转换的类,并使用转换方法。此外,我们还将transformer添加到instrumentation engine:

private static void transformClass(
  String className, Instrumentation instrumentation) {
    Class<?> targetCls = null;
    ClassLoader targetClassLoader = null;
    // see if we can get the class using forName
    try {
        targetCls = Class.forName(className);
        targetClassLoader = targetCls.getClassLoader();
        transform(targetCls, targetClassLoader, instrumentation);
        return;
    } catch (Exception ex) {
        LOGGER.error("Class [{}] not found with Class.forName");
    }
    // otherwise iterate all loaded classes and find what we want
    for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
        if(clazz.getName().equals(className)) {
            targetCls = clazz;
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        }
    }
    throw new RuntimeException(
      "Failed to find class [" + className + "]");
}

private static void transform(
  Class<?> clazz, 
  ClassLoader classLoader,
  Instrumentation instrumentation) {
    AtmTransformer dt = new AtmTransformer(
      clazz.getName(), classLoader);
    instrumentation.addTransformer(dt, true);
    try {
        instrumentation.retransformClasses(clazz);
    } catch (Exception ex) {
        throw new RuntimeException(
          "Transform failed for: [" + clazz.getName() + "]", ex);
    }
}

现在,让我们为MyAtm类定义transformer

定义我们的Transformer

类转换器必须实现ClassFileTransformer并实现转换方法。

我们将使用Javassist向MyAtm类添加字节码,并添加一个记录ATW取款交易总时间的日志:

public class AtmTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
      ClassLoader loader, 
      String className, 
      Class<?> classBeingRedefined, 
      ProtectionDomain protectionDomain, 
      byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName
          .replaceAll("\\.", "/"); 
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) 
              && loader.equals(targetClassLoader)) {
 
            LOGGER.info("[Agent] Transforming class MyAtm");
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(
                  WITHDRAW_MONEY_METHOD);
                m.addLocalVariable(
                  "startTime", CtClass.longType);
                m.insertBefore(
                  "startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append(
                  "endTime = System.currentTimeMillis();");
                endBlock.append(
                  "opTime = (endTime-startTime)/1000;");

                endBlock.append(
                  "LOGGER.info(\"[Application] Withdrawal operation completed in:" +
                                "\" + opTime + \" seconds!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }
        return byteCode;
    }
}

创建Agent Manifest文件

最后,为了获得一个正常工作的Java agent,我们需要一个包含两个属性的manifest文件。

因此,我们可以在工具包官方文档中找到清单属性的完整列表。

在最终的Java agent jar文件中,我们将向manifest文件添加以下行:

Agent-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

我们的Java instrumentation agent现在已经完成。要运行它,请参阅本文的加载Java agent一节。

结论

在本文中,我们讨论了Java Instrumentation API。我们研究了如何静态和动态地将Java agent加载到JVM中。

我们还研究了如何从头开始创建自己的Java agent。

与往常一样,可以在Github上找到该示例的完整实现:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jvm