javaagent入门指南

本文深入探讨JavaAgent技术,介绍如何利用Instrumentation接口在类加载前后修改字节码,实现类的动态代理。涵盖javaagent的配置、使用场景及其实现细节。

第一次见到javaagent时,是偶然了解到Spring的AOP中使用了一个Instrumentation技术,对自己来说是一个新的知识点,所以很好奇,因此查阅相关文档和资料进行学习,在此记录,如有不妥之处,请指正。

运行环境:

  • 操作系统:Windows10
  • jdk版本:openjdk version 11.0.7

概述

javaagent顾名思义就是一个java代理,我们知道任何一项java应用的启动都需要有一个入口函数,加载从入口函数开始一直扩散到整个应用。类在jvm中的加载顺序是:加载——>验证——>准备——>解析——>初始化。在加载阶段,jvm需要读入类的二进制信息,如果我们有一项技术,可以在验证阶段开始前对类的二进制信息进行操作,岂不是美滋滋?此时就需要用到java代理,从外部视角可以将它看做一个java程序的代理,可以通过它在类加载过程中对类信息进行合法操作。

当我们在启动应用程序时,可以使用-javaagent选项指定对应的javaagent:

-javaagent:<jar 路径>[=<选项>]
                  加载 Java 编程语言代理, 请参阅 java.lang.instrument

实际上instrument模块是在java.instrument下,不知道为什么它显示的不对:1592704787342

对于javaagent类,它的主函数前置启动方法(即main函数开始前的方法)的方法签名有特定的要求,必须要满足以下两种格式:

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

注意:文章的主函数前置启动方法主函数后置启方法都是我自己为了便于称呼起的。

jvm会优先加载第一种,如果第一种没有则加载第二种方法,反之则忽略:1592704979168

Instrumentation接口的方法如下:

public interface Instrumentation {
    /**
     * 注册一个Class文件转换器,转换器可以观测到除转换器所用到的Class外的所有Class的定义。
     * 转换器的顺序由ClassFileTransformer传入顺序决定,当转换器出现异常时,也会按照顺序
     * 调用下一个转换器。
     * 参数 canRetransform 设置是否允许重新转换。
     */
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    /**
     * Registers the supplied transformer.
     * <P>
     * Same as <code>addTransformer(transformer, false)</code>.
     *
     * @param transformer          the transformer to register
     * @throws java.lang.NullPointerException if passed a <code>null</code> transformer
     * @see    #addTransformer(ClassFileTransformer,boolean)
     */
    void addTransformer(ClassFileTransformer transformer);

    /**
     * 删除一个Class转换器
     */
    boolean removeTransformer(ClassFileTransformer transformer);

    /**
     * 判断JVM配置是否支持转换这个类
     */
    boolean isRetransformClassesSupported();

    /**
     * 在类加载之后,重新定义该Class
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

   /**
    * 判断JVM配置是否支持重新定义这个类
    */
    boolean isRedefineClassesSupported();

    /**
     * 使用提供的类文件重新定义提供的类集
     */
    void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;


    /**
     * 判断该Class是否被retransformation或redefinition修饰
     */
    boolean isModifiableClass(Class<?> theClass);

    /**
     * 返回已经被JVM加载的Class
     */
    @SuppressWarnings("rawtypes")
    Class[]  getAllLoadedClasses();

    /**
     * 返回被初始化的所有类的集合
     */
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    /**
     * 返回指定对象的大小
     */
    long getObjectSize(Object objectToSize);


    /**
     * 添加一个包含由引导类加载器定义的instrumentation类的JAR文件。
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /**
     * 添加一个包含由系统类加载器定义的instrumentation类的JAR文件。
     */
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    /**
     * 判断当前JVM配置是否支持设置本地方法前缀
     */
    boolean isNativeMethodPrefixSupported();

    /**
     * 设置本地方法浅醉
     */
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);

    /**
     * 重定义Module
     */
    void redefineModule(Module module,
                        Set<Module> extraReads,
                        Map<String, Set<Module>> extraExports,
                        Map<String, Set<Module>> extraOpens,
                        Set<Class<?>> extraUses,
                        Map<Class<?>, List<Class<?>>> extraProvides);

    /**
     * 判断Module是否重定义过
     */
    boolean isModifiableModule(Module module);
}

对于javaagent的主函数后置启动方法(main函数执行后)的方法签名有以下两种:

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

同主函数前置启动方法一样,不带Instrumentation参数的方法优先级较高。对于主函数后置启动方法,它在加载时通过java的Attach API实现:1592715492945

我们要注意一下VirtualMachineVirtualMachineDescriptor类:

  • VirtualMachine映射的是一个java虚拟机,它提供了java虚拟机的相关信息访问和操作。它里面有个attache(String id)方法,用于通过pid来连接对应的目标虚拟机。它的loadAgent(String agent)方法可以给虚拟机注册一个agent,在这个agent中会得到一个Instrumentation实例,该实例可以在class加载前改变class字节码,也可以在 class加载后改变其字节码。
  • VirtualMachineDescriptor是一个描述虚拟机的容器类,其中的displayName方法和id方法用于返回虚拟机的名字和pid。
  • VirtualMachineImplVirtualMachine的进一步实现,它继承了HotSpotVirtualMachine类(不同的虚拟机的类不同,我的是HotSpot),而HotSpotVirtualMachine类又继承了VirtualMachine类。

注意:对于主函数后置启动方法的绑定,需要在MASIFEST.MF中添加如下信息:

# Agent-Class的值是实现了agentmain方法的全限定类名
Agent-Class: bigkai.javaagent.AgentTest

示例

测试的目录结构为:

|--src
     |--main
          |--java
               |--AgentTest.java
               |--MainTest.java
     |--resources
          |--META-INF
               |--MANIFEST.MF

注意:当你使用maven进行打包时,默认情况下maven会自动生成MANIFEST.MF,从而将你的文件覆盖,所以你需要对pom.xml文件进行一些配置:

 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>bigkai.javaagent.AgentTest</Premain-Class>
                            <Agent-Class>bigkai.javaagent.AgentTest</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

premain方法

public class AgentTest {
    /**
     * 将在主程序的main方法之前运行
     * @param agentArgs 传递过来的参数
     * @param inst  agent技术主要使用的API可以使用它来改变和重新定义类的行为
     */
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("premain start");
        System.out.println(agentArgs);
    }

}
public class MainTest {
    public static void main(String[] args) throws Exception {
        System.out.println("main starting");
    }
}

修改MANIFEST.MF文件:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: AgentTest

当创建了上面两个类后,便对文件进行打包,然后根据上面所说,对JVM参数进行设置:

-javaagent:E:\test_demo-1.0-SNAPSHOT.jar

接下来你可以启动你的MainTest程序,结果打印出来后是这样的:1592716617243

agentmain方法

public class AgentTest {

    /**
     * 将在主程序的main方法之后运行
     * @param agentArgs 传递过来的参数
     * @param inst  agent技术主要使用的API可以使用它来改变和重新定义类的行为
     */
    public static void agentmain(String agentArgs, Instrumentation inst){
        System.out.println("agentmain start");
        System.out.println(agentArgs);
    }
}
public class MainTest {
    public static void main(String[] args) throws Exception {
        System.out.println("main starting");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list){
            if (vmd.displayName().equals("MainTest")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("E:\\test_demo-1.0-SNAPSHOT.jar");
                virtualMachine.detach();
            }
        }
    }
}

修改MANIFEST.MF文件:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: AgentTest
Agent-Class: AgentTest

当创建了上面两个类后,便对文件进行打包,然后根据上面所说,对JVM参数进行设置:

-javaagent:E:\test_demo-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true

接下来你可以启动你的MainTest程序,结果打印出来后是这样的:1592716686686

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值