5°

Instrument In JVM

1. JVM agent

  1. JVM 提供了一个类优化服务(主要通过调整修改字节码),java.lang.Instrumention
  2. java.lang.Instrumention 提供了一系列 为JVM添加各种各样ClassFileTrasformer(这个类的就是字节码的修改逻辑)的接口
  3. JVM在加载新的类文件或者重新加载类文件时,会调用所有的ClassFileTrasformer实例的transform方法(有一套调用顺序和逻辑),输入为原始类文件的字节数组,最终需要返回一个新的类文件字节数组;整个修改的流程,不允许修改原类文件内的field的和方法签名);
  4. JVM 会使用这个新的类文件字节数组进行类的解析,在解析生成类的时,并不会修改这个类的任何实例的状态;

2. Agent启用方式

2.1 static:在main方法执行之前执行

2.1.1 基本要求

  1. 要求Agent Class有一个public static void premain(String agentArgs,Intrumention instrumention)这样签名的方法
  2. META-INF/MANIFEST.MF 文件:要求有Premain-Class 这个key,Value就是Agent的全类名
Premain-Class: com.aruforce.myAop.jvmagent.Agent
  1. command-Line
java -jar app.jar -javaagent:pathto/agent.jar

2.1.2 一个Agent

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.aruforce.jvm-agent</groupId>
    <artifactId>jvm-agent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
        <encoding>UTF-8</encoding>
    </properties>
    <build>
        <finalName>${project.artifactId}-${project.version}</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Premain-Class>com.aruforce.myAop.jvmagent.Agent</Premain-Class>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Agent:

package com.aruforce.myAop.jvmagent;
import java.lang.instrument.Instrumentation;
/**
 * @Author
 * JVM 提供了一种JVM启动后(在main方法之前)执行agentJar内premain方法的机制
 * 启动一个pre-Main-Agent的方式是使用command-line 参数指定Jar的path:- javaagent: pathToAgentJar[agentArgs];
 * 注意可以有多个-javaagent参数sample:
 * java -jar HelloWorld.jar -javaagent:pathToAgentA[agentArgs] -javaagent:pathToAgentB[agentArgs]
 */
public class Agent {
    public static Instrumentation instrumentation = null;
    public static String agentArgs = null;
    /**
     *  JVM初始化完成后,会按照命令行的指定Agent的顺序依次调用每个agentJar包内的Premain-class 值指定类的premain方法,全部执行完成后后再执行main方法;
     *  JVM会优先尝试执行{@link #premain(String agentArgs,Instrumentation instrument)},如果成功则执行下一个Agent的premain,否则尝试{@link #premain(String agentArgs)}
     * @param agentArgs 命令行参数
     * @param instrument JVM自动注入的一个工具类,提供了一套API 用于类文件字节码的修改buf等等,虚拟机级别的AOP 支持 ;
     */
    public static void premain(String agentArgs,Instrumentation instrument){
        System.out.println("now invoking method 'premain(String agentArgs,Instrumentation instrument)'");
        Agent.agentArgs = agentArgs;
        Agent.instrumentation = instrument;
    }
    /**
     *
     * @param agentArgs
     */
    public static void premain(String agentArgs){
        System.out.println("now invoking method 'premain(String agentArgs)'");
        Agent.agentArgs = agentArgs;
    }
}

2.1.3 使用jvm-agent

Main

package com.aruforce.myAop.app;
import com.aruforce.myAop.jvmagent.Agent;
public class Main{
   public static void main(String [] args){
       System.out.println("Agent.instrumention != null >>"+(Agent.instrumention!=null);
   }
}

CommandLine:

java com.aruforce.myAop.app.Main -javaagent:pathto/jvm-agent-0.0.1-SNAPSHOT.jar

log:

now invoking method 'premain(String agentArgs,Instrumentation instrument)'
Agent.instrumention != null >>true

2.2. agentmain(在main方法启动后执行)

当JVM已经处于running mode时候再启用agent

2.2.1 基本要求

