Guide to Java Instrumentation – Java Instrumentation指南

最后修改: 2018年 7月 23日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this tutorial, we’re going to talk about Java Instrumentation API. It provides the ability to add byte-code to existing compiled Java classes.

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

We’ll also talk about java agents and how we use them to instrument our code.

我们还将讨论java代理以及我们如何使用它们来检测我们的代码。

2. Setup

2.设置

Throughout the article, we’ll build an app using instrumentation.

在整个文章中,我们将使用仪器化建立一个应用程序。

Our application will consist of two modules:

我们的应用程序将由两个模块组成。

  1. An ATM app that allows us to withdraw money
  2. And a Java agent that will allow us to measure the performance of our ATM by measuring the time invested spending money

The Java agent will modify the ATM byte-code allowing us to measure withdrawal time without having to modify the ATM app.

Java代理将修改ATM的字节码,使我们能够测量取款时间,而无需修改ATM应用程序。

Our project will have the following structure:

我们的项目将有以下结构。

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

Before getting too much into the details of instrumentation, let’s see what a java agent is.

在过多地讨论仪器化的细节之前,让我们看看什么是java代理。

3. What Is a Java Agent

3.什么是Java代理

In general, a java agent is just a specially crafted jar file. It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a JVM.

一般来说,java代理只是一个专门制作的jar文件。它利用JVM提供的Instrumentation API来改变JVM中加载的现有字节代码。

For an agent to work, we need to define two methods:

为了让代理发挥作用,我们需要定义两种方法。

  • premain – will statically load the agent using -javaagent parameter at JVM startup
  • agentmain – will dynamically load the agent into the JVM using the Java Attach API

An interesting concept to keep in mind is that a JVM implementation, like Oracle, OpenJDK, and others, can provide a mechanism to start agents dynamically, but it is not a requirement.

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

First, let’s see how we’d use an existing Java agent.

首先,让我们看看我们如何使用一个现有的Java代理。

After that, we’ll look at how we can create one from scratch to add the functionality we need in our byte-code.

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

4. Loading a Java Agent

4.加载一个Java代理

To be able to use the Java agent, we must first load it.

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

We have two types of load:

我们有两种类型的负载。

  • static – makes use of the premain to load the agent using -javaagent option
  • dynamic – makes use of the agentmain to load the agent into the JVM using the Java Attach API

Next, we’ll take a look at each type of load and explain how it works.

接下来,我们将看看每种类型的负载,并解释它是如何工作的。

4.1. Static Load

4.1.静态载荷

Loading a Java agent at application startup is called static load. Static load modifies the byte-code at startup time before any code is executed.

在应用程序启动时加载一个Java代理被称为静态加载。静态加载在启动时,在任何代码被执行之前修改字节码。

Keep in mind that the static load uses the premain method, which will run before any application code runs, to get it running we can execute:

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

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

It’s important to note that we should always put the –javaagent parameter before the –jar parameter.

值得注意的是,我们应该总是把-javaagent参数放在-jar参数之前。

Below are the logs for our command:

下面是我们的命令的日志。

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!

We can see when the premain method ran and when MyAtm class was transformed. We also see the two ATM withdrawal transactions logs which contain the time it took each operation to complete.

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

Remember that in our original application we didn’t have this time of completion for a transaction, it was added by our Java agent.

请记住,在我们原来的应用程序中,我们没有这个交易完成的时间,它是由我们的Java代理添加的。

4.2. Dynamic Load

4.2.动态载荷

The procedure of loading a Java agent into an already running JVM is called dynamic load. The agent is attached using the Java Attach API.

将Java代理加载到已经运行的JVM中的程序被称为动态加载。代理是使用Java Attach API附加的。

A more complex scenario is when we already have our ATM application running in production and we want to add the total time of transactions dynamically without downtime for our application.

一个更复杂的情况是,当我们已经在生产中运行我们的ATM应用程序,我们想动态地增加交易的总时间,而不给我们的应用程序带来停机。

Let’s write a small piece of code to do just that and we’ll call this class AgentLoader. For simplicity, we’ll put this class in the application jar file. So our application jar file can both start our application, and attach our agent to the ATM application:

让我们写一小段代码来完成这个任务,我们将这个类称为AgentLoader。为了简单起见,我们将把这个类放在应用程序的jar文件中。因此,我们的应用程序jar文件既可以启动我们的应用程序,又可以将我们的代理附加到ATM应用程序中。

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

