java注解處理器學習在編譯期修改語法樹教程
從需求說起
由于相關政策,最近公司安全部要求各系統(tǒng)在rpc接口調(diào)用的交互過程中把相應的參數(shù)及結果以相應的格式發(fā)送到安全部統(tǒng)一記錄,例如參數(shù)或結果含手機號和郵箱則格式如:“mail:axxx@126.com,phone:183xxxx1967”,其它系統(tǒng)信息等先忽略。
以便在數(shù)據(jù)泄露時可據(jù)此分析出數(shù)據(jù)的泄露源頭,以及若有黑客攻克有些接口時公司能有跡可循。
總體架構是各個接口把入?yún)⒑徒Y果打印日志,然后由統(tǒng)一的日志收集器收集日志通過mq發(fā)送到安全部。這樣每個系統(tǒng)只用在接口中添加參數(shù)和結果的打日志代碼。
添加打印日志代碼的方案
第一種方案,硬編碼
即直接在接口中編寫打印日志的代碼。這種工作量太大,公司各個部門,以往積累了眾多的項目,這樣改造的工作量太大。
第二種方案,AOP
利用aop框架,在切面類中打印日志??梢允褂胹pring 支持的aop功能或其他aop框架。
這個方案應該來說改動及工作量都大大降低,公司也是采用的這種方案。但是其弊端也很明顯,
一、是對框架的依賴(如用spring aop的話則非spring項目則不適用)
二、就是不同的項目或接口,入?yún)⒒蚪Y果變量名不同,如手機號:有的叫mobilePhone, 有的叫telephone等;但打印日志時要統(tǒng)一打印,如:phone:183xxxx1967; 所以要在參數(shù)上加注解,以表明打印日志時的名稱。這個重復工作量也不小。
第三種方案,修改class文件
針對第二種方案的弊端,我設計出這第三種方案。
利用相關技術,直接修改class文件,在接口中添加打印日志的字節(jié)碼。例如Javassist,asm等技術。
通過調(diào)研,在編譯期通過修改語法樹來達到修改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ù)日志的代碼。對于方法的結果是同樣的道理。
關于對語法樹的操作,網(wǎng)上資料相對較少及較為片段,介紹起來篇幅略長,故放在最后面進行介紹。
名稱分析模塊的思想及設計
剩下的一個關鍵問題是如何把不同的參數(shù)名統(tǒng)一成打印日志時的名稱,例如參數(shù)名為mobilePhone或telephone,但要打印的是phone。如果在注解屬性中指定的話,通過注解可以獲取到,但是當接口或參數(shù)很多的情況下也是一件重復性的力氣活。
故我設計出一種不讓開發(fā)人員手動指定名稱的方案,既對老的項目修改的少,又減輕開發(fā)人員的工作量,對新項目的應用也是高效率的。
如圖:

詞庫存儲(可用類的靜態(tài)字段存儲)需要打印的日志名稱及其對應的詞根及單詞。如:

上圖紅框為打印日志時要打印的名稱。綠框中為詞根及單詞:如若業(yè)務參數(shù)有用postbox作為郵箱變量名的則也可把postbox加入到mail的詞庫中。
這樣當業(yè)務參數(shù)為mobilePhone或telephone時,名稱分析模塊能夠分析出參數(shù)名包含phone詞根,從而得到對應的打印日志名“phone”;這就要求業(yè)務參數(shù)的名義要有具體的含義,不能隨便字母組合沒有含義的詞語,這應該也是每個公司開發(fā)時的基本要求。
這里只是舉例一個簡單的可行性方案,名稱分析模塊也可利用AI技術,根據(jù)輸入的變量名利用智能技術分析出此變量名的含義。
語法樹的操作:
下面對語法樹的操作進行詳細的說明,這里需要提到三個類:
JavacTrees 提供了待處理的抽象語法樹TreeMaker 封裝了創(chuàng)建AST節(jié)點的一些方法Names 提供了創(chuàng)建標識符的方法
可在init方法中對這三個類初始化,以便在process方法中利用它們對語法樹進行操作。如圖:

AST(抽象語法樹)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節(jié)點都代表著程序代碼中的一個語法結構,例如包、類型、修飾符、運算符、接口、返回值,甚至代碼注釋等都可以是一個語法結構。
JCTree的介紹

JCTree是語法樹元素的基類。
如上圖,它包含兩個屬性,
字段type表示語法結構的類型
字段pos用于指明當前語法樹節(jié)點(JCTree)在語法樹中的位置,因此我們不能直接用new關鍵字來創(chuàng)建語法樹節(jié)點,即使創(chuàng)建了也沒有意義,而要用TreeMaker來進行操作。
重點介紹幾個JCTree的子類:
JCStatement:聲明語法樹節(jié)點,常見的子類如下 JCBlock:語句塊JCReturn:return語句JCClassDecl:類定義JCVariableDecl:字段/變量定義JCIf: if語句
2.JCMethodDecl:方法定義語法樹節(jié)點
3.JCModifiers:訪問標志語法樹節(jié)點
4.JCExpression:表達式語法樹節(jié)點,常見的子類如下
JCAssign:賦值語句JCAssignOp:+=JCIdent:標識符,可以是變量,類型,關鍵字等等JCLiteral: 字面量表達式,如123, “string”等JCBinary:二元操作符
JCTree的子類很多,大部分可以從字面上看出其意義

如上圖,在jdk1.8.0_65里JCTree有58個子類。
下面具體介紹對語法樹的操作。
TreeMaker
TreeMaker創(chuàng)建語法樹節(jié)點的所有方法,創(chuàng)建時會為創(chuàng)建出來的JCTree設置pos字段,所以必須用上下文相關的TreeMaker對象來創(chuàng)建語法樹節(jié)點,而不能直接new語法樹節(jié)點。
TreeMaker.Modifiers
該方法用于創(chuàng)建訪問標志語法樹節(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:訪問標志
annotations:注解列表
其中flags可以用枚舉類型com.sun.tools.javac.code.Flags,且支持相加(Flags的值按二進制設計),如圖:

用法示例: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:訪問標志
name:方法名
restype:返回類型
typarams:泛型參數(shù)列表
params:參數(shù)列表
thrown:異常聲明列表
body:方法體
defaultValue:默認方法(可能是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:訪問標志
name:方法名
restype:返回類型
typarams:泛型參數(shù)列表
params:參數(shù)列表
thrown:異常聲明列表
body:方法體
defaultValue:默認方法(可能是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:訪問標志
vartype:類型
init:初始化語句
v:變量符號
TreeMaker.Ident
用于創(chuàng)建標識符語法樹節(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)建域訪問/方法訪問(當是方法訪問時,常和方法的調(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:.運算符左邊的表達式
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:賦值語句左邊表達式
rhs:賦值語句右邊表達式
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:訪問標志
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)建 域/方法 的多級訪問, 方法的標識只能是最后一個
例如: 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";
編譯器會對此語句進行優(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");
}
以上列出了一下常用的語句的語法樹操作方法,希望對理解操作語法樹有幫助。筆者也正在研究中,對編譯原理了解的話,學起來也就容易多了。
推薦幾本書,看完的話定定會受益匪淺:《編譯原理(高清龍書中文版)》、《兩周自制腳本語言》、《現(xiàn)代編譯器的Java實現(xiàn)(第二版)》。
更多關于java編譯期修改語法樹的資料請關注腳本之家其它相關文章!
相關文章
多jdk環(huán)境下指定springboot外部配置文件詳解
這篇文章主要為大家介紹了多jdk環(huán)境下指定springboot外部配置文件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Java StringBuilder 實現(xiàn)原理全攻略
StringBuilder 是 Java 提供的可變字符序列類,位于 java.lang 包中,專門用于高效處理字符串的拼接和修改操作,本文給大家介紹Java StringBuilder 實現(xiàn)原理深度解析,感興趣的朋友跟隨小編一起看看吧2025-09-09
解決SpringBoot運行Test時報錯:SpringBoot Unable to find
這篇文章主要介紹了SpringBoot運行Test時報錯:SpringBoot Unable to find a @SpringBootConfiguration,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
Java數(shù)據(jù)結構之圖的基礎概念和數(shù)據(jù)模型詳解
在現(xiàn)實生活中,有許多應用場景會包含很多點以及點點之間的連接,而這些應用場景我們都可以用即將要學習的圖這種數(shù)據(jù)結構去解決。本文主要介紹了圖的基礎概念和數(shù)據(jù)模型,感興趣的可以了解一下2022-11-11
SpringBoot中注解實現(xiàn)定時任務的兩種方式
這篇文章主要介紹了SpringBoot中注解實現(xiàn)定時任務的兩種方式,SpringBoot 定時任務是一種在SpringBoot應用中自動執(zhí)行任務的機制,通過使用Spring框架提供的@Scheduled注解,我們可以輕松地創(chuàng)建定時任務,需要的朋友可以參考下2023-10-10
Java跨session實現(xiàn)token接口測試過程圖解
這篇文章主要介紹了Java跨session實現(xiàn)token接口測試過程圖解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04
FeignClientFactoryBean創(chuàng)建動態(tài)代理詳細解讀
這篇文章主要介紹了FeignClientFactoryBean創(chuàng)建動態(tài)代理詳細解讀,當直接進去注冊的方法中,一步步放下走,都是直接放bean的定義信息中放入值,然后轉成BeanDefinitionHolder,最后在注冊到IOC容器中,需要的朋友可以參考下2023-11-11
mybatis初始化SqlSessionFactory失敗的幾個原因分析
這篇文章主要介紹了mybatis初始化SqlSessionFactory失敗的幾個原因分析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
microlog4android將Android Log日志寫到SD卡文件中實現(xiàn)方法
這篇文章主要介紹了microlog4android將Android Log日志寫到SD卡文件中實現(xiàn)方法的相關資料,需要的朋友可以參考下2016-10-10

