一、什么是javaagent

javaagent是一个JVM“插件”,一种专门精心制作的.jar文件,它能够利用JVM提供的Instrumentation API。

1.1、概要

Java Agent由三部分组成:代理类、代理类元信息和JVM加载.jar和代理的机制,整体内容如下图所示:

javaagent使用 java javaagent_java

1.2、javaagent的基石

java.lang.instrument

javaagent的启动方式有以下几种:

  • 通过在命令行指定参数启动。
  • JVM启动后启动。例如,提供一种工具,该工具可以依附到已运行的应用,并允许在已运行的应用内加载代理。
  • 与应用一起打包为可执行文件。

1.3、启动 javaagent

1.3.1、命令行启动

命令行启动参数如下:

-javaagent:<jarpath>[=<options>]

<jarpath> :javaagent的路径,比如 /opt/var/Agent-1.0.0.jar
<options> : javaagent参数,参数的解析由javaagent负责。
javaagent JAR文件清单必须包含 Premain-Class 属性,属性的值为agent class的全路径名(包名+类名)。代理类必须实现 premain 方法,premain 方法和 main

premain

public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

JVM首先尝试在代理中调用签名为1的方法,如果代理类没有实现签名为1的方法,JVM尝试调用签名为2的方法:

代理类可以有一个 agentmain函数,函数会在JVM启动完成之后调用。如果,使用命令行启动代理,agentmain 方式不会被调用。

代理的所有参数被当作一个字符串通过 agentArgs

如果代理因为代理类无法被加载、代理类未实现 premain

javaagent的启动不要求实现一定提供命令行的方式,如果,实现支持通过命令行启动,实现必须支持在命令行中通过指定 -javaagent 参数启动。 -javaagent 可以在命令行中使用多次,启动多个代理。premain 函数的调用顺序和命令行中指定的顺序一致,多个代理可以使用相同 <jarpath>

没有一个严格模型来定义 premain 函数的工作范围,任何 main 函数可以做的工作,比如创建线程,在 premain

1.3.2、JVM启动后启动

实现可以提供在JVM启动之后再启动代理的机制。代理如何启动的细节特定于实现,通常应用程序已经启动,并且它的 main

  • 清单文件包含 Agent-Class
  • 代理类必须实现 public static agentmain

agentmain方法有以下两个函数签名:

public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)

JVM首先尝试调用具有签名1的方法,如果,代理类没有实现该方法,JVM尝试调用签名为2的方法。

代理类可以同时实现 premainagentmain 两个方法,当代理以命令行方式启动时,JVM调用 premain 函数,当代理在JVM启动之后启动时,JVM调用 agentmain 函数,而且JVM不会调用 premain

agentmain 函数参数的传递也是通过 agentArgs,所有参数组合为一个字符串,参数的解析由代理负责。

agentmain 函数必须完成启动代理所有必须的初始化动作,当启动完成后,agentmain

1.3.3、打包为可执行文件

如果代理打包到可执行JAR文件中,可执行JAR文件的清单中必须包含 Launcher-Agent-Class

public static void agentmain(String agentArgs, Instrumentation inst)

如果,代理类没有实现上述方法,JVM则调用下面的方法。

public static void agentmain(String agentArgs)

agentArgs

agentmain

1.3.4、加载代理类以及代理类可用的模块/类

系统类加载器负责加载代理JAR文件中的所有类,并且成为系统类加载器的未命名模块的成员。 系统类加载器通常也定义包含应用程序 main

  • 启动层中的模块导出的包中的类。 启动层是否包含所有平台模块取决于初始模块或应用程序的启动方式。
  • 类可被系统类加载器定义。
  • 启动类加载器定义的所有代理的类为其未命名模块的成员。

如果代理类需要链接到不在启动层中的平台(或其他)模块中的类,则需要以确保这些模块位于启动层中的方式启动应用程序。 例如,在JDK实现中,--add-modules

启动类加载器可以加载代理支持的类(通过 appendToBootstrapClassLoaderSearch

如果配置了自定义系统类加载器(通过 getSystemClassLoader 方法中指定的系统属性 java.system.class.loader ),则必须定义 appendToSystemClassLoaderSearch 中指定的 appendToClassPathForInstrumentation

1.4、javaagent清单属性

属性

说明

是否必选

默认值

Premain-Class

包含premain方法的类

依赖启动方式


Agent-Class

包含agentmain方法的类

依赖启动方式


Boot-Class-Path

启动类加载器搜索路径



Can-Redefine-Classis

是否可以重定义代理所需的类


false

Can-Retransform-Classis

是否能够重新转换此代理所需的类


false

Can-Set-Native-Method-Prefix

是否能够设置此代理所需的本机方法前缀


false

二、写一个Java Agent

基于上面的介绍,我们实现一个下载JVM中所有非系统类的javaagent。

整个开发过程包括以下三步:

  • 1)定义代理类,实现类下载功能;
  • 2)配置、打包;
  • 3)命令行启动测试。

2.1、代理类实现

实现 premain

package io.ct.java.agent;

import java.lang.instrument.Instrumentation;

public class AgentApplication {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.err.println("agent startup , args is " + arg);
        // 注册我们的文件下载函数
        instrumentation.addTransformer(new DumpClassesService());
    }
}

文件下载类实现 ClassFileTransformer

package io.ct.java.agent;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;

/**
 * Copyright (C), 2018-2018, open source
 * FileName: DumpClassesService
 *
 * @author : 大哥
 * Date:     2018/12/8 21:01
 */
public class DumpClassesService implements ClassFileTransformer {
    private static final List<String> SYSTEM_CLASS_PREFIX = Arrays.asList("java", "sum", "jdk");

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!isSystemClass(className)) {
            System.out.println("load class " + className);
            FileOutputStream fos = null;
            try {
                // 将类名统一命名为classNamedump.class格式
                fos = new FileOutputStream(className + "dump.class");
                fos.write(classfileBuffer);
                fos.flush();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            } finally {
                // 关闭文件输出流
                if (null != fos) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return classfileBuffer;
    }

    /**
     * 判断一个类是否为系统类
     *
     * @param className 类名
     * @return System Class then return true,else return false
     */
    private boolean isSystemClass(String className) {
        // 假设系统类的类名不为NULL而且不为空
        if (null == className || className.isEmpty()) {
            return false;
        }

        for (String prefix : SYSTEM_CLASS_PREFIX) {
            if (className.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }
}

2.2、配置MANIFEST.MF

MANIFEST.MF 文件两种方式生成:手动配置和自动生成,手动配置只需要在 resources 文件下创建 META-INF/MENIFEST.MF

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>io.ct.java.agent.AgentApplication</Premain-Class>
                            <Agent-Class>io.ct.java.agent.AgentApplication</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

生成的jar包格式如下:

javaagent使用 java javaagent_JVM_02


其中MANIFEST.MF的文件内容如下(不同的配置生成的文件内容不完全一致):

Manifest-Version: 1.0
Implementation-Title: agent
Premain-Class: io.ct.java.agent.AgentApplication
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: chentong
Agent-Class: io.ct.java.agent.AgentApplication
Can-Redefine-Classes: true
Implementation-Vendor-Id: io.ct.java
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_171
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/agent

2.3、命令行启动Java Agent

执行下面的命令,运行已经编译好的类Hello,可以在同级目录下生成一个名为Hellodump.class的文件。

java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello