天天看點

如何為線上正在運作的服務的某個類加條日志?前言如何為正在飛馳的汽車換輪子總結

文章目錄

  • 前言
  • 如何為正在飛馳的汽車換輪子
    • 要把大象裝冰箱,一共分幾步
      • JVM加載class檔案過程
        • 類加載時機
        • 類加載器
      • 如何替換JVM上正在運作的class檔案
        • java.lang.instrument.Instrumentation
      • 如何替換伺服器上正在運作的class檔案
        • 直接操作位元組碼修改class
        • BTrace
  • 總結

前言

請您思考這樣一個問題:如何為線上服務的某個類加條日志?

您可能說,這還不簡單,在代碼裡加條日志,Git一送出,釋出一下不就搞定了!

但是如果這個服務特别重要,你沒辦法随意重新開機,你該怎麼辦呢?

本篇,我們就來聊一聊這個“頭疼的問題”。

如何為正在飛馳的汽車換輪子

你有沒有遇見過這樣的場景,一個接口的邏輯非常之複雜,涉及到大量的接口調用與内部多層次邏輯嵌套處理,好似這樣:

如何為線上正在運作的服務的某個類加條日志?前言如何為正在飛馳的汽車換輪子總結

突然某一天,産品來找你,說她登入不好用了!你得排查一下吧,看了半天代碼,你懷疑是她手機問題(逃),開個玩笑,你懷疑是serviceB的傳回值可能不對,但是代碼裡還沒有打日志,線上服務正在運作,現在是流量高峰期,你也沒辦法重新開機,産品還一個勁的催你解決,你想這可咋辦呀?

如何為線上正在運作的服務的某個類加條日志?前言如何為正在飛馳的汽車換輪子總結

别慌,我們來一起想想,有什麼辦法可以解決這個問題。

要把大象裝冰箱,一共分幾步

不知你有沒有看過趙本山與宋丹丹老師春晚經典的獨幕喜劇,其中經典的台詞就是:

要把大象裝冰箱,一共分幾步?

分析這個問題,也是一樣的道理,我們來拆解一下問題,我們的核心訴求是希望知道serviceB的傳回值是什麼,那麼最好的方式就是加點日志看看,但是還不能重新開機服務,那麼問題就變成了

怎麼在不重新開機服務的前提下,給代碼中加點日志呢?
如何為線上正在運作的服務的某個類加條日志?前言如何為正在飛馳的汽車換輪子總結

OK,我們再來把問題向下分解,Java類運作,都是需要編譯為

class

檔案,在JVM虛拟機上去執行的,那麼我們希望在Java類中加點日志輸出,本質上是需要生成一個新的

class

檔案,來替換掉JVM上正在運作的

class

檔案,這樣我們的訴求就可以達成了,那麼又帶來兩個問題,

1、JVM是如何加載

class

檔案的。

2、我們如何替換JVM上正在運作的

class

檔案。

我們一個一個來說。

JVM加載class檔案過程

那麼JVM是如何去加載

class

檔案的呢?我們來看一下Oracle官方的說法:

The Java Virtual Machine dynamically loads, links and initializes classes and interfaces. 
Loading is the process of finding the binary representation of a class 
or interface type with a particular name 
and creating a class or interface from that binary representation. 
Linking is the process of taking a class or interface 
and combining it into the run-time state of the Java Virtual Machine 
so that it can be executed. 
Initialization of a class or interface consists of 
executing the class or interface initialization method <clinit>
           

JVM把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是JVM的類加載機制。

類加載時機

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備、解析3個部分統稱為連接配接。

如何為線上正在運作的服務的某個類加條日志?前言如何為正在飛馳的汽車換輪子總結

那什麼情況下需要開始類加載過程的第一個階段,加載呢?

Java虛拟機規範中沒有進行強制限制,這一點可以交給虛拟機的具體實作來自由把握。但是對于初始化階段,虛拟機規範則是嚴格規定了有且隻有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

  1. 遇到

    new

    getstatic

    putstatic

    或者

    invokestatic

    這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字執行個體化對象的時候、讀取或設定一個類的靜态字段(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)的時候,以及調用一個類的靜态方法的時候。
  2. 使用

    java.lang.reflect

    包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛拟機會先初始化這個主類。
  5. 當使用JDK1.7的動态語言支援的時候,如果一個

    java.lang.invoke.MethodHandle

    執行個體最後的解析結果

    REF_getStatic

    REF_putStatic

    REF_invokeStatic

    的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

對于這5種觸發類進行初始化的場景,虛拟機規範使用了一個很強的限定語:“有且隻有”,這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。

類加載器

上面我們了解了JVM中判斷一個

class

何時應該被加載,那麼這個加載的活是誰來幹呢?答案是

類加載器。

JVM設計團隊把類加載階段中的“通過一個類的全限定名來擷取描述此類的二進制位元組流”這個動作放到JVM外部來實作,以便讓應用程式自己決定如何擷取所需的類。實作這個動作的代碼子產品稱為

類加載器。

類加載器雖然隻用于實作類的加載動作,但它在Java程式中起到的作用卻遠遠不限于類加載階段,對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立其在Java虛拟機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

這句話可以表達的更通俗一些:比較兩個類是否“相等”,隻有在這兩個類是

由同一個類加載器的前提下

才有意義,否則,即使這兩個類來源自同一個

class

檔案,被同一個虛拟機加載,隻要加載它們的類加載器不同,那這兩個類就必定不相等。

如何替換JVM上正在運作的class檔案

好了,第一個問題我們解釋完畢,那麼再來看第二個問題,如何替換JVM上正在運作的

class

檔案?

設計JDK的大師們早就預料到了這個情況,是以早在Java5中,就加入了這個能力,來解決這個問題。

java.lang.instrument.Instrumentation

那麼這個包是幹嘛用的呢?我們來看一下Oracle官方的解釋:

This class provides services needed to instrument Java programming language code. 
Instrumentation is the addition of byte-codes to methods for 
the purpose of gathering data to be utilized by tools. 
Since the changes are purely additive, these tools do not modify application state or behavior. 
Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.
           

簡單的說,使用

Instrumentation

,可以建構一個獨立于應用程式的代理程式(Agent),用來監測和協助運作在 JVM 上的程式,甚至能夠替換和修改某些類的定義。

有了這樣的功能,開發者就可以實作更為靈活的運作時虛拟機監控和 Java 類操作了,這樣的特性實際上提供了一種虛拟機級别支援的

AOP

實作方式,使得開發者無需對JDK做任何更新和改動,就可以實作某些

AOP

的功能了。

看到這裡,是不是有點小激動呢?通過JDK提供的這個能力,我們就可以在不重新開機JVM的前提下,動态的修改

class

的内容了,那麼具體該怎麼做呢?

翻閱API文檔,我們發現這麼兩個接口:

redefineClasses

retransformClasses

。一個是重新定義class,一個是修改class。這兩個大同小異,看

redefineClasses

的說明:

This method is used to replace the definition of a class 
without reference to the existing class file bytes, 
as one might do when recompiling from source for fix-and-continue debugging. 
Where the existing class file bytes are to be transformed 
(for example in bytecode instrumentation) retransformClasses 
should be used.
           

都是替換已經存在的class檔案,

redefineClasses

是自己提供位元組碼檔案替換掉已存在的class檔案,

retransformClasses

是在已存在的位元組碼檔案上修改後再替換之。

當然,運作時直接替換類很不安全。比如新的class檔案引用了一個不存在的類,或者把某個類的一個

field

給删除了等等,這些情況都會引發異常。是以如文檔中所言,

instrument

存在諸多的限制:

The redefinition may change method bodies, the constant pool 
and attributes. The redefinition must not add, 
remove or rename fields or methods, change the signatures of methods, or change inheritance. 
These restrictions maybe be lifted in future versions. 
The class file bytes are not checked, verified and installed 
until after the transformations have been applied, 
if the resultant bytes are in error this method 
will throw an exception.
           

什麼意思呢?重定義可能會更改方法體、常量池和屬性。重定義不得添加、移除、重命名字段或方法;不得更改方法簽名、繼承關系。在以後的版本中,可能會取消這些限制。在應用轉換之前,類檔案位元組不會被檢查、驗證和安裝。如果結果位元組錯誤,此方法将抛出異常。

可以了解成

Instrumentation