Now that we have our AgentLoader, we start our application making sure that in the ten-second pause between transactions, we’ll attach our Java agent dynamically using the AgentLoader.

现在我们有了AgentLoader,我们启动我们的应用程序,确保在事务之间的10秒停顿时间里,我们将使用AgentLoader动态地附加我们的Java代理。

Let’s also add the glue that will allow us to either start the application or load the agent.

让我们也添加胶水,让我们可以启动应用程序或加载代理。

We’ll call this class Launcher and it will be our main jar file class:

我们将这个类称为Launcher,它将是我们的主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);
        }
    }
}

Starting the Application

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!

Attaching Java Agent

After the first operation, we attach the java agent to our JVM:

在第一次操作之后,我们将java代理附加到我们的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

Check Application Logs

Now that we attached our agent to the JVM we’ll see that we have the total completion time for the second ATM withdrawal operation.

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

This means that we added our functionality on the fly, while our application was running:

这意味着我们在应用程序运行的同时,在空中添加了我们的功能。

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!

5. Creating a Java Agent

5.创建一个Java代理

After learning how to use an agent, let’s see how we can create one. We’ll look at how to use Javassist to change byte-code and we’ll combine this with some instrumentation API methods.

在学习了如何使用代理之后,让我们看看如何创建一个代理。我们将看看如何使用Javassist来改变字节码,我们将把这与一些仪器化的API方法结合起来。

Since a java agent makes use of the Java Instrumentation API, before getting too deep into creating our agent, let’s see some of the most used methods in this API and a short description of what they do:

由于java代理使用了Java Instrumentation API,在深入创建我们的代理之前,让我们看看这个API中最常用的一些方法以及对其作用的简短描述。

  • addTransformer – adds a transformer to the instrumentation engine
  • getAllLoadedClasses – returns an array of all classes currently loaded by the JVM
  • retransformClasses – facilitates the instrumentation of already loaded classes by adding byte-code
  • removeTransformer – unregisters the supplied transformer
  • redefineClasses – redefine the supplied set of classes using the supplied class files, meaning that the class will be fully replaced, not modified as with retransformClasses

5.1. Create the Premain and Agentmain Methods

5.1.创建PremainAgentmain方法

We know that every Java agent needs at least one of the premain or agentmain methods. The latter is used for dynamic load, while the former is used to statically load a java agent into a JVM.

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

Let’s define both of them in our agent so that we’re able to load this agent both statically and dynamically:

让我们在我们的代理中定义这两者,这样我们就能够静态和动态地加载这个代理。

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

In each method, we declare the class that we want to change and then dig down to transform that class using the transformClass method.

在每个方法中,我们声明我们想要改变的类,然后向下挖掘,使用transformClass方法改造该类。

Below is the code for the transformClass method that we defined to help us transform MyAtm class.

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

In this method, we find the class we want to transform and using the transform method. Also, we add the transformer to the instrumentation engine:

在这个方法中,我们找到我们要转换的类,并使用transform方法。同时,我们将转化器添加到仪表引擎中。

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

With this out of the way, let’s define the transformer for MyAtm class.

说完这些,让我们为MyAtm类定义转换器。

5.2. Defining Our Transformer

5.2.定义我们的变换器

A class transformer must implement ClassFileTransformer and implement the transform method.

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

We’ll use Javassist to add byte-code to MyAtm class and add a log with the total ATW withdrawal transaction time:

我们将使用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;
    }
}

5.3. Creating an Agent Manifest File

5.3.创建一个代理清单文件

Finally, in order to get a working Java agent, we’ll need a manifest file with a couple of attributes.

最后,为了得到一个工作的Java代理,我们需要一个带有几个属性的清单文件。

Hence, we can find the full list of manifest attributes in the Instrumentation Package official documentation.

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

In the final Java agent jar file, we will add the following lines to the manifest file:

在最后的Java代理jar文件中,我们将在清单文件中添加以下几行。

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

Our Java instrumentation agent is now complete. To run it, please refer to Loading a Java Agent section of this article.

我们的Java仪表代理现在已经完成。要运行它,请参考本文的Loading a Java Agent部分。

6. Conclusion

6.结论

In this article, we talked about the Java Instrumentation API. We looked at how to load a Java agent into a JVM both statically and dynamically.

在这篇文章中,我们谈到了Java Instrumentation API。我们研究了如何静态和动态地将一个Java代理加载到JVM中。

We also looked at how we would go about creating our own Java agent from scratch.

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

As always, the full implementation of the example can be found over on Github.

一如既往,该示例的完整实现可以在Github上找到over