首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Instrumentation 實戰:類載入期自動植入方法進出日誌

     編輯:Java 相關知識

背景動機

排錯時若能自動記錄每個方法的進入/離開參數與回傳值,將大幅縮短除錯時間。傳統手工插入 log4j 語句既繁瑣又易遺漏。Java 5 引入的 Java Instrumentation API 允許透過 Java Agent 在類載入前操作位元碼,實現無侵入式自動化日誌植入。

核心架構三部曲

  1. ClassFileTransformer 實作類:transform() 方法接收原始位元碼,回傳修改後位元碼
  2. premain() 靜態進入點:JVM 啟動時呼叫,向系統類載入器註冊 Transformer
  3. MANIFEST.MF:指定 Premain-Class 與 Boot-Class-Path(載入 Javassist 等依賴)

LoggerAgent 完整實作

package com.runjva.instrumentation;

import java.lang.instrument.*;
import java.util.*;
import javassist.*;

public class LoggerAgent implements ClassFileTransformer {

    public static void premain(String agentArgs, Instrumentation inst) {
        // 解析參數,例如 -javaagent:loggeragent.jar=time
        if (agentArgs != null) {
            String[] args = agentArgs.split(",");
            Set<String> argSet = new HashSet<>(Arrays.asList(args));
            if (argSet.contains("time")) {
                System.out.println("Start at " + new Date());
                Runtime.getRuntime().addShutdownHook(new Thread(() -> 
                    System.out.println("Stop at " + new Date())));
            }
        }
        inst.addTransformer(new LoggerAgent());
    }

    // 排除系統類別避免效能影響
    private static final String[] IGNORE_PREFIX = {"sun/", "java/", "javax/"};
    private static final String LOGGER_FIELD = "private static java.util.logging.Logger _log;";
    private static final String IF_LOG = "if (_log.isLoggable(java.util.logging.Level.INFO))";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class clazz,
                            ProtectionDomain domain, byte[] bytes) {
        for (String prefix : IGNORE_PREFIX) {
            if (className.startsWith(prefix)) return bytes;
        }
        return doClass(className, clazz, bytes);
    }

    private byte[] doClass(String name, Class clazz, byte[] bytes) {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = null;
        try {
            ctClass = pool.makeClass(new java.io.ByteArrayInputStream(bytes));
            if (!ctClass.isInterface()) {
                // 1. 加入靜態 Logger 欄位
                CtField field = CtField.make(LOGGER_FIELD, ctClass);
                String getLogger = "java.util.logging.Logger.getLogger(\"" 
                    + name.replace('/', '.') + ".class.getName());";
                ctClass.addField(field, getLogger);

                // 2. 處理每個非空方法
                for (CtBehavior method : ctClass.getDeclaredBehaviors()) {
                    if (!method.isEmpty()) doMethod(method);
                }
                return ctClass.toBytecode();
            }
        } catch (Exception e) {
            System.err.println("Could not instrument " + name + ": " + e.getMessage());
        } finally {
            if (ctClass != null) ctClass.detach();
        }
        return bytes;
    }

    private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
        String signature = JavassistHelper.getSignature(method);  // 方法簽名含參數
        String returnValue = JavassistHelper.returnValue(method); // 回傳值片段

        // 植入進入日誌
        method.insertBefore(IF_LOG + "_log.info(\">> " + signature + "\");");
        // 植入離開日誌(含回傳值)
        method.insertAfter(IF_LOG + "_log.info(\"<< " + signature + returnValue + "\");");
    }
}

輔助類別:JavassistHelper(關鍵邏輯)

負責產生方法簽名字串(含參數值)與回傳值擷取位元碼,利用 Javassist 編譯 Java 片段插入。完整程式碼請參考附件原始碼。

MANIFEST.MF 關鍵內容

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

建置與執行

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

執行結果對比

原始程式:

Hello World

植入 Agent 後:

Start at Fri Apr 18 21:13:53 CEST 2008
18-04-2008 21:13:54 com.runjva.demo.HelloWorld main
INFO: >> main(args=[])
Hello World
18-04-2008 21:13:54 com.runjva.demo.HelloWorld main
INFO: << main(args=[])
Stop at Fri Apr 18 21:13:54 CEST 2008

優缺點與適用場景

優點缺點/限制
零侵入原始碼、編譯後類別檔啟動效能稍有影響(類載入期處理)
日誌自動同步程式碼變更大量日誌輸出可能影響執行效能
支援任意第三方庫類別需注意 final/private 方法無法插桿
生產環境可動態開關複雜回傳值序列化需額外處理

適用場景:生產環境疑難雜症排查、效能分析熱點方法、審計追蹤關鍵業務流程。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved