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:java

<?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:git

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

Mainshell

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:apache

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

log:api

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:app

<?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:jvm

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:maven

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:ide

<?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的執行順序;

相關文章
相關標籤/搜索