歡迎您光臨本站 註冊首頁

用Java Instrumentation 在類載入時添加記錄

←手機掃碼閱讀     火星人 @ 2014-03-10 , reply:0

  在分析程序出錯的原因時,了解它當時的狀態將是非常有用的.在許多情況下,我們可以通過堆棧追蹤實現此目的,但這些信息經常都是不可用的,或者您需要的可能是程序在出錯時處理數據的相關信息.

  傳統做法是使用 log4j 和 Java Logging API 等記錄框架,然後再通過人工來編寫和維護所需的記錄語句.這種操作非常單調乏味且容易出錯,一般適合自動實現.Java 5 添加的 Java Instrumentation 機制允許您通過提供 "Java 代理" 來檢查和修改載入的類位元組代碼.

  本文將展示如何實現這種 Java 代理,它藉助標準 Java Logging API 透明地對類中所有方法添加入口和出口記錄.以 Hello World 為例:


 public class HelloWorld {

public static void main(String args[]) {

System.out.println("Hello World");

}

}

下面是添加了入口和出口記錄語句的同一個用例:


import java.util.Arrays;

import java.util.logging.Level;

import java.util.logging.Logger;

public class LoggingHelloWorld {

final static Logger _log = Logger.getLogger(LoggingHelloWorld.class.getName());

public static void main(String args[]) {

if (_log.isLoggable(Level.INFO)) {

_log.info("> main(args=" Arrays.asList(args) ")");

}

System.out.println("Hello World");

if (_log.isLoggable(Level.INFO)) {

_log.info("< main()");

}

}

}

  默認記錄程序生成的輸出格式大體為:

  


2007-12-22 22:08:52 LoggingHelloWorld main

INFO: > main(args=[])

Hello World

2007-12-22 22:08:52 LoggingHelloWorld main

INFO: < main()

  可以看到,每個記錄語句顯示為兩行:一行顯示時間戳、提供的記錄名稱和生成調用的方法,另一行是提供的記錄正文.

  本文的其餘部分將演示如何通過操作載入的位元組代碼使原始的 Hello World 程序和有記錄的 Hello World 程序有一致的行為效果.使用的操作機制是在 Java 5 中添加 Java Instrumentation.

  使用 Java Instrumentation API

  您可以通過 JVM arguments 調用 Java -javaagent:youragent.jar 或者 -javaagent:youragent.jar=argument 在試著運行指定的 main 之前使 Java 調用位於 youragent.jar 清單上的 premain(...) 方法.此 premain(...) 方法允許您通過系統類載入器註冊類文件 transformer,它能提供 transform(...) 方法.在此後的進程中,此方法會作為每個類的一部分進行調用,而且可以在由類載入器處理為實際 Class 之前操作實際代碼.

  為此,必須保證實現以下幾點:

  一個用來實現 ClassFileTransformer 的類.

  transform(...) 方法將在每個類載入時被調用.參數是整個類完全的、原始的位元組代碼.

  一個用來提供一個靜態空白點 premain() 方法的類.

  premain(...) 方法必須通過類載入器註冊上面的轉換器.它也能處理命令行上的參數.

  一個正確的 MANIFEST.MF 文件 .

  MANIFEST.MF 必須包含 Pre-Class: .. 行通過 premain() 方法訪問類.此外,使用 Boot-Class-Path: 訪問外部 .jar 文件.

  此代碼必須和清單一起放入 .jar 文件,否則它將失敗.

  com.runjva.instrumentation.LoggerAgent 示例代理

  本節列出一個名為 com.runjva.instrumentation.LoggerAgent 示例代理.它操作 java.lang.instrument.ClassFileTransformer 介面並提供所需的 premain(...) 方法.

  位於 transform(...) 方法中的實際位元組代碼操作通過 JBoss "Javassist" 庫來實現.這個庫提供一個 Java 片斷編譯器和高級位元組代碼操作常式.這個編譯器允許我們通過創建 Java 字元串片斷並編譯然後插入到合適的位置進行操作.

  簽名抽取和返回值位元組抽取方法是相當複雜的,並已經被放置在 com.runjva.instrumentation.JavassistHelper 內.它雖然沒有列出但在示例代碼 .zip 文件中可用.

  參閱 參考資料 示例代碼部分並鏈接到 Javassist 和相關背景文章.

  這是 com.runjva.instrumentation.LoggerAgent 類:

  


package com.runjva.instrumentation;

import java.lang.instrument.*;

import java.util.*;

import javassist.*;

public class LoggerAgent implements ClassFileTransformer {

public static void premain(String agentArgument,

Instrumentation instrumentation) {

if (agentArgument != null) {

String[] args = agentArgument.split(",");

Set argSet = new HashSet(Arrays.asList(args));

if (argSet.contains("time")) {

System.out.println("Start at " new Date());

Runtime.getRuntime().addShutdownHook(new Thread() {

public void run() {

System.out.println("Stop at " new Date());

}

});

}

// ... more agent option handling here

}

instrumentation.addTransformer(new LoggerAgent());

}

  premain(...) 作為類轉換器用來添加 LoggerAgent.它也將字元串參數看作一個逗號分隔的選項列表.如果給出選項 time,則將在此時或停機時列印出日期.


String def = "private static java.util.logging.Logger _log;";

String ifLog = "if (_log.isLoggable(java.util.logging.Level.INFO))";

String[] ignore = new String[] { "sun/", "java/", "javax/" };

public byte[] transform(ClassLoader loader, String className,

Class clazz, java.security.ProtectionDomain domain,

byte[] bytes) {

for (int i = 0; i < ignore.length; i ) {

if (className.startsWith(ignore[i])) {

return bytes;

}

}

return doClass(className, clazz, bytes);

}

  

transform(...) 方法在示例化為實際對象前由系統類載入器載入的每個類調用.每個類都包含載入這些類所需要的代碼,避免了對運行時庫類添加記錄器.需要查看類名稱,並返回未修改的庫類(注意:分隔符為斜線而不是點).


