java.lang.instrument使用


Java在1.5引入java.lang.instrument,你可以由此實現一個Java agent,通過此agent來修改類的字節碼即改變一個類。

程序啟動之時啟動代理(pre-main)

通過java instrument 實現一個簡單的profiler。當然instrument並不限於profiler,instrument可以做很多事情,它類似一種更低級,更松耦合的AOP,可以從底層來改變一個類的行為,你可以由此產生無限的遐想。

接下來要做的事情,就是計算一個方法所花的時間,通常我們會在代碼這么寫: 
在方法開始開頭加入long stime = System.nanoTime();
在方法結尾通過System.nanoTime()-stime得出方法所花時間,

你不得不在你想監控的每個方法中寫入重復的代碼,好一點的情況,你可以用AOP來干這事,但總是感覺有點別扭,這種profiler的代碼還是打包在你的項目中,java instrument使得這更干凈。

寫agent類

import java.lang.instrument.Instrumentation;  
import java.lang.instrument.ClassFileTransformer;  
public class PerfMonAgent {  
    static private Instrumentation inst = null;  
    /** 
     * This method is called before the application’s main-method is called, 
     * when this agent is specified to the Java VM. 
     **/  
    public static void premain(String agentArgs, Instrumentation _inst) {  
        System.out.println("PerfMonAgent.premain() was called.");  
        // Initialize the static variables we use to track information.  
        inst = _inst;  
        // Set up the class-file transformer.  
        ClassFileTransformer trans = new PerfMonXformer();  
        System.out.println("Adding a PerfMonXformer instance to the JVM.");  
        inst.addTransformer(trans);  
    }  
}  

寫ClassFileTransformer類

import java.lang.instrument.ClassFileTransformer;  
import java.lang.instrument.IllegalClassFormatException;  
import java.security.ProtectionDomain;  
import javassist.CannotCompileException;  
import javassist.ClassPool;  
import javassist.CtBehavior;  
import javassist.CtClass;  
import javassist.NotFoundException;  
import javassist.expr.ExprEditor;  
import javassist.expr.MethodCall;  
public class PerfMonXformer implements ClassFileTransformer {  
    public byte[] transform(ClassLoader loader, String className,  
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,  
            byte[] classfileBuffer) throws IllegalClassFormatException {  
        byte[] transformed = null;  
        System.out.println("Transforming " + className);  
        ClassPool pool = ClassPool.getDefault();  
        CtClass cl = null;  
        try {  
            cl = pool.makeClass(new java.io.ByteArrayInputStream(  
                    classfileBuffer));  
            if (cl.isInterface() == false) {  
                CtBehavior[] methods = cl.getDeclaredBehaviors();  
                for (int i = 0; i < methods.length; i++) {  
                    if (methods[i].isEmpty() == false) {  
                        doMethod(methods[i]);  
                    }  
                }  
                transformed = cl.toBytecode();  
            }  
        } catch (Exception e) {  
            System.err.println("Could not instrument  " + className  
                    + ",  exception : " + e.getMessage());  
        } finally {  
            if (cl != null) {  
                cl.detach();  
            }  
        }  
        return transformed;  
    }  
    private void doMethod(CtBehavior method) throws NotFoundException,  
            CannotCompileException {  
        // method.insertBefore("long stime = System.nanoTime();");  
        // method.insertAfter("System.out.println(/"leave "+method.getName()+" and time:/"+(System.nanoTime()-stime));");  
        method.instrument(new ExprEditor() {  
            public void edit(MethodCall m) throws CannotCompileException {  
                m.replace("{ long stime = System.nanoTime(); $_ = $proceed($$); System.out.println(/""  
                                + m.getClassName()+"."+m.getMethodName()  
                                + ":/"+(System.nanoTime()-stime));}");  
            }  
        });  
    }  
}  

上面兩個類就是agent的核心了,jvm啟動時並會在應用加載前會調用 PerfMonAgent.premain, 然后PerfMonAgent.premain中實例化了一個定制的ClassFileTransforme即 PerfMonXformer並通過inst.addTransformer(trans);把PerfMonXformer的實例加入Instrumentation實例(由jvm傳入),這就使得應用中的類加載的時候, PerfMonXformer.transform都會被調用,你在此方法中可以改變加載的類,為了改變類的字節碼,使用了jboss的javassist,雖然你不一定要這么用,但jboss的javassist真的很強大,讓你很容易的改變類的字節碼。在上面的方法中通過改變類的字節碼,在每個類的方法入口中加入了long stime = System.nanoTime();,在方法的出口加入了System.out.println("methodClassName.methodName:"+(System.nanoTime()-stime));

打包agent

對於agent的打包,有點講究,

  1. jar的META-INF/MANIFEST.MF加入Premain-Class: xx, xx在此語境中就是我們的agent類,即org.toy.PerfMonAgent
  2. 如果你的agent類引入別的包,需使用Boot-Class-Path: xx,xx在此語境中就是上面提到的jboss javassit 即/home/pwlazy/.m2/repository/javassist/javassist/3.8.0 .GA/javassist-3.8.0.GA.jar

下面附上maven的pom

<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>org.toy</groupId>  
  <artifactId>toy-inst</artifactId>  
  <packaging>jar</packaging>  
  <version>1.0-SNAPSHOT</version>  
  <name>toy-inst</name>  
  <url>http://maven.apache.org</url>  
  <dependencies>  
     <dependency>  
      <groupId>javassist</groupId>  
      <artifactId>javassist</artifactId>  
      <version>3.8.0.GA</version>  
    </dependency>  
    <dependency>  
      <groupId>junit</groupId>  
      <artifactId>junit</artifactId>  
      <version>3.8.1</version>  
      <scope>test</scope>  
    </dependency>  
  </dependencies>  
   <build>  
    <plugins>  
      <plugin>  
        <groupId>org.apache.maven.plugins</groupId>  
        <artifactId>maven-jar-plugin</artifactId>  
        <version>2.2</version>  
        <configuration>  
          <archive>  
            <manifestEntries>  
              <Premain-Class>org.toy.PerfMonAgent</Premain-Class>  
              <Boot-Class-Path>/home/pwlazy/.m2/repository/javassist/javassist/3.8.0.GA/javassist-3.8.0.GA.jar</Boot-Class-Path>  
            </manifestEntries>  
          </archive>  
        </configuration>  
      </plugin>  
      <plugin>  
       <artifactId>maven-compiler-plugin </artifactId >  
              <configuration>  
                  <source> 1.6 </source >  
                  <target> 1.6 </target>  
              </configuration>  
     </plugin>  
    </plugins>  
  </build>  
</project>  

最終打成一個包toy-inst-1.0-SNAPSHOT.jar

隨便打包個應用

package org.toy;  
public class App {  
    public static void main(String[] args) {  
        new App().test();  
    }  
    public void test() {  
        System.out.println("Hello World!!");  
    }  
}  

最終打成一個包toy-1.0-SNAPSHOT.jar

執行命令運行應用

java -javaagent:target/toy-inst-1.0-SNAPSHOT.jar -cp /home/pwlazy/work/projects/toy/target/toy-1.0-SNAPSHOT.jar org.toy.App   

java選項中有-javaagent:xx,xx就是你的agent jar,java通過此選項加載agent,由agent來監控classpath下的應用。

最后的執行結果

PerfMonAgent.premain() was called.  
Adding a PerfMonXformer instance to the JVM.  
Transforming org/toy/App  
Hello World!!  
java.io.PrintStream.println:314216  
org.toy.App.test:540082  
Transforming java/lang/Shutdown  
Transforming java/lang/Shutdown$Lock  
java.lang.Shutdown.runHooks:29124  
java.lang.Shutdown.sequence:132768  

我們由執行結果可以看出執行順序以及通過改變org.toy.App的字節碼加入監控代碼確實生效了。你也可以發現通過instrment實現agent是的監控代碼和應用代碼完全隔離了。

程序啟動之后啟動代理(agent-main)

agentmain 需要在 main 函數開始運行后才啟動,這樣的時機應該如何確定呢,這樣的功能又如何實現呢?

在 Java SE 6 文檔當中,開發者也許無法在 java.lang.instrument 包相關的文檔部分看到明確的介紹,更加無法看到具體的應用 agnetmain 的例子。不過,在 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)。

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; 
            } 
        } 
    } 
 }

 import java.io.File; 
 import java.io.FileInputStream; 
 import java.io.IOException; 
 import java.io.InputStream; 
 import java.lang.instrument.ClassFileTransformer; 
 import java.lang.instrument.IllegalClassFormatException; 
 import java.security.ProtectionDomain; 

 class Transformer implements ClassFileTransformer { 

    public static final String classNumberReturns2 = "TransClass.class.2"; 

    public static byte[] getBytesFromFile(String fileName) { 
        try { 
            // precondition 
            File file = new File(fileName); 
            InputStream is = new FileInputStream(file); 
            long length = file.length(); 
            byte[] bytes = new byte[(int) length]; 

            // Read in the bytes 
            int offset = 0; 
            int numRead = 0; 
            while (offset <bytes.length 
                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { 
                offset += numRead; 
            } 

            if (offset < bytes.length) { 
                throw new IOException("Could not completely read file "
                        + file.getName()); 
            } 
            is.close(); 
            return bytes; 
        } catch (Exception e) { 
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName()); 
            return null; 
        } 
    } 

    public byte[] transform(ClassLoader l, String className, Class<?> c, 
            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { 
        if (!className.equals("TransClass")) { 
            return null; 
        } 
        return getBytesFromFile(classNumberReturns2); 

    } 
 }

 public class TransClass { 
     public int getNumber() { 
     return 1; 
    } 
 }

含有 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 文件差不多, 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(); 

 }

如果時間掌握得不太差的話,程序首先會在屏幕上打出 1,這是改動前的類的輸出,然后會打出一些 2,這個表示 agentmain 已經被 Attach API 成功附着到 JVM 上,代理程序生效了,當然,還可以看到“Agent Main Done”字樣的輸出。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM