动手写一个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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

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