 private byte[] doClass(String name, Class clazz, byte[] b) {

ClassPool pool = ClassPool.getDefault();

CtClass cl = null;

try {

cl = pool.makeClass(new java.io.ByteArrayInputStream(b));

if (cl.isInterface() == false) {

CtField field = CtField.make(def, cl);

String getLogger = "java.util.logging.Logger.getLogger("

name.replace('/', '.') ".class.getName());";

cl.addField(field, getLogger);

CtBehavior[] methods = cl.getDeclaredBehaviors();

for (int i = 0; i < methods.length; i ) {

if (methods[i].isEmpty() == false) {

doMethod(methods[i]);

}

}

b = cl.toBytecode();

}

} catch (Exception e) {

System.err.println("Could not instrument " name

", exception : " e.getMessage());

} finally {

if (cl != null) {

cl.detach();

}

} return b;

}

  

doClass(...) 方法使用 Javassist 分析提供的位元組流.如果它是一個實際類(與介面相對),則會添加一個名為 _log 的記錄器欄位,並初始化為類名稱.每個非空方法通過 doMethod(...) 處理. finally 語句確保類定義從 Javassist 池中刪除以減少內存佔用.

  


private void doMethod(CtBehavior method)

throws NotFoundException, CannotCompileException {

String signature = JavassistHelper.getSignature(method);

String returnValue = JavassistHelper.returnValue(method);

method.insertBefore(ifLog "_log.info(">> " signature

");");

method.insertAfter(ifLog "_log.info("<< " signature

returnValue ");");

}

}

  doMethod(...) 類創建 if (_log.isLoggable(INFO))_log.info(...) 代碼並插入到每個方法的開頭和結尾.選擇這個級別作為無需任何記錄系統配置就可生成輸出的最低級別.

  需要注意的是 JavassistHelper 類在示例代碼 .zip 文件中是可用的.(請參閱 參考資料)

  示例 MANIFEST.MF 文件

  此處,只需要兩行:一行通過 premain 方法指出類,另一行使 Javassist 可用於代理.

  Premain-Class: com.runjva.instrumentation.LoggerAgentBoot-Class-Path: ../lib/javassist.jar

  需要注意,dist/loggeragent.jar 需要 lib/javassist.jar,即 ../lib 相對路徑.

  示例 build.xml 文件

  build.xml 文件包含一個編譯目標、一個 .jar 目標、一個傳統的 HelloWorld 目標和一個具有記錄器代理活動的 HelloWorld 目標.

  


project name="Logger Agent (Java 5 )" default="all">
<target name="all" depends="compile,jar,withoutAgent,withAgent"/>

<target name="withAgent" description="run with logging added by java agent">
<java fork="yes" classpath="bin" classname="com.runjva.demo.HelloWorld">
<jvmarg value="-javaagent:dist/loggeragent.jar=time"/>
</java>
</target>

<target name="withoutAgent" description="run normally">
<java fork="yes" classpath="bin" classname="com.runjva.demo.HelloWorld">
</java>
</target>


<target name="compile" description="compile classes">
<delete dir="bin" />
<mkdir dir="bin" />
<javac source="1.4" srcdir="src" destdir="bin" debug="true"
optimize="true" verbose="false" classpath="lib/javassist.jar">
</javac>
</target>

<target name="jar" depends="compile" description="create agent jar">
<jar basedir="bin" destfile="dist/loggeragent.jar" manifest="Manifest.mf"/>
</target>
</project>
  運行 ant 產生的輸出大體為:


 Buildfile: build.xml

compile:

[delete] Deleting directory /home/ravn/workspace/com.runjva.instrumentation/bin

[mkdir] Created dir: /home/ravn/workspace/com.runjva.instrumentation/bin

[javac] Compiling 3 source files to /home/ravn/workspace/com.runjva.instrumentation/bin

jar:

[jar] Building jar: /home/ravn/workspace/com.runjva.instrumentation/dist/loggeragent.

jarwithoutAgent:

[java] Hello World

withAgent:

[java] Start at Fri Apr 18 21:13:53 CEST 2008

[java] 18-04-2008 21:13:54 com.runjva.demo.HelloWorld main

[java] INFO: >> main(args=[]) [java] Hello World

[java] 18-04-2008 21:13:54 com.runjva.demo.HelloWorld main

[java] INFO: << main(args=[])

[java] Stop at Fri Apr 18 21:13:54 CEST 2008all:BUILD SUCCESSFULTotal time: 2 seconds

  此輸出顯示已經添加了記錄語句並實際生成了輸出.實際的語句順序可能在運行中有所改變,這是由於記錄語句將被寫入 System.err 和時間信息,輸出將從 HelloWorld 寫入 System.out.

  結束語

  Java Instrumentation API 可以不需要改變源代碼或編譯的位元組代碼透明地對運行時上的任何 Java 代碼添加方法-調用記錄.通過自動生成記錄語句,保證了他們總是最新的,這樣,減輕了程序員單調繁重的任務操作.


[火星人 ] 用Java Instrumentation 在類載入時添加記錄已經有839次圍觀

http://coctec.com/docs/java/show-post-61646.html