一个简单JavaAgent的实现

一、什么是javaagent

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

1.1、概要

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

1.2、javaagent的基石

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

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 方法一样分别是代理和应用的入口点。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启动后启动代理,代理必须满足以下条件:

  • 清单文件包含 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 函数必须返回。如果,代理不能启动或抛出未捕获的异常,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-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 文件即可。除去手动配置外,可以使用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包格式如下:
在这里插入图片描述
其中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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值