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的执行顺序;

转载于:https://my.oschina.net/Aruforce/blog/3073266

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值