偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

如何讓Java編譯器幫你寫代碼

開發(fā)
本文主要是結合監(jiān)控埋點這個場景分享一種解決樣板化代碼的思路,希望能起到拋磚引玉的作用。下面將從組件介紹、技術選型過程、實現原理及部分源碼實現逐步展開講解。

背景

監(jiān)控是服務端應用需要具備的一個非常重要的能力,通過監(jiān)控可以直觀的看到核心業(yè)務指標、服務運行質量等,而要做到可監(jiān)控就需要進行相應的監(jiān)控埋點。大家在埋點過程中經常會編寫大量重復代碼,雖能實現基本功能,但耗時耗力,不夠優(yōu)雅。根據“DRY(Don't Repeater Yourself)"原則,這是代碼中的“壞味道”,對有代碼潔癖的人來講,這種重復是不可接受的。那有什么方法解決這種“重復”嗎?經過綜合調研,基于前端編譯器插樁技術,實現了一個埋點組件,通過織入埋點邏輯,讓Java 編譯器幫我們寫代碼。經過不斷打磨,已經被包括京東APP主站服務端在內的很多團隊廣泛使用。

本文主要是結合監(jiān)控埋點這個場景分享一種解決樣板化代碼的思路,希望能起到拋磚引玉的作用。下面將從組件介紹、技術選型過程、實現原理及部分源碼實現逐步展開講解。

組件介紹

京東內部監(jiān)控系統(tǒng)叫UMP,與所有的監(jiān)控系統(tǒng)一樣,核心部分有埋點、上報、分析整合、報警、看板等等,本文講的組件主要是為對監(jiān)控埋點原生能力的增強,提供一種更優(yōu)雅簡潔的實現。

我們先來看下傳統(tǒng)硬編碼的埋點方式,主要分為創(chuàng)建埋點對象、可用率記錄、提交埋點 3 個步驟:

圖片

通過上圖可以看到,真正的邏輯只有紅框中的范圍,為了完成埋點要把這段代碼都圍繞起來,代碼層級變深,可讀性差,所有埋點都是這樣的樣板代碼。

下面來看下使用組件后的埋點方式:

圖片

通過對比很容易看到,使用組件后的方式只要在方法上加一個注解就可以了,代碼可讀性有明顯的提升。組件由埋點封裝API和AST操作處理器 2 部分組成。

  • 埋點API封裝:在運行時被調用,對原生埋點做了封裝和抽象,方便使用者進行監(jiān)控KEY的擴展。
  • AST操作處理器:在編譯期調用,它將根據注解@UMP把埋點封裝API按照規(guī)則織入方法體內。

(注:結合京東實際業(yè)務場景,組件實現了fallback、自定義可用率、重名方法區(qū)分、配套的IDE插件、監(jiān)控key自定義生成規(guī)則等細節(jié)功能,由于本文主要是講解底層實現原理,詳細功能不在此贅述,感興趣的京東同事可以內網聯(lián)系咨詢:liushijie3)

技術選型過程

通過上面的示例代碼,相信很多人覺得這個功能很簡單,用 Spring AOP 很快就能搞定了。的確很多團隊也是這么做的,不過這個方案并不是那么完美,下面的選型分析中會有相關的解釋,請耐心往下看。如下圖,從軟件的開發(fā)周期來看,可織入埋點的時機主要有 3 個階段:編譯期、編譯后和運行期。

圖片

01編譯期

這里的編譯期指將Java源文件編譯為class字節(jié)碼的過程。Java編譯器提供了基于 JSR 269 規(guī)范[1]的注解處理器機制,通過操作AST (抽象語法樹,Abstract Syntax Tree,下同)實現邏輯的織入。業(yè)內有不少基于此機制的應用,比如Lombok 、MapStruct 、JPA 等;此機制的優(yōu)點是因為在編譯期執(zhí)行,可以將問題前置,沒有多余依賴,因此做出來的工具使用起來比較方便。缺點也很明顯,要熟練操作 AST并不是想的那么簡單,不理解前后關聯(lián)的流程寫出來的代碼不夠穩(wěn)定,因此要花大量時間熟悉編譯器底層原理。當然這個過程對使用者來講是沒有感知的。

02編譯后

編譯后是指編譯成 class 字節(jié)碼之后,通過字節(jié)碼進行增強的過程。此階段插樁需要適配不同的構建工具:Maven、Gradle、Ant、Ivy等,也需要使用方增加額外的構建配置,因此存在開發(fā)量大和使用不夠方便的問題,首先要排除掉此選項??赡苤挥袠O少數場景下才會需要在此階段插樁。

03運行期

運行期是指在程序啟動后,在運行時進行增強的過程,這個階段有 3 種方式可以織入邏輯,按照啟動順序,可以分為:靜態(tài) Agent、AOP 和動態(tài) Agent。

1、 靜態(tài) Agent

JVM 啟動時使用 -javaagent 載入指定 jar 包,調用 MANIFEST.MF 文件里的 Premain-Class 類的 premain 方法觸發(fā)織入邏輯。是技術中間件最常使用的方式,借助字節(jié)碼工具完成相關工作。應用此機制的中間件有很多,比如:京東內部的鏈路監(jiān)控 pfinder、外部開源的 skywalking 的探針、阿里的 TTL 等等。這種方式優(yōu)點是整體比較成熟,缺點主要是兼容性問題,要測試不同的 JDK 版本代價較大,出現問題只能在線上發(fā)現。同時如果不是專業(yè)的中間件團隊,還是存在一定的技術門檻,維護成本比較高;

2、 Spring AOP

Spring AOP大家都不陌生,通過 Spring 代理機制,可以在方法調用前后織入邏輯。AOP 最大的優(yōu)點是使用簡單,同樣存在不少缺點:

  1. 同一類內方法A調用方法B時,是無法走到切面的,這是Spring 官方文檔的解釋[2] “However, once the call has finally reached the target object (the SimplePojo reference in this case), any method calls that it may make on itself, such as this.bar() or this.foo(), are going to be invoked against the this reference, and not the proxy”。這個問題會導致內部方法調用的邏輯執(zhí)行不到。在監(jiān)控埋點這個場景下就會出現丟數據的情況;
  2. AOP只能環(huán)繞方法,方法體內部的邏輯沒有辦法干預??坎蹲疆惓E袛噙壿嬍遣粔虻?,有些場景需要是通過返回值狀態(tài)來判斷邏輯是否正常,使用介紹里面的示例代碼就是此種情況,這在 RPC 調用解析里是很平常的操作。
  3. 私有方法、靜態(tài)方法、final class和方法等場景無法走切面

3、 動態(tài) Agent

動態(tài)加載jar包,調用MANIFEST.MF文件中聲明的Agent-Class類的agentmain方法觸發(fā)織入邏輯。這種方式主要用來線上動態(tài)調試,使用此機制的中間件也有很多,比如:Btrace、Arthas等,此方式不適合常駐內存使用,因此要排除掉。

04最終方案

選擇通過上面的分析梳理可知,要實現重復代碼的抽象有 3 種方式:基于JSR 269 的插樁、基于 Java Agent 的字節(jié)碼增強、基于Spring AOP的自定義切面。接下來進一步的對比:

圖片

如上表所示,從實現成本上來看,AOP 最簡單,但這個方案不能覆蓋所有場景,存在一定的局限性,不符合我們追求極致的調性,因此首先排除。Java Agent 能達到的效果與 JSR 269 相同,但是啟動參數里需要增加 -javaagent 配置,有少量的運維工作,同時還有 JDK 兼容性的坑需要趟,對非中間件團隊來說,這種方式從長久看會帶來負擔,因此也要排除。

基于 JSR 269 的插樁方式,對Java編譯器工作流程的理解和 AST 的操作會帶來實現上的復雜性,前期投入比較大,但是組件一旦成型,會帶來一勞永逸的解決方案,可以很自信的講,插樁實現的組件是監(jiān)控埋點場景里的銀彈(事實證明了這點,不然也不敢這么吹)。

冰山之上,此組件給使用者帶來了簡潔優(yōu)雅的體驗,一個jar包,一行代碼,妙筆生花。那冰山之下是如何實現的呢?那就要從原理說起了。

插樁實現原理

簡單來講,插樁是在編譯期基于 JSR 269的注解處理器中操作AST的方式操縱語法節(jié)點,最終編譯到class文件中。要做好插樁理解相關的底層原理是必要的。大多數讀者對編譯器相關內容比較陌生,這里會用較大的篇幅做個相對系統(tǒng)的介紹。

Java編譯器是將源碼翻譯成 class 字節(jié)碼的工具,Java編譯器有多種實現:Open JDK的javac、Eclipse的ecj和ajc、IBM的jikes等,javac是公司內主要的編譯器,本文是基于Open JDK 1.8 講解。

作為一款工業(yè)級編譯器內部實現比較復雜,其涵蓋的內容足夠寫一本書了。結合本人對javac源碼的理解,嘗試通俗易懂的講清楚插樁涉及到的知識,有不盡之處歡迎指正。有興趣進一步研究的讀者建議閱讀 javac源碼[6]。下面將講解編譯器執(zhí)行流程,相關javac源碼導航,以及注解處理器如何運作。

01編譯器執(zhí)行流程

根據官網資料[3]javac 處理流程可以粗略的分為 3個部分:Parse and Enter、Annotation Processing、Analyse and Generate,如下圖:

圖片

Parse and EnterParse

階段主要通過詞法分析器(Scanner)讀取源碼生產 token 流,被語法分析器(JavacParser)消費構造出AST,Java代碼都可以通過AST表達出來,讀者可以通過JCTree查看相關的實現。為了讓讀者能更直觀的理解AST,本人做了一個源碼解析成AST后的圖形化展示:

示例源碼:

圖片

token流:[ package ] <- [ com ] <- [ . ] <- …... <- [ } ]解析成AST后如下:

圖片

Enter階段主要是根據AST填充符號表,此處為插樁之后的流程,因此不再展開。

Annotation Processing

注解處理階段,此處會調用基于 JSR269 規(guī)范的注解處理器,是javac對外的擴展。通過注解處理器讓開發(fā)者(指非javac開發(fā)者,下同)具備自定義執(zhí)行邏輯的能力,這就是插樁的關鍵。在這個階段,可以獲取到前一階段生成的AST,從而進行操作。

Analyse and Generate

分析AST并生成class字節(jié)碼,此處為插樁之后的流程,不再展開。

02相關javac源碼導航

javac觸發(fā)入口類路徑是:com.sun.tools.javac.Main,代碼如下:

圖片

經驗證Maven 執(zhí)行構建調的是此類中的main方法。其他構建工具未做驗證,猜測類似的。在JDK內部也提供了javax.tools.ToolProvider#getSystemJavaCompiler的入口,實際上內部實現也是調的這個類里的compile方法。

經過一系列的命令參數解析和初始化操作,最終調到真正的核心入口,方法是com.sun.tools.javac.main.JavaCompiler#compile,如下圖:

圖片

這里有3個關鍵調用:

  • 852行:初始化注解處理器,通過Main入口的調用是通過JDK SPI的方式收集。
  • 855 – 858行:對應前面流程圖里的Parse and Enter和Annotation Processing兩個階段的流程,其中方法processAnnotations便是執(zhí)行注解處理器的觸發(fā)入口。
  • 860行:對應Analyse and Generate階段的流程。

03注解處理器

Java從JDK 1.6 開始,引入了基于JSR 269 規(guī)范的注解處理器,允許開發(fā)者在編譯期間執(zhí)行自己的代碼邏輯。如本文講的UMP監(jiān)控埋點插樁組件一樣,由此衍生出了很多優(yōu)秀的技術組件,如前面提到的Lombok、Mapstruct等。注解處理器使用比較簡單,后面示例代碼有注解處理器簡單實現也可以參考。這里重點講一下注解處理器整體執(zhí)行原理:

  1. 編譯開始的時候,會執(zhí)行方法initProcessAnnotations (compile的截圖852行),以SPI的方式收集到所有的注解處理器,SPI對應接口:javax.annotation.processing.Processor。
  2. 在方法processAnnotations中執(zhí)行注解處理器調用方法JavacProcessingEnvironment#doProcessing。
  3. 所有的注解處理器處理完畢一次,稱為一輪(round),每輪開始會執(zhí)行一次Processor#init方法以便開發(fā)者自定義初始化信息,如緩存上下文等。初始化完成后,javac會根據注解、版本等條件過濾出符合條件的注解處理器,并調用其接口方法Processor#process,即開發(fā)者自定義的實現。
  4. 在開發(fā)者自定義的注解處理器里,實現AST操作的邏輯。
  5. 一輪執(zhí)行完成后,發(fā)現新的Java源文件或者class文件,則開啟新的一輪。直到不再產生Java或者class文件為止。有的開源項目實現注解處理器時,為了保證自身可以繼續(xù)執(zhí)行,會通過這個機制創(chuàng)建一個空白的Java文件達到目的,其實這也是理解原理的好處。
  6. 如果在一輪中未發(fā)現新的Java源文件和class文件產生則執(zhí)行最后一輪(lastRound)。最后一輪執(zhí)行完畢后,如果有新的Java源文件生成,則進行Parse and Enter 流程處理。到這里,整個注解處理器的流程就結束了。
  7. 進入Analyse and Generate階段,最終生成class,完成整體編譯。

接下來將通過UMP監(jiān)控埋點功能來展示怎么在注解處理器中操作AST。

源碼示例

關于AST 操作的探索,早在2008年就有相關資料了[4],Lombok、Mapstruct都是開源的工具,也可以用來參考學習。這里簡單講一個示例,展示如何插樁。

注解處理器使用框架

圖片

上圖展示了注解處理器具體的基本使用框架,init、process是注解處理器的核心方法,前者是初始化注解處理器的入口,后者是操作AST的入口。javac還提供了一些有用的工具類,比如:

  • TreeMaker:創(chuàng)建AST的工廠類,所有的節(jié)點都是繼承自JCTree,并通過TreeMaker完成創(chuàng)建。
  • JavacElements:操作Element的工具類,可以用來定位具體AST。

向類中織入一個import節(jié)點

這里舉一個簡單場景,向類中織入一個import節(jié)點:

圖片

為方便理解對代碼實現做了簡化,可以配合注釋查看如何織入:

圖片

圖片

總的來說,織入邏輯是通過TreeMaker創(chuàng)建AST 節(jié)點,并操作現有AST織入創(chuàng)建的節(jié)點,從而達到了織入代碼的目的。

反思與總結

到這里,講了埋點組件的使用、技術選型、以及插樁相關的內容,最終開發(fā)出來的組件在工作中也起到了很好的效果。但是在這個過程中有一些反思。

插樁門檻高

通過前面的內容不難得出一個事實,要實現一個小小的功能,需要開發(fā)者花費大量的精力去學習理解編譯器底層的一些原理。從ROI角度看,投入和產出是嚴重不成正比的。為了能提供可靠的實現,個人花費了大量業(yè)余時間去做技術選型分析和編譯器相關知識,可以說是純靠個人的興趣和一股倔勁一點點搭建起來的,細節(jié)是魔鬼,這個踩坑的過程比較枯燥。實際上插樁機制有很多通用的場景可以探索,之所以一直很少見到此類機制的應用。主要是其門檻較高,對大多數開發(fā)者來說比較陌生。因此降低開發(fā)者使用門檻才能讓一些想法變成現實。做一把好用的錘子,比砸入一個釘子要更有價值。在監(jiān)控埋點插樁組件真正落地時,在項目內做了一定抽象,并支持了一些開關、自定義鏈路跟蹤等功能。但從作用范圍來講是不夠的,所以下一步計劃做一個插樁方面的技術框架,從易用性、可維護性等方面做好進一步的抽象,同時做好可測試性相關工作,包含驗證各版本JDK的支持、各種Java語法的覆蓋等。

插樁是把雙刃劍

javac官方對修改AST的方式持保守態(tài)度,也存在一些爭議。然而時間是最好的驗證工具,從Lombok 等組件的發(fā)展看出,插樁機制是能經住長久考驗的。如何合理利用這種能力是非常重要的,合理使用可使系統(tǒng)簡潔優(yōu)雅,使用不當就等于在代碼里下毒了。所以要有節(jié)制的修改AST,要懂前后運行機制,圍繞通用的場景使用,避免濫用。

認識當前上下文環(huán)境的局限性

遇到問題時,如果在當前的上下文環(huán)境里找不到合適的解決方案,從這個環(huán)境跳出來換個維度也許能看到不同的風景。就像物理機到虛擬機再到現在的容器,都是打破了原來的規(guī)則逐步發(fā)展出新的技術生態(tài)。大多數的開發(fā)工作都是基于一個高層次的封裝上面進行,而突破往往都是從底層開始的,適當的時候也可以向下做一些探索,可能會產生一些有價值的東西。

責任編輯:未麗燕 來源: 京東零售技術
相關推薦

2012-04-05 09:13:17

C代碼

2022-12-12 12:04:59

ChatGPT代碼軟件

2017-03-20 18:01:55

編譯器匯編

2023-11-15 17:58:58

C++代碼

2020-12-04 07:49:54

AICtrl C代碼

2011-05-18 11:06:25

java編譯器

2010-09-16 15:57:25

Java編譯器

2010-01-14 15:29:44

C++編譯器

2020-04-02 15:39:51

代碼編譯器前端

2021-06-08 07:48:26

lambda表達式編譯器

2014-05-04 12:51:21

Javascript編譯器

2010-09-16 16:05:24

Java編譯器

2010-01-27 16:39:48

C++編譯器

2010-01-18 10:34:21

C++編譯器

2010-01-21 09:11:38

C++編譯器

2009-08-10 17:12:54

C#編譯器

2013-03-29 10:02:37

編譯器語言編譯開發(fā)

2010-03-23 11:17:16

Python 動態(tài)編譯

2023-04-14 10:40:45

工具編譯器優(yōu)化

2010-03-26 16:23:07

Visual Stud
點贊
收藏

51CTO技術棧公眾號