提供的能力,類比你租來的房子,隻允許在客廳裡放盆花,加個凳子之類的,但是你想砸承重牆,兩居改三居,那是萬萬不要想滴!

如何替換伺服器上正在運作的class檔案

好了,最開始的兩個問題,我們現在都已經有了答案,通過JDK提供的

Instrumentation

,我們就可以達成我們的訴求,現在我們理論方針是有了,那麼最關鍵的一步,我們如何去替換伺服器上正在運作的class檔案呢?

直接操作位元組碼修改class

通過JDK提供的

Instrumentation

,我們可以通過一些手段直接修改

class

檔案,在類中加一段列印日志的代碼,然後調用

retransformClasses

就可以了。

比較有名的操作位元組碼的架構有cglib、ASM,我們知道Spring就是通過cglib來直接操作位元組碼,生成代理對象的。

但是這裡又有一個問題:我們并非先知,不可能知道未來有沒有可能遇到這種問題。我們也不可能在每個工程中都開發一段專門做這些修改位元組碼、重新加載位元組碼的代碼。

同時ASM、cglib的使用并不友好,編寫代碼較為複雜,讓我們直接去開發這種代碼,難度顯然是有點頗高,那麼有沒有一個簡單一些,對開發人員較為友好的工具,也可以達成一樣的效果呢?

幸運的是,答案是肯定的。

BTrace

BTrace是一個開源項目,源碼托管于GitHub,在GitHub上也有很高的熱度,目前的Star數是3.5K+,位址https://github.com/btraceio/btrace。

那麼BTrace是什麼?

A safe, dynamic tracing tool for the Java platform.

BTrace can be used to dynamically trace a running Java program
 (similar to DTrace for OpenSolaris applications and OS). 
 BTrace dynamically instruments the classes of the target 
 application to inject tracing code ("bytecode tracing").
           

BTrace是基于Java語言的一個安全的、可提供動态追蹤服務的工具。BTrace基于ASM、Java Attach Api、Instruments開發,為使用者提供了很多注解。依靠這些注解,我們可以編寫BTrace腳本(簡單的Java代碼)達到我們想要的效果。

BTrace真像它說的這麼簡潔高效麼?我們來看一個官方的示例(https://github.com/btraceio/btrace/blob/master/samples/ArgArray.java):

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;

/**
 * This sample demonstrates regular expression
 * probe matching and getting input arguments
 * as an array - so that any overload variant
 * can be traced in "one place". This example
 * traces any "readXX" method on any class in
 * java.io package. Probed class, method and arg
 * array is printed in the action.
 */
@BTrace public class ArgArray {
    @OnMethod(
        clazz="/java\\.io\\..*/",
        method="/read.*/"
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
        println(pcn);
        println(pmn);
        printArray(args);
    }
}
           

這個示例示範了去攔截所有java.io包中所有類中以read開頭的方法,并列印類名、方法名和參數名。

似乎沒有騙我們,真的非常簡潔易用!

BTrace的功能非常強大,可以支援通過程序PID,來動态的執行操作,比如這樣:

btrace java程序PID ./scripts/你實作的Instrumentation去替換class的操作.java
           

關于BTrace的功能,這裡我僅做一個抛磚引玉,具體的能力,您可以查找相關資料,進行深入了解。

總結

好了,到現在,我們在回頭看本文開頭的問題,如何在不重新開機線上服務的前提下,在Java類中加行日志輸出呢?

要把大象裝冰箱,一共分幾步?

一樣,分三步:

1、把冰箱門打開(編寫Instruments的實作,替換掉目标class檔案)

2、把大象裝進去(在伺服器執行BTrace指令,執行替換操作)

3、把冰箱門關上(觀察日志,驗證class替換是否生效)

關于本文中提到的技術實作,為您推薦幾篇博文:

IBM社群的《Instrumentation 新功能》:

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

JDK8官方文檔:https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html

BTrace github:https://github.com/btraceio/btrace

本篇的内容就到這裡,感謝您的閱讀。

本文參考:

Java動态追蹤技術探究

BTrace

Instrumentation

Instrumentation 新功能

深入了解JVM虛拟機:(五)虛拟機類加載機制(上)

深入了解JVM虛拟機:(六)虛拟機類加載機制(下)