  1. 要求Agent Class有一个public static void agentmain(String agentArgs,Intrumention instrumention)这样签名的方法
  2. META-INF/MANIFEST.MF文件:要求有Agent-Class 这个key,Value就是Agent的全类名
Agent-Class: com.aruforce.myAop.jvmagent.Agent

2.2.0 parent-pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.aruforce.myAop</groupId>
    <artifactId>myAop</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>jvm-agent</module>
        <module>app</module>
    </modules>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
        <encoding>UTF-8</encoding>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!--about log,代码只允许使用slf4j-api-->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.25</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.2.3</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!--log start-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>
</project>

2.2.2 一个Agent

pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>

<parent> <groupId>com.aruforce.myAop</groupId> <artifactId>myAop</artifactId> <version>0.0.1-SNAPSHOT</version> </parent>

<groupId>com.aruforce.myAop</groupId> <artifactId>jvm-agent</artifactId> <version>${parent.version}</version> <packaging>jar</packaging> <build> <finalName>${project.artifactId}-${project.version}</finalName> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>com.aruforce.myAop.jvmagent.Agent</Agent-Class> </manifestEntries> </archive> </configuration> </plugin> </plugins> </pluginManagement> </build> </project>

Agent:

package com.aruforce.myAop.jvmagent;
import java.lang.instrument.Instrumentation;

public class Agent { public static Instrumentation instrumentation = null; public static String agentArgs = null; public static void agentmain(String agentArgs,Instrumentation instrument){ System.out.println("now invoking method 'agentmain(String agentArgs,Instrumentation instrument)'"); Agent.agentArgs = agentArgs; Agent.instrumentation = instrument; instrumentation.addTransformer(new CustomClassTransformer()); } public static void agentmain(String agentArgs){ System.out.println("now invoking method 'agentmain(String agentArgs)'"); Agent.agentArgs = agentArgs; } }

CustomClassTransformer:

package com.aruforce.myAop.jvmagent;

import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;

public class CustomClassTransformer implements ClassFileTransformer { private static final String doChangeClassName = ""; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("now ["+className+"] is loaded"); return null;//return null 相当于没有对文件进行修改,实际上可以使用AspectJ等工具在这里对类文件进行增强,classfileBuffer 就是输入的class文件字节序列(并不一定是原始的类文件,可能时上个transformer处理过后的byte[]),不允许修改,自己new一个返回 } }

2.2.3 使用

pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.aruforce.myAop</groupId>
        <artifactId>myAop</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.aruforce.myAop</groupId>
    <artifactId>app</artifactId>
    <version>${parent.version}</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.7.0</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}-${project.version}</finalName>
    </build>
</project>

Test:

package com.aruforce.myAop.app;

import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException;

import java.io.IOException;

public class Test { private static final String agentPath = "D:\WorkSpacMvn\myAop\jvm-agent\target\jvm-agent-0.0.1-SNAPSHOT.jar"; public static void main(String[] args) throws IOException, AttachNotSupportedException { try { VirtualMachineAttchTools.attechAgent(agentPath); // 就是这么加载到JVM,(注意这个影响范围JVM级别的,而Spring那套是ClassLoader级别的,原理和触发机制不太一样) } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } Logic.doLogic();//展示Logic.class在被类加载器加载到JVM时,会被CustomClassFileTransFormer 处理 } }

VirtualMachineAttchTools: 一个工具类利用JVM tools attech agent到当前JVM 进程

package com.aruforce.myAop.app;

import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;

import java.io.IOException; import java.lang.management.ManagementFactory;

public class VirtualMachineAttchTools { public static void attechAgent(String agentPath) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String processDs = ManagementFactory.getRuntimeMXBean().getName(); String pid = ""; if (processDs.indexOf("@")>0){ pid = processDs.substring(0, processDs.indexOf("@")); } else{ pid = processDs; } VirtualMachine currentVM = VirtualMachine.attach(pid); currentVM.loadAgent(agentPath); currentVM.detach(); } }

Logic:

package com.aruforce.myAop.app;
public class Logic {
    public static void doLogic(){
        System.out.println("doLogic invoking");
    }
}

log:

now invoking method 'agentmain(String agentArgs,Instrumentation instrument)' //这个时attachJVM时执行的
now [java/lang/IndexOutOfBoundsException] is loaded
now [com/aruforce/myAop/app/Logic] is loaded
doLogic invoking
now [java/lang/Shutdown] is loaded
now [java/lang/Shutdown$Lock] is loaded

3. Instrumention 和ClassFileTranformer

3.1 Instrumention

JVM提供的一个机制:使JVM编写的Agent能够对运行在JVM内的程序进行修改和调整,(一般是通过修改字节码的形式达成目标);

3.1.1 启动方式

上面写的command-line(permain)或者vm.loadAgent(agentmain)

3.1.2 重要的API

3.1.2.1 addTransformer(ClassFileTransformer transformer,boolean canRetransform)

  1. 操作:向JVM注入一个ClassFileTransformer.
  2. 效果:所有JVM加载或者重新定义的类文件都会被这个transformer处理,但是不包括[所有的transformer]依赖的类;除此之外,即使当前这个transformer抛出了异常,也不影响下一个transformer的调用;canRetransform表示这个transformer支不支持对一个已被自己处理过的类文件再次处理?

3.1.2.2 retransformClasses(Class<?>... classes)

功能及执行时对JVM的影响:

  1. 对一组已经被加载的类文件重新处理(不管是不是处理过).
  2. 在这个过程中如果有活动的线程在使用某些method,这些活动线程会继续使用method原来的代码;
  3. 这个方法不会造成类的再次重新初始化,也就是说静态代码块不会再次执行
  4. 这个方法要求不允许增加或者减少方法,也不允许修改方法签名,也不允许修改继承关系

tip:

无法理解如何上面的1是如何做到的.

具体的执行过程:

  1. 输入为原始的字节码(编译后直接生成的字节码)
  2. 对于不支持的重新处理的Class文件的transformer,他们之前处理的结果会被复用,而类似于直接跳过执行tansform方法
  3. 对于支持的重新处理的transformer,他们的transform会被直接调用
  4. 处理完的结果会被JVM重新安装

注解参看下面的ClassTransformer执行顺序

  1. 不支持retransform的Java 编写的transformer
  2. 不支持retransform的Native的transformer(比如C编写的JVM扩展dll什么的)
  3. 支持retransform的Java 编写的transformer
  4. 支持retransform的Native的transformer

运行逻辑大概如下代码:

触发逻辑一般就是ClassLoader在Load或者redifineClass时间发生:

public class Instrumention{
    private ArrayList<ClassFileTransformer> retransCapbleformers = new ArrayList<ClassFileTransformer>();
    private ArrayList<ClassFileTransformer> retransInCapbleformers = new ArrayList<ClassFileTransformer>();
    Map<ClassFileTransformer,Map<String,byte []>> tranResult = new  ConcurrentHashMap<ClassFileTransformer,Map<String,byte []>>;
    Map<String,byte [] > originBytes = new HashMap<String,byte []>();
    public void addTransformer(ClassFileTransformer former,boolean retransCapble){
        if(retransCapble){
            retransCapbleformers.add(former);
        }else{
            retransInCapbleformers.add(former);
        }
        sort(transformers);//主要是排序
    }
    public byte [] transform(String className,byte [] classBytes){
        originBytes.put(className,classBytes);
        byte result = classBytes;
        //先由 不能重新处理的来
        for(ClassFileTransformer transformer:retransInCapbleformers){
            byte [] transBytes = transformer.tranform(className,classBytes);
            result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
            tranResult.get(transformer).put(className,transBytes);
        }
        //再由 能重新处理的来
        for(ClassFileTransformer transformer:retransInCapbleformers){
            byte [] transBytes = transformer.tranform(className,classBytes);
            result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
            tranResult.get(transformer).put(className,transBytes);
        }
        return result;
    }
    public byte [] retransform(String className){
        byte result = originBytes.get(className);
        //先由 不能重新处理的来.主要是获取到原来处理结果
        for(ClassFileTransformer transformer:retransInCapbleformers){
            byte [] transBytes = tranResult.get(transformer).get(className);
            result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
        }
        //再由 能重新处理的来
        for(ClassFileTransformer transformer:retransInCapbleformers){
            byte [] transBytes = transformer.tranform(className,result);
            result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
            tranResult.get(transformer).put(className,transBytes);
        }
        return result;
    }
}
class ClassLoader{
    Instrumention instrumention;
    public Class loadClass(String className){
        byte [] orignBytes = IOUTIL.loadClassFile(className);
        byte [] buffedBytes = instrumention.transform(className,orignBytes);
        return installClass(buffedBytes)
    }
    public Class reloadClass(String className){
        byte [] buffedBytes = instrumention.retransform(className);
        return installClass(buffedBytes)
    }
    public native Class installClass(byte [] classBytes);
}

native installClass 这个是我无法理解,涉及到JVM本身代码实现,到底什么情况什么时机下可以对方法栈进行替换?

3.2 ClassFileTransformer

就是一个接口,在JVM define某个类前,ClassFileTransformer可以对这个类字节码的转换;虚拟机级别的AOP支持

3.2.1 方法: transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)

  1. 分为支持与不支持 retransform的两个类型
  2. 一旦在JVM内注册完成,任何新类被define或者任何类被重新define
  3. classfileBuffer 这个就是传入的类文件,read-only方法规定不允许修改, 需要返回一个new byte[] 或者 null

3.2.2 执行顺序

请参看上面的解释性代码;

4. 源代码

myAop.git 虽然其完全不是AOP,等我点了ASM的科技树,我就来还债,稍微运行一下就可以;Agent代码和文章里面稍微有点不一样,只是用来说明ClassTransformer的执行顺序;

本文由【Aruforce】发布于开源中国,原文链接:https://my.oschina.net/Aruforce/blog/3073266

全部评论: 0

    我有话说: