Java成神之路——javaAgent(插桩,attach)

javaAgent

Javaagent 是什么?

javaAgent运行类加载器在加载类之前对类做出动态的修改.

运行java命令执行时添加参数 -javaagent指定打包好的agent的jar即可以. 可以定义多个agent,按指定顺序执行

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

字节码插桩,bTrace,Arthas 都是通过这种方式来实现。

javaAgent类方法

创建一个类里面定义agent方法

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

JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。Instrumentation是一个重要的参数。

Instrumentation 接口定义的方法

public interface Instrumentation {
    
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

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

    boolean isRetransformClassesSupported();

    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    boolean isRedefineClassesSupported();

    
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

    boolean isModifiableClass(Class<?> theClass);

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

  
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    //获取一个对象的大小
    long getObjectSize(Object objectToSize);


   
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    
    boolean isNativeMethodPrefixSupported();

    
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

Java Agent - Hello World

使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent: jar包路径 启动要代理的方法。

在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。

创建javaAgent类

public class HelloAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new DefineTransformer(), true);
    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

创建MANIFEST.MF

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.tttiger.HelloAgent

注意最后一行需要有空行。在idea中添加article,提示不接受不用管,idea会自动给你创建MANIFEST.MF可能会冲突,让idea创建,创建好后再去修改配置。

agent挂载运行

新创建一个类或jar,在运行的时候添加命令 -javaagent:e:/xxx.jar 打包完成的javaagentjar包路径。

使用maven进行打包

添加maven插件指定javaagent类,maven自动完成manifest配置,不用自己再去配置推荐

<plugin>
    <groupId>org.apache.maven.pluginsgroupId>
    <artifactId>maven-jar-pluginartifactId>
    <version>3.1.0version>
    <configuration>
        <archive>
            
            <manifest>
                <addClasspath>trueaddClasspath>
            manifest>
            <manifestEntries>
                <Premain-Class>com.rickiyang.learn.PreMainTraceAgentPremain-Class>
                <Agent-Class>com.rickiyang.learn.PreMainTraceAgentAgent-Class>
                <Can-Redefine-Classes>trueCan-Redefine-Classes>
                <Can-Retransform-Classes>trueCan-Retransform-Classes>
            manifestEntries>
        archive>
    configuration>
plugin>

MANIFEST.MF参数说明

Premain-Class :包含 premain 方法的类(类的全路径名)main方法运行前代理

Agent-Class :包含 agentmain 方法的类(类的全路径名)另一种代理main开始后可以修改类结构

Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)说白就是agent依赖的类

Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)

Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)

Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)


mainAgent

在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。
跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类:

//采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,
//这个时候需要借助Instrumentation#retransformClasses(Class... classes)
//让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

agentMain 主要用于对java程序的监控,调用java进程,将自己编写的agentMain 注入目标完成对程序的监控,修改。

创建agentmain

public class TestMainAgent {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("loadagent after main run.args=" + agentArgs);

        Class<?>[] classes = instrumentation.getAllLoadedClasses();

        for (Class<?> cls : classes)
        {
            System.out.println(cls.getName());
        }

        System.out.println("agent run completely.");
    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

添加maven插件打包

 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-jar-pluginartifactId>
                <version>3.1.0version>
                <configuration>
                    <archive>
                        
                        <manifest>
                            <addClasspath>trueaddClasspath>
                        manifest>
                        <manifestEntries>
                            <Agent-Class>com.tttiger.TestMainAgentAgent-Class>
                            <Can-Redefine-Classes>trueCan-Redefine-Classes>
                            <Can-Retransform-Classes>trueCan-Retransform-Classes>
                        manifestEntries>
                    archive>
                configuration>
            plugin>
        plugins>
    build>

测试agentMain插桩到其他类

另外启用了一个jvm进程,找到需要attach的jvm进程,让它加载agentMain,那么agentMain就会被加载到对方jvm执行。arthas就是使用这种方式attach进jvm进程,开启一个socket然后进行目标jvm的监控。

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, InterruptedException {
        //获取当前系统中所有 运行中的 虚拟机
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.tttiger.TestJVM")) {
                System.out.println(vmd.id());
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("e:/test-agentMain-1.0-SNAPSHOT.jar");
                virtualMachine.detach();
                System.out.println("attach");
            }
        }
        Thread.sleep(10000L);
    }

VirtualMachine 字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。

代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能

通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
Java成神之路——javaAgent(插桩,attach)_第1张图片

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:

  1. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
  2. 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
    2.1 新类和老类的父类必须相同;
    2.2 新类和老类实现的接口数也要相同,并且是相同的接口;
    2.3 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
    2.4 新类和老类新增或删除的方法必须是private static/final修饰的;
    2.5 可以修改方法体。

除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。

参考文章链接

咸鱼IT技术交流群:89248062,在这里有一群和你一样有爱、有追求、会生活的朋友! 大家在一起互相支持,共同陪伴,让自己每天都活在丰盛和喜乐中!同时还有庞大的小伙伴团体,在你遇到困扰时给予你及时的帮助,让你从自己的坑洞中快速爬出来,元气满满地重新投入到生活中

你可能感兴趣的:(Java成神之路)