插入式注解處理器在《深入理解Java虛擬機(jī)》一書中有一些介紹(前端編譯篇有提到),但一直沒(méi)有機(jī)會(huì)使用,直到碰到這個(gè)需求,覺(jué)得再合適不過(guò)了,就簡(jiǎn)單用了一下,這里做個(gè)記錄。
了解過(guò)lombok底層原理的都知道其使用的就是的插入式注解,那么今天筆者就以真實(shí)場(chǎng)景演示一下插入式注解的使用。
需求
我們?yōu)?a target="_blank">公司提供了一套通用的JAVA基礎(chǔ)組件包,組件包內(nèi)有不同的模塊,比如熔斷模塊、負(fù)載均模塊、rpc模塊等等,這些模塊均會(huì)被打成jar包,然后發(fā)布到公司的內(nèi)部代碼倉(cāng)庫(kù)中,供其他人引入使用。
這份代碼會(huì)不斷的迭代,我們希望可以通過(guò)promethus來(lái)監(jiān)控現(xiàn)在公司內(nèi)使用各版本代碼庫(kù)的比例,希望達(dá)到的效果圖如下:
我們希望看到每一個(gè)版本的使用率,這有利于我們做版本兼容,必要的時(shí)候可以對(duì)古早版本使用者溯源。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
問(wèn)題
需求似乎很簡(jiǎn)單,但真要獲取自身的jar版本號(hào)還是挺麻煩的,有個(gè)比較簡(jiǎn)單但陰間的辦法,就是給每一個(gè)組件都加上當(dāng)前的jar版本號(hào),寫到配置文件里或者直接設(shè)置成常量,這樣上報(bào)promethus時(shí)就可以直接獲取到j(luò)ar包版本號(hào)了,這個(gè)方法雖然可以解決問(wèn)題,但每次迭代版本都要跟著改一遍所有組件包的版本號(hào)數(shù)據(jù),過(guò)于麻煩。
有沒(méi)有更好的解決辦法呢?比如我們可不可以在gradle打包構(gòu)建時(shí)拿到j(luò)ar包的版本號(hào),然后注入到每個(gè)組件中去呢?就像lombok那樣,不需要寫get、set方法,只需要加個(gè)注解標(biāo)記就可以自動(dòng)注入get、set方法。
比如我們可以給每個(gè)組件定義一個(gè)空常量,加上自定義的注解:
@TrisceliVersion publicstaticfinalStringversion="";
然后像lombok生成set/get方法那樣注入真正的版本號(hào):
@TrisceliVersion publicstaticfinalStringversion="1.0.31-SNAPSHOT";
參考lombok的實(shí)現(xiàn),這其實(shí)是可以做到的,下面來(lái)看解決方案。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
解決
java中解析一個(gè)注解的方式主要有兩種:編譯期掃描、運(yùn)行期反射,這是lombok @Setter的實(shí)現(xiàn):
@Target({ElementType.FIELD,ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public@interfaceSetter{ //略... }
可以看到@Setter的Retention是SOURCE類型的,也就是說(shuō)這個(gè)注解只在編譯期有效,它甚至不會(huì)被編入class文件,所以lombok無(wú)疑是第一種解析方式,那用什么方式可以在編譯期就讓注解被解析到并執(zhí)行我們的解析代碼呢?答案就是定義插入式注解處理器(通過(guò)JSR-269提案定義的Pluggable Annotation Processing API實(shí)現(xiàn))
插入式注解處理器的觸發(fā)點(diǎn)如下圖所示:
也就是說(shuō)插入式注解處理器可以幫助我們?cè)诰幾g期修改抽象語(yǔ)法樹(shù)(AST)!所以現(xiàn)在我們只需要自定義一個(gè)這樣的處理器,然后其內(nèi)部拿到j(luò)ar版本信息(因?yàn)槭蔷幾g期,可以找到源碼的path,源碼里隨便搞個(gè)文件存放版本號(hào),然后用java io讀取進(jìn)來(lái)即可),再將注解對(duì)應(yīng)語(yǔ)法樹(shù)上的常量值設(shè)置成jar包版本號(hào),語(yǔ)法樹(shù)變了,最終生成的字節(jié)碼也會(huì)跟著變,這樣就實(shí)現(xiàn)了我們想在編譯期給常量version注入值的愿望。
自定義一個(gè)插入式注解處理器也很簡(jiǎn)單,首先要將自己的注解定義出來(lái):
@Documented @Retention(RetentionPolicy.SOURCE)//只在編譯期有效,最終不會(huì)打進(jìn)class文件中 @Target({ElementType.FIELD})//僅允許作用于類屬性之上 public@interfaceTrisceliVersion{ }
然后定義一個(gè)繼承了AbstractProcessor的處理器:
/** *{@linkAbstractProcessor}就屬于PluggableAnnotationProcessingAPI */ publicclassTrisceliVersionProcessorextendsAbstractProcessor{ privateJavacTreesjavacTrees; privateTreeMakertreeMaker; privateProcessingEnvironmentprocessingEnv; /** *初始化處理器 * *@paramprocessingEnv提供了一系列的實(shí)用工具 */ @SneakyThrows @Override publicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){ super.init(processingEnv); this.processingEnv=processingEnv; this.javacTrees=JavacTrees.instance(processingEnv); Contextcontext=((JavacProcessingEnvironment)processingEnv).getContext(); this.treeMaker=TreeMaker.instance(context); } @Override publicSourceVersiongetSupportedSourceVersion(){ returnSourceVersion.latest(); } @Override publicSetgetSupportedAnnotationTypes(){ HashSet set=newHashSet<>(); set.add(TrisceliVersion.class.getName());//支持解析的注解 returnset; } @Override publicbooleanprocess(Set?extends?TypeElement>annotations,RoundEnvironmentroundEnv){ for(TypeElementt:annotations){ for(Elemente:roundEnv.getElementsAnnotatedWith(t)){//獲取到給定注解的element(element可以是一個(gè)類、方法、包等) //JCVariableDecl為字段/變量定義語(yǔ)法樹(shù)節(jié)點(diǎn) JCTree.JCVariableDecljcv=(JCTree.JCVariableDecl)javacTrees.getTree(e); StringvarType=jcv.vartype.type.toString(); if(!"java.lang.String".equals(varType)){//限定變量類型必須是String類型,否則拋異常 printErrorMessage(e,"Type'"+varType+"'"+"isnotsupport."); } jcv.init=treeMaker.Literal(getVersion());//給這個(gè)字段賦值,也就是getVersion的返回值 } } returntrue; } /** *利用processingEnv內(nèi)的Messager對(duì)象輸出一些日志 * *@parameelement *@parammerrormessage */ privatevoidprintErrorMessage(Elemente,Stringm){ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,m,e); } privateStringgetVersion(){ /** *獲取version,這里省略掉復(fù)雜的代碼,直接返回固定值 */ return"v1.0.1"; }
定義好的處理器需要SPI機(jī)制被發(fā)現(xiàn),所以需要定義META.services:
測(cè)試
新建測(cè)試模塊,引入剛才寫好的代碼包:
這是Test類:
現(xiàn)在我們只需要讓gradle build一下,新得到的字節(jié)碼中該字段就有值了:
這只是插入式注解處理器 功能的冰山一角,既然它可以通過(guò)修改抽象語(yǔ)法樹(shù)來(lái)控制生成的字節(jié)碼,那么自然就有人能充分利用其特性來(lái)實(shí)現(xiàn)一些很酷的插件,比如lombok,我們?cè)僖膊挥脤懼T如set/get這種模板式的代碼了,只要我們足夠有創(chuàng)意,就可以讓基于這一套API實(shí)現(xiàn)的插件在功能上有很大的發(fā)揮空間。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
19395瀏覽量
230691 -
JAVA
+關(guān)注
關(guān)注
19文章
2973瀏覽量
104939 -
SPI
+關(guān)注
關(guān)注
17文章
1721瀏覽量
91920 -
RPC
+關(guān)注
關(guān)注
0文章
111瀏覽量
11548
原文標(biāo)題:項(xiàng)目終于用上了插入式注解,真香!
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論