动手写一个javaagent

子曰:小胜靠智,大胜靠德,常胜靠身体。

1 什么是javaagent

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

1.1 概要

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


Java Agent

1.2 javaagent的基石

java.lang.instrument为javaagent 通过修改方法字节码的方式操作运行在JVM上的程序提供服务。javaagent以JAR包的形式部署,JAR文件清单中的属性指定要加载的代理类,以启动代理。javaagent的启动方式有以下几种:

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

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方法一样分别是代理和应用的入口点。JVM初始化完成后首先调用代理的premain函数,然后调用应用的main函数,premain方法必须返回后进程才能启动。

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方法或抛出了未被捕获的异常,JVM将会退出。

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

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

1.3.2 JVM启动后启动

实现可以提供在JVM启动之后再启动代理的机制。代理如何启动的细节特定于实现,通常应用程序已经启动,并且它的main方法已经被调用。如果实现支持在JVM启动后启动代理,代理必须满足以下条件:

  1. 清单文件包含Agent-Class属性,属性的值为代理类全名。

  2. 代理类必须实现 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函数必须返回。如果,代理不能启动或抛出未捕获的异常,JVM都会退出。

1.3.3 打包为可执行文件

如果代理打包到可执行JAR文件中,可执行JAR文件的清单中必须包含Launcher-Agent-Class 属性,指定一个在应用main函数调用之前代理启动的类。JVM尝试在代理上调用以下方法:

public static void agentmain(String agentArgs, Instrumentation inst)

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

public static void agentmain(String agentArgs)

agentArgs 参数的值必须为空字符串。
agentmain函数必须完成代理启动必须的所有初始化动作并在启动后返回。如果,代理无法启动或抛出未捕获的异常,JVM会退出。

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

系统类加载器负责加载代理JAR文件中的所有类,并且成为系统类加载器的未命名模块的成员。 系统类加载器通常也定义包含应用程序main方法的类。对代理类可见的所有类都对系统类加载器可见,必须满足下面的最低要求:

  • 启动层中的模块导出的包中的类。 启动层是否包含所有平台模块取决于初始模块或应用程序的启动方式。

  • 类可被系统类加载器定义。

  • 启动类加载器定义的所有代理的类为其未命名模块的成员。

如果代理类需要链接到不在启动层中的平台(或其他)模块中的类,则需要以确保这些模块位于启动层中的方式启动应用程序。 例如,在JDK实现中,--add-modules命令行选项可用于将模块添加到要在启动时解析的根模块集中。

启动类加载器可以加载代理支持的类(通过appendToBootstrapClassLoaderSearch或指定Boot-Class-Path属性)必须仅链接到定义启动类加载器的类。 无法保证启动类加载器可以在所有平台工作。

如果配置了自定义系统类加载器(通过getSystemClassLoader方法中指定的系统属性java.system.class.loader),则必须定义appendToSystemClassLoaderSearch中指定的appendToClassPathForInstrumentation方法。 换句话说,自定义系统类加载器必须支持将代理JAR文件添加到系统类加载器搜索范围内的机制。

1.4 javaagent清单属性

属性 说明 是否必选 默认值
Premain-Class 包含premain方法的类 依赖启动方式
Agent-Class 包含agentmain方法的类 依赖启动方式
Boot-Class-Path 启动类加载器搜索路径
Can-Redefine-Classes 是否可以重定义代理所需的类 false
Can-Retransform-Classes 是否能够重新转换此代理所需的类 false
Can-Set-Native-Method-Prefix 是否能够设置此代理所需的本机方法前缀 false

2 写一个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文件即可。除去手动配置外,可以使用maven插件在打包阶段自动生成,maven的插件配置如下:

           <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包格式如下:


java-agent.jpg

其中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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 131,304评论 18 138
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 11,350评论 2 56
  • 0 介绍 使用 Instrumentation,使得开发者可以构建一个独立于应用程序的代理程序(Agent),用来...
    七寸知架构阅读 25,632评论 3 84
  • 仓鼠在森林里找寻食物,她饿了,但是毕竟是成年了,所以想着不能再依赖鼠爸鼠妈了所以今天她决定自己在森林里面建造一个家...
    00_cca3阅读 588评论 0 0