java注解處理器學(xué)習(xí)在編譯期修改語法樹教程
從需求說起
由于相關(guān)政策,最近公司安全部要求各系統(tǒng)在rpc接口調(diào)用的交互過程中把相應(yīng)的參數(shù)及結(jié)果以相應(yīng)的格式發(fā)送到安全部統(tǒng)一記錄,例如參數(shù)或結(jié)果含手機(jī)號和郵箱則格式如:“mail:axxx@126.com,phone:183xxxx1967”,其它系統(tǒng)信息等先忽略。
以便在數(shù)據(jù)泄露時可據(jù)此分析出數(shù)據(jù)的泄露源頭,以及若有黑客攻克有些接口時公司能有跡可循。
總體架構(gòu)是各個接口把入?yún)⒑徒Y(jié)果打印日志,然后由統(tǒng)一的日志收集器收集日志通過mq發(fā)送到安全部。這樣每個系統(tǒng)只用在接口中添加參數(shù)和結(jié)果的打日志代碼。
添加打印日志代碼的方案
第一種方案,硬編碼
即直接在接口中編寫打印日志的代碼。這種工作量太大,公司各個部門,以往積累了眾多的項目,這樣改造的工作量太大。
第二種方案,AOP
利用aop框架,在切面類中打印日志??梢允褂胹pring 支持的aop功能或其他aop框架。
這個方案應(yīng)該來說改動及工作量都大大降低,公司也是采用的這種方案。但是其弊端也很明顯,
一、是對框架的依賴(如用spring aop的話則非spring項目則不適用)
二、就是不同的項目或接口,入?yún)⒒蚪Y(jié)果變量名不同,如手機(jī)號:有的叫mobilePhone, 有的叫telephone等;但打印日志時要統(tǒng)一打印,如:phone:183xxxx1967; 所以要在參數(shù)上加注解,以表明打印日志時的名稱。這個重復(fù)工作量也不小。
第三種方案,修改class文件
針對第二種方案的弊端,我設(shè)計出這第三種方案。
利用相關(guān)技術(shù),直接修改class文件,在接口中添加打印日志的字節(jié)碼。例如Javassist,asm等技術(shù)。
通過調(diào)研,在編譯期通過修改語法樹來達(dá)到修改class文件的效果,這種對用戶來說完全透明,不依賴任何框架。針對弊端二則發(fā)明名稱分析模塊,讓程序自動分析出參數(shù)的含義,從而避免了手工添加注解的麻煩。
下面就具體說明第三種方案的實現(xiàn):
利用JDK的注解處理器,可在編譯期間對注解處理,可以讀取、修改、添加抽象語法樹中的任意元素。
注解處理器是JDK1.6開始提供的功能,利用注解處理器可以干涉編譯器的行為,只要有足夠的創(chuàng)意,可以利用注解處理器實現(xiàn)許多原本只能在編碼中完成的事情。
注解處理器的用法:
1、實現(xiàn)AbstractProcessor
實現(xiàn)init和process方法
顧名思義,init是完成一些初始化工作;process完成具體的邏輯處理。后邊會有具體的例子說明。
2、添加注解
@SupportedAnnotationTypes 指定此注解處理器支持的注解,可用*指定所有注解
@SupportedSourceVersion 指定支持的java的版本
注解實例:
在process方法中可獲取到注解有@Safety的類和方法。
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Safety.class);
然后可遍歷方法,獲取到方法的入?yún)ⅲ治鋈雲(yún)?,給方法的語法樹添加打印參數(shù)日志的代碼。對于方法的結(jié)果是同樣的道理。
關(guān)于對語法樹的操作,網(wǎng)上資料相對較少及較為片段,介紹起來篇幅略長,故放在最后面進(jìn)行介紹。
名稱分析模塊的思想及設(shè)計
剩下的一個關(guān)鍵問題是如何把不同的參數(shù)名統(tǒng)一成打印日志時的名稱,例如參數(shù)名為mobilePhone或telephone,但要打印的是phone。如果在注解屬性中指定的話,通過注解可以獲取到,但是當(dāng)接口或參數(shù)很多的情況下也是一件重復(fù)性的力氣活。
故我設(shè)計出一種不讓開發(fā)人員手動指定名稱的方案,既對老的項目修改的少,又減輕開發(fā)人員的工作量,對新項目的應(yīng)用也是高效率的。
如圖:
詞庫存儲(可用類的靜態(tài)字段存儲)需要打印的日志名稱及其對應(yīng)的詞根及單詞。如:
上圖紅框為打印日志時要打印的名稱。綠框中為詞根及單詞:如若業(yè)務(wù)參數(shù)有用postbox作為郵箱變量名的則也可把postbox加入到mail的詞庫中。
這樣當(dāng)業(yè)務(wù)參數(shù)為mobilePhone或telephone時,名稱分析模塊能夠分析出參數(shù)名包含phone詞根,從而得到對應(yīng)的打印日志名“phone”;這就要求業(yè)務(wù)參數(shù)的名義要有具體的含義,不能隨便字母組合沒有含義的詞語,這應(yīng)該也是每個公司開發(fā)時的基本要求。
這里只是舉例一個簡單的可行性方案,名稱分析模塊也可利用AI技術(shù),根據(jù)輸入的變量名利用智能技術(shù)分析出此變量名的含義。
語法樹的操作:
下面對語法樹的操作進(jìn)行詳細(xì)的說明,這里需要提到三個類:
JavacTrees 提供了待處理的抽象語法樹TreeMaker 封裝了創(chuàng)建AST節(jié)點的一些方法Names 提供了創(chuàng)建標(biāo)識符的方法
可在init方法中對這三個類初始化,以便在process方法中利用它們對語法樹進(jìn)行操作。如圖:
AST(抽象語法樹)是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式,語法樹的每一個節(jié)點都代表著程序代碼中的一個語法結(jié)構(gòu),例如包、類型、修飾符、運算符、接口、返回值,甚至代碼注釋等都可以是一個語法結(jié)構(gòu)。
JCTree的介紹
JCTree是語法樹元素的基類。
如上圖,它包含兩個屬性,
字段type表示語法結(jié)構(gòu)的類型
字段pos用于指明當(dāng)前語法樹節(jié)點(JCTree)在語法樹中的位置,因此我們不能直接用new關(guān)鍵字來創(chuàng)建語法樹節(jié)點,即使創(chuàng)建了也沒有意義,而要用TreeMaker來進(jìn)行操作。
重點介紹幾個JCTree的子類:
JCStatement:聲明語法樹節(jié)點,常見的子類如下 JCBlock:語句塊JCReturn:return語句JCClassDecl:類定義JCVariableDecl:字段/變量定義JCIf: if語句
2.JCMethodDecl:方法定義語法樹節(jié)點
3.JCModifiers:訪問標(biāo)志語法樹節(jié)點
4.JCExpression:表達(dá)式語法樹節(jié)點,常見的子類如下
JCAssign:賦值語句JCAssignOp:+=JCIdent:標(biāo)識符,可以是變量,類型,關(guān)鍵字等等JCLiteral: 字面量表達(dá)式,如123, “string”等JCBinary:二元操作符
JCTree的子類很多,大部分可以從字面上看出其意義
如上圖,在jdk1.8.0_65里JCTree有58個子類。
下面具體介紹對語法樹的操作。
TreeMaker
TreeMaker創(chuàng)建語法樹節(jié)點的所有方法,創(chuàng)建時會為創(chuàng)建出來的JCTree設(shè)置pos字段,所以必須用上下文相關(guān)的TreeMaker對象來創(chuàng)建語法樹節(jié)點,而不能直接new語法樹節(jié)點。
TreeMaker.Modifiers
該方法用于創(chuàng)建訪問標(biāo)志語法樹節(jié)點(JCModifiers),源碼如下:
public JCModifiers Modifiers(long flags, List<JCAnnotation> annotations) { JCModifiers tree = new JCModifiers(flags, annotations); boolean noFlags = (flags & Flags.StandardFlags) == 0; tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos; return tree; } public JCModifiers Modifiers(long flags) { return Modifiers(flags, List.<JCAnnotation>nil()); }
參數(shù)解釋:
flags:訪問標(biāo)志
annotations:注解列表
其中flags可以用枚舉類型com.sun.tools.javac.code.Flags,且支持相加(Flags的值按二進(jìn)制設(shè)計),如圖:
用法示例:treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL);
TreeMaker.ClassDef
該方法用于創(chuàng)建類定義語法樹節(jié)點(JCClassDef),源碼如下:
public JCClassDecl ClassDef(JCModifiers mods, Name name, List<JCTypeParameter> typarams, JCTree extending, List<JCExpression> implementing, List<JCTree> defs) { JCClassDecl tree = new JCClassDecl(mods, name, typarams, extending, implementing, defs, null); tree.pos = pos; return tree; }
參數(shù)解釋:
mods:訪問標(biāo)志
name:方法名
restype:返回類型
typarams:泛型參數(shù)列表
params:參數(shù)列表
thrown:異常聲明列表
body:方法體
defaultValue:默認(rèn)方法(可能是interface中的那個default)
m:方法符號
mtype:方法類型。包含多種類型,泛型參數(shù)類型、方法參數(shù)類型,異常參數(shù)類型、返回參數(shù)類型
TreeMaker.MethodDef
用于創(chuàng)建方法定義語法樹節(jié)點(JCMethodDecl),源碼如下:
public JCMethodDecl MethodDef(JCModifiers mods, Name name, JCExpression restype, List<JCTypeParameter> typarams, List<JCVariableDecl> params, List<JCExpression> thrown, JCBlock body, JCExpression defaultValue) { JCMethodDecl tree = new JCMethodDecl(mods, name, restype, typarams, params, thrown, body, defaultValue, null); tree.pos = pos; return tree; }
參數(shù)解釋:
mods:訪問標(biāo)志
name:方法名
restype:返回類型
typarams:泛型參數(shù)列表
params:參數(shù)列表
thrown:異常聲明列表
body:方法體
defaultValue:默認(rèn)方法(可能是interface中的那個default)
m:方法符號
mtype:方法類型。包含多種類型,泛型參數(shù)類型、方法參數(shù)類型,異常參數(shù)類型、返回參數(shù)類型
TreeMaker.VarDef
用于創(chuàng)建字段/變量定義語法樹節(jié)點(JCVariableDecl),源碼如下:
public JCVariableDecl VarDef(JCModifiers mods, Name name, JCExpression vartype, JCExpression init) { JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null); tree.pos = pos; return tree; }
參數(shù)解釋:
mods:訪問標(biāo)志
vartype:類型
init:初始化語句
v:變量符號
TreeMaker.Ident
用于創(chuàng)建標(biāo)識符語法樹節(jié)點(JCIdent),源碼如下:
public JCIdent Ident(Name name) { JCIdent tree = new JCIdent(name, null); tree.pos = pos; return tree; } public JCIdent Ident(Symbol sym) { return (JCIdent)new JCIdent((sym.name != names.empty) ? sym.name : sym.flatName(), sym) .setPos(pos) .setType(sym.type); } public JCExpression Ident(JCVariableDecl param) { return Ident(param.sym); }
TreeMaker.Return
用于創(chuàng)建return語句語法樹節(jié)點(JCReturn)
TreeMaker.NewClass
用于創(chuàng)建new語句語法樹節(jié)點(JCNewClass),源碼如下:
public JCNewClass NewClass(JCExpression encl, List<JCExpression> typeargs, JCExpression clazz, List<JCExpression> args, JCClassDecl def) { JCNewClass tree = new JCNewClass(encl, typeargs, clazz, args, def); tree.pos = pos; return tree; }
參數(shù)解釋:
encl:不太明白此參數(shù)含義
typeargs:參數(shù)類型列表
clazz:待創(chuàng)建對象的類型
args:參數(shù)列表
def:類定義
TreeMaker.Select
用于創(chuàng)建域訪問/方法訪問(當(dāng)是方法訪問時,常和方法的調(diào)用TreeMaker.Apply一起使用語法樹節(jié)點(JCFieldAccess)
public JCFieldAccess Select(JCExpression selected, Name selector) { JCFieldAccess tree = new JCFieldAccess(selected, selector, null); tree.pos = pos; return tree; } public JCExpression Select(JCExpression base, Symbol sym) { return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type); }
參數(shù)解釋:
selected:.運算符左邊的表達(dá)式
selector:.運算符右邊的名字
TreeMaker.Apply
用于創(chuàng)建方法調(diào)用語法樹節(jié)點(JCMethodInvocation),源碼如下:
public JCMethodInvocation Apply(List<JCExpression> typeargs, JCExpression fn, List<JCExpression> args) { JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args); tree.pos = pos; return tree; }
參數(shù)解釋:
typeargs:參數(shù)類型列表
fn:調(diào)用語句
args:參數(shù)列表
TreeMaker.Assign
用于創(chuàng)建賦值語句語法樹節(jié)點(JCAssign),源碼如下:
public JCAssign Assign(JCExpression lhs, JCExpression rhs) { JCAssign tree = new JCAssign(lhs, rhs); tree.pos = pos; return tree; }
參數(shù)解釋:
lhs:賦值語句左邊表達(dá)式
rhs:賦值語句右邊表達(dá)式
TreeMaker.Exec
用于創(chuàng)建可執(zhí)行語句語法樹節(jié)點(JCExpressionStatement),源碼如下:
public JCExpressionStatement Exec(JCExpression expr) { JCExpressionStatement tree = new JCExpressionStatement(expr); tree.pos = pos; return tree; }
TreeMaker.Block
用于創(chuàng)建組合語句語法樹節(jié)點(JCBlock),源碼如下:
public JCBlock Block(long flags, List<JCStatement> stats) { JCBlock tree = new JCBlock(flags, stats); tree.pos = pos; return tree; }
參數(shù)解釋:
flags:訪問標(biāo)志
stats:語句列表
用法實例:
下面來介紹一下實例來加深對API用法的理解:
1、根據(jù)字符串獲取Name,(利用Names的fromString靜態(tài)方法)
private Name getNameFromString(String s) { return names.fromString(s); }
2、創(chuàng)建變量語句
private JCTree.JCVariableDecl makeVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression vartype, JCTree.JCExpression init) { return treeMaker.VarDef( modifiers, getNameFromString(name), //名字 vartype, //類型 init //初始化語句 ); }
3、創(chuàng)建 域/方法 的多級訪問, 方法的標(biāo)識只能是最后一個
例如: java.lang.System.out.println
private JCTree.JCExpression memberAccess(String components) { String[] componentArray = components.split("\\."); JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0])); for (int i = 1; i < componentArray.length; i++) { expr = treeMaker.Select(expr, getNameFromString(componentArray[i])); } return expr; }
4、聲明變量并賦值(利用以上包裝的方法)
JCTree.JCVariableDecl var = makeVarDef(treeMaker.Modifiers(0), "xiao", memberAccess("java.lang.String"), treeMaker.Literal("methodName"));
生成語句為:String xiao = "methodName";
5、給變量賦值
private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) { return treeMaker.Exec( treeMaker.Assign( lhs, rhs ) ); } makeAssignment(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("assignment test"));
生成的賦值語句為:xiao = "assignment test";
6、兩個字符串字面量相加并賦值
treeMaker.Exec( treeMaker.Assign(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Binary( JCTree.Tag.PLUS, treeMaker.Literal("-Binary operator one"), treeMaker.Literal("-Binary operator two") )) );
生成語句為:xiao = "-Binary operator one" + "-Binary operator two";
編譯器會對此語句進(jìn)行優(yōu)化,因為兩個字面量在編譯器即能確定,所以編譯器會把此語句優(yōu)化為:“xiao = "-Binary operator one-Binary operator two";”
7、+=語句
treeMaker.Exec( treeMaker.Assignop( JCTree.Tag.PLUS_ASG, treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("-Assignop test") ) );
生成語句為:xiao += "-Assignop test";
8、聲明整型變量并賦值
makeVarDef(treeMaker.Modifiers(0), "zhen", memberAccess("java.lang.Integer"), treeMaker.Literal(1));
生成語句為:Integer zhen = 1;
9、++語句
treeMaker.Exec( treeMaker.Unary( JCTree.Tag.PREINC, treeMaker.Ident(getNameFromString("zhen")) ) );
生成語句:zhen++;
10、加法語句
treeMaker.Exec( treeMaker.Unary( JCTree.Tag.PREINC, treeMaker.Ident(getNameFromString("zhen")) ) );
生成語句:zhen = zhen + 10;
11、方法調(diào)用(以輸出語句舉例)
treeMaker.Exec( treeMaker.Assign( treeMaker.Ident(getNameFromString("zhen")), treeMaker.Binary( JCTree.Tag.PLUS, treeMaker.Ident(getNameFromString("zhen")), treeMaker.Literal(10) )) );
生成語句:System.out.println(xiao);
12、方法調(diào)用,輸出字符串
JCTree.JCExpressionStatement printVar = treeMaker.Exec(treeMaker.Apply( List.of(memberAccess("java.lang.String")),//參數(shù)類型 memberAccess("java.lang.System.out.println"), List.of(treeMaker.Ident(getNameFromString("xiao"))) ) );
生成語句:System.out.println("xiao test zhen");
13、if語句
treeMaker.If( treeMaker.Binary( JCTree.Tag.LT, treeMaker.Ident(getNameFromString("zhen")), treeMaker.Literal(10) ), printVar, printLiteral );
生成語句:
if (zhen < 10) {
System.out.println(xiao);
} else {
System.out.println("xiao test zhen");
}
14、if語句(null判斷)
treeMaker.If( treeMaker.Parens( treeMaker.Binary( JCTree.Tag.NE, treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal(TypeTag.BOT, null)) ), printVar, printLiteral )
生成語句:
if (xiao != null) {
System.out.println(xiao);
} else {
System.out.println("xiao test zhen");
}
以上列出了一下常用的語句的語法樹操作方法,希望對理解操作語法樹有幫助。筆者也正在研究中,對編譯原理了解的話,學(xué)起來也就容易多了。
推薦幾本書,看完的話定定會受益匪淺:《編譯原理(高清龍書中文版)》、《兩周自制腳本語言》、《現(xiàn)代編譯器的Java實現(xiàn)(第二版)》。
更多關(guān)于java編譯期修改語法樹的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ReadWriteLock接口及其實現(xiàn)ReentrantReadWriteLock方法
下面小編就為大家?guī)硪黄猂eadWriteLock接口及其實現(xiàn)ReentrantReadWriteLock方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06Spring?MVC數(shù)據(jù)響應(yīng)處理詳解
這篇文章主要給大家介紹了關(guān)于Spring?MVC數(shù)據(jù)響應(yīng)處理的相關(guān)資料,本教程詳細(xì)的講解SpringMVC框架的使用,非常詳細(xì)的案例講解,一步一步帶你走入springmvc框架的核心,需要的朋友可以參考下2022-05-05Java?webservice的POST和GET請求調(diào)用方式
這篇文章主要介紹了Java?webservice的POST和GET請求調(diào)用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03java數(shù)據(jù)庫數(shù)據(jù)分批讀取的實現(xiàn)示例
在處理大量數(shù)據(jù)時,直接從數(shù)據(jù)庫一次性讀取所有數(shù)據(jù)可能會導(dǎo)致內(nèi)存溢出或者性能下降,本文就來介紹一下java數(shù)據(jù)庫數(shù)據(jù)分批讀取的實現(xiàn)示例,感興趣的可以了解一下2024-01-01Java實現(xiàn)HTML轉(zhuǎn)為Word的示例代碼
本文以Java代碼為例為大家詳細(xì)介紹如何實現(xiàn)將HTML文件轉(zhuǎn)為Word文檔(.docx、.doc)。在實際開發(fā)場景中可參考此方法來轉(zhuǎn)換,感興趣的可以了解一下2022-06-06