天天看點

Java 灰盒測試實戰經驗分享三之 Java Instrumentation 更新版(Java SE 6)

前言

結合上一篇文章[Java 灰盒測試實戰經驗分享二之 Java Instrumentation 新功能(JAVA SE 5)]主要講解了如何使用Java的Instrumentation特性(Java SE 5提出)實作對Java程式中main函數啟動前的修改。

這一功能(或特性)可以靈活運用于如灰盒測試在内的不修改主程式而達到故障注入的架構設計中。

不過,還是存在一定的局限性。即,需要在java程式(Application)啟動之前預先定義好啟動腳本(如Manifast中定義Premain-Class),在進行程式的運作,方能達到外界修改main函數的效果。

這個局限性導緻不能在Java程式運作過程中實作動态代理。

是以,到了Java SE 6時代,提供了新的特性 - 虛拟機啟動後的動态Instrument。

Java 虛拟機(JVM)啟動後的動态Instrument

在Java SE 5當中,開發者隻能在premain當中施展想象力,所做的Instrumentation也僅限于main函數執行之前,這樣的方法存在一定的局限性。

在Java SE 5的基礎上,Java SE 6針對這種狀況做出了改進,開發者可以在main函數開始執行後,再啟動自己的Instrumentation程式。

在Java SE 6的Instrumentation當中,有一個跟premain稱得上“并駕齊驅”的“agentmain”方法,可以再main函數開始運作之後再運作。

跟premain函數一樣,程式員們可以編寫一個含有“agentmain”函數的Java類:

public static void agentmain (String agentArgs, Instrumentation inst);          [1] 
public static void agentmain (String agentArgs);            [2]
           

同樣,[1]的優先級高于[2],将會被優先執行。

跟premain函數一樣,開發者可以再agentmain中進行對類的各種操作。其中的agentArgs和Inst的作用跟premain一模一樣。

與“Premain-Class”類似,開發者必須在manifest檔案裡面設定“Agent-Class”來指定包含agentmain函數的類。

可是,跟premain不同的是,agentmain需要在main函數開始運作後才啟動,這樣的時機該如何确定是好呢,這樣的功能又如何實作是好呢?

在Java SE 6文檔當中,開發者也許無法在java.lang.instrument包相關的文檔部分看到明确的介紹,更無法看到具體的應用agentmain的例子。不過,在Java SE 6的新特性裡面,有一個不起眼的地方,揭示了agentmain的用法。

這就是Java SE 6當中的Attach API。

Attach API不是Java标準的API,而是Sun公司提供的一套擴充API。用來向目标JVM“附着(Attach)”代理工具程式的。有了它,開發者可以友善監控一個JVM,運作一個外加的代理程式。

Attach API很簡單,隻有2個主要的類,都在com.sun.tools.attach包裡面:VirtualMachine代表一個Java虛拟機,也就是程式需要監控的目标虛拟機,提供了JVM枚舉,Attach動作和Detach動作(Attach動作的相反 行為,從JVM上面解除一個代理)等等。

VirtualMachineDescriptor則是一個描述虛拟機的容器類,配合VirtualMachine類完成各種功能。

為了很簡單起見,我們舉例簡化如下:依然用類檔案替換的方法,将一個傳回1的函數替換成傳回2的函數。Attach API解除安裝一個線程裡面,用睡眠等待的方式,每隔半秒時間檢查一次所有的Java虛拟機,當發現有新的虛拟機出現的時候,就調用attach函數,随後再按照Attach API文檔裡面所描述的方式裝在Jar檔案。等到5秒的時候,attach程式自動結束。而在main函數裡,程式每隔半秒就會輸出一次傳回值(顯示出傳回值從1變成2)。

TransClass類和T按時former類的代碼不變,參照上一篇文章介紹。含有main函數的TestMainInJar代碼為:

public class TestMainInJar { 
   public static void main(String[] args) throws InterruptedException { 
       System.out.println(new TransClass().getNumber()); 
       int count = 0; 
       while (true) { 
           Thread.sleep(500); 
           count++; 
           int number = new TransClass().getNumber(); 
           System.out.println(number); 
           if (3 == number || count >= 10) { 
               break; 
           } 
       } 
   } 
}
           

含有agentmain的AgentMain類的代碼為:

import java.lang.instrument.ClassDefinition; 
import java.lang.instrument.Instrumentation; 
import java.lang.instrument.UnmodifiableClassException; 
 
public class AgentMain { 
   public static void agentmain(String agentArgs, Instrumentation inst) 
           throws ClassNotFoundException, UnmodifiableClassException, 
           InterruptedException { 
       inst.addTransformer(new Transformer (), true); 
       inst.retransformClasses(TransClass.class); 
       System.out.println("Agent Main Done"); 
   } 
}
           

其中,retransformClasses是Java SE 6裡面的新方法,它跟redefineClasses一樣,可以批量轉換類定義,多用于agentmain場合。

Jar檔案跟Premain那個例子裡面的Jar檔案差不多,也是把main和agentmain的類,TransClass,Transformer等類放在一起,打包為“TestInstrument1.jar”,而Jar檔案當中的Manifest檔案為:

Manifest-Version: 1.0 
Agent-Class: AgentMain
           

另外,為了運作Attach API,我們可以再洗诶個控制程式來模拟監控過程:

import com.sun.tools.attach.VirtualMachine; 
 import com.sun.tools.attach.VirtualMachineDescriptor; 
……
 // 一個運作 Attach API 的線程子類
 static class AttachThread extends Thread { 
         
 private final List<VirtualMachineDescriptor> listBefore; 
 
        private final String jar; 
 
        AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { 
            listBefore = vms;  // 記錄程式啟動時的 VM 集合
            jar = attachJar; 
        } 
 
        public void run() { 
            VirtualMachine vm = null; 
            List<VirtualMachineDescriptor> listAfter = null; 
            try { 
                int count = 0; 
                while (true) { 
                    listAfter = VirtualMachine.list(); 
                    for (VirtualMachineDescriptor vmd : listAfter) { 
                        if (!listBefore.contains(vmd)) { 
 // 如果 VM 有增加,我們就認為是被監控的 VM 啟動了
 // 這時,我們開始監控這個 VM 
                            vm = VirtualMachine.attach(vmd); 
                            break; 
                        } 
                    } 
                    Thread.sleep(500); 
                    count++; 
                    if (null != vm || count >= 10) { 
                        break; 
                    } 
                } 
                vm.loadAgent(jar); 
                vm.detach(); 
            } catch (Exception e) { 
                 ignore 
            } 
        } 
    } 
……
 public static void main(String[] args) throws InterruptedException {    
     new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start(); 
 
 }
           

運作時,可以首先運作上面這個啟動新線程的main函數,然後,在5秒鐘内,運作如下指令,啟動測試Jar檔案:

java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar
           

如果時間掌握的不太差的話,程式首先會在控制台輸出1,這是改動前的類的輸出,然後會列印出一些2,這表示agentmain已經被Attach API成功附着到JVM上,代理程式生效。

當然,還可以看到“Agent Main Done”字樣的輸出。

以上例子,僅僅隻是簡單實示例,簡單說明這個特性。真實的例子往往比較複雜,而且可能運作在分布式環境中的多個JVM之中。