AST簡介
AST(Abstract syntax tree)即為“抽象語法樹”,是編輯器對代碼的第一步加工之后的結(jié)果堵泽,是一個樹形式表示的源代碼。源代碼的每個元素映射到一個節(jié)點或子樹。
Java的編譯過程可以分成三個階段:
- 所有源文件會被解析成語法樹未桥。
- 調(diào)用注解處理器衙吩。如果注解處理器產(chǎn)生了新的源文件互妓,新文件也要進行編譯。
- 最后坤塞,語法樹會被分析并轉(zhuǎn)化成類文件冯勉。
例如:下面一段java代的抽象語法樹大概長這樣:
編輯器對代碼處理的流程大概是:
JavaTXT->詞語法分析-> 生成AST ->語義分析 -> 編譯字節(jié)碼
通過操作AST,可以達到修改源代碼的功能摹芙,相比AOP三劍客灼狰,他的時機更為提前:
什么是 AST 轉(zhuǎn)換?
AST 轉(zhuǎn)換 是在編譯過程中用來修改抽象語法樹結(jié)構(gòu)的代碼的名稱浮禾。修改 AST交胚,通過在將其轉(zhuǎn)換為字節(jié)碼之前增加附加節(jié)點,是更好的生成代碼的方法盈电。
之前我們了解到APT的三個弱點:
1蝴簇、預(yù)留入口不編譯會報紅,正常運行就可以
2挣轨、反射獲得新的類效率又太差
3军熏、無法實現(xiàn)定點插樁,只能生成新的類
AST則很好的解決了上面的問題卷扮。
如何操作AST荡澎?
1均践、直接使用Javac語法生成AST:
/* final int PRIME = 31; */ {
if (!fields.isEmpty() || callSuper) {
statements.append(maker.VarDef(maker.Modifiers(Flags.FINAL),
primeName, maker.TypeIdent(Javac.getCTCint(TypeTags.class, "INT")),
maker.Literal(31)));
}
}
在javac.tree的JCTree里面,幾乎可以看到所有常用語法的關(guān)鍵字:
比如JCImport摩幔,JCClassDecl彤委、JCIf、JCBreak或衡、JCReturn焦影、JCThrow
、JCDoWhileLoop封断、JCTry斯辰、JCCatch、JCAnnotation等坡疼,你可以直接用這些對象的操作組合成你想要的源碼彬呻,類似于javapoet的組裝模式。
2柄瑰、借助工具庫闸氮,更加簡單的操作AST
Rewrite、JavaParser等開源工具可以幫助你更簡單的操作AST
3教沾、擴展Lombok自定義注解處理器(自行了解)
AOP之AST:
AOP定位插樁蒲跨,相比重量級的AspectJ,ASM授翻、Javassisit或悲,修改AST可以做更加輕量級的代碼插樁實現(xiàn)方案:
void onClick(View v)
{
//插入你想要的埋點代碼;
doSomeThing();
}
AST可以實現(xiàn)任意代碼的增刪修改,相比其他AOP手段藏姐,效率更高(編輯器級別)隆箩。如果拿做飯為例子,AST就是你躺著你老婆給你做飯喂你吃,APT就是你老婆做飯羔杨,你打下手(類似留口子手動調(diào)用)捌臊;AspectJ就是叫外賣,用別人的廚具食材(編譯器)做好了給你送貨上門兜材,但是不能保證飯菜質(zhì)量理澎;ASM或Javassisit就是打車去飯店排隊點菜等上菜(類似Gradle插件在編譯過程中的Task流程);而運行期間的AOP可以利用反射曙寡,也就是你自己動手做黑暗料理了糠爬。
舉個例子:
正常運行期間,我們程序里面的斷言是不會起作用的:
assert str != null : "Must not be null";
如果我們想举庶,在編譯期間斷言自動轉(zhuǎn)化成if执隧,就可以使用操作AST來實現(xiàn),把assert手動改成if判斷:
基本步驟:
1、定義AbstractProcessor镀琉,注明@SupportedAnnotationTypes("*")
2峦嗤、初始化:
private int tally;
private Trees trees;
private TreeMaker make;
private Name.Table names;
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
trees = Trees.instance(env);
Context context = ((JavacProcessingEnvironment)
env).getContext();
make = TreeMaker.instance(context);
names = Names.instance(context).table;//Name.Table.instance(context);
tally = 0;
}
注意魔法:我們把ProcessingEnvironment強轉(zhuǎn)成JavacProcessingEnvironment,后面的操作都變成了IDE編輯器內(nèi)部的操作了屋摔。
3烁设、處理所有輸入的AST:
Set<? extends Element> elements = roundEnv.getRootElements();
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
JCTree tree = (JCTree) trees.getTree(each);
TreeTranslator visitor = new Inliner();
tree.accept(visitor);
}
}
4、操作AST增加代碼
@Override
public void visitAssert(JCTree.JCAssert tree) {
super.visitAssert(tree);
JCTree.JCStatement newNode = makeIfThrowException(tree);
result = newNode;
tally++;
}
private JCTree.JCStatement makeIfThrowException(JCTree.JCAssert node) {
// make: if (!(condition) throw new AssertionError(detail);
List<JCTree.JCExpression> args = node.getDetail() == null
? List.<JCTree.JCExpression>nil()
: List.of(node.detail);
JCTree.JCExpression expr = make.NewClass(
null,
null,
make.Ident(names.fromString("AssertionError")),
args,
null);
return make.If(
make.Unary(JCTree.Tag.NOT, node.cond),
make.Throw(expo),
null);
}
5钓试、查看最終結(jié)果:
再來個例子:我們還可以使用AST自動清除線上Log装黑,防止裸奔:
private class LogClear extends TreeTranslator {
@Override
public void visitBlock(JCTree.JCBlock jcBlock) {
super.visitBlock(jcBlock);
final List<JCTree.JCStatement> statements = jcBlock.getStatements();
if (statements != null && statements.size() > 0) {
List<JCTree.JCStatement> out = List.nil();
for (JCTree.JCStatement statement : statements) {
if (statement.toString().contains("Log.")) {
mMessager.printMessage(Diagnostic.Kind.WARNING, this.getClass().getCanonicalName() + " 自動清除Log: LogClear:" + statement.toString());
} else {
out = out.append(statement);
}
}
jcBlock.stats = out;
}
}
}
同時還可以避免log參數(shù)的計算以及方法調(diào)用的額外無用開銷。
擴展AST:
1弓熏、樣板代碼less:著名的Lombok恋谭,注解@Data,自動生成setter挽鞠、getter箕别,toString、equals滞谢、hashCode等模版方法
Lombok除了可以修改AST,還可以聯(lián)合編輯器做消除警告和代碼提示除抛。在保存代碼的時候狮杨,悄無聲息的生成了新的AST,并且在編輯器上給予你代碼提示的功能到忽。然而你看到的橄教,仍然是最初的簡潔的代碼。
簡直可以媲美kotlin的data:
data class Mountain(val name: String, val age: Int)
2喘漏、自定義Lint护蝶,實現(xiàn)CodeReview自動化
Lint從第一個版本就選擇了lombok-ast作為自己的AST Parser,并且用了很久翩迈。但是Java語言本身在不斷更新持灰,Android也在不斷迭代出新,lombok-ast慢慢跟不上發(fā)展负饲,所以Lint在25.2.0版增加了IntelliJ的PSI(Program Structure Interface)作為新的AST Parser堤魁。但是PSI于IntelliJ、于Lint也只是個過渡性方案返十,事實上IntelliJ早已開始了新一代AST Parser妥泉,UAST(Unified AST)的開發(fā),而Lint也將于即將發(fā)布的25.4.0版中將PSI更新為UAST洞坑。
3盲链、語法糖優(yōu)化,空安全
kotlin的空安全:
bob?.department?.head?.name
AST可以更簡潔的實現(xiàn)
bob.department.head.name
原理就是自動幫你加了空判斷
諸如此類,AST可以幫你實現(xiàn)更多類似于kotlin的語法糖刽沾,有了AST本慕,你不必再羨慕kotlin。
AST操作推薦庫:
推薦閱讀
annotation processing介紹
AST介紹
Lombok原理分析與功能實現(xiàn)
利用 Project Lombok 自定義 AST 轉(zhuǎn)換
https://www.ibm.com/developerworks/cn/java/j-lombok/?ca=drs-
Lombok自定義annotation擴展含Intellij插件 http://www.alliedjeep.com/128803.htm
lombok如何做的冗余代碼消除悠轩。https://blog.csdn.net/faicm/article/details/46772591
如何巧妙利用JSR269來重寫AST: https://my.oschina.net/superpdm/blog/129715
老司機趕緊進群開車: 555343041
例子比較簡單间狂,直接上源碼:
import com.google.auto.service.AutoService;
import com.sun.source.util.Trees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
/**
* Created by baixiaokang on 18/4/10.
*/
@AutoService(Processor.class)//自動生成 javax.annotation.processing.IProcessor 文件
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes("*")
public class ForceAssertions extends AbstractProcessor {
private int tally;
private Trees trees;
private TreeMaker make;
private Name.Table names;
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
trees = Trees.instance(env);
Context context = ((JavacProcessingEnvironment)
env).getContext();
make = TreeMaker.instance(context);
names = Names.instance(context).table;//Name.Table.instance(context);
tally = 0;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
Set<? extends Element> elements = roundEnv.getRootElements();
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
JCTree tree = (JCTree) trees.getTree(each);
TreeTranslator visitor = new Inliner();
tree.accept(visitor);
}
}
} else
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
tally + " assertions inlined.");
return false;
}
private class Inliner extends TreeTranslator {
@Override
public void visitAssert(JCTree.JCAssert tree) {
super.visitAssert(tree);
JCTree.JCStatement newNode = makeIfThrowException(tree);
result = newNode;
tally++;
}
private JCTree.JCStatement makeIfThrowException(JCTree.JCAssert node) {
// make: if (!(condition) throw new AssertionError(detail);
List<JCTree.JCExpression> args = node.getDetail() == null
? List.<JCTree.JCExpression>nil()
: List.of(node.detail);
JCTree.JCExpression expr = make.NewClass(
null,
null,
make.Ident(names.fromString("AssertionError")),
args,
null);
return make.If(
make.Unary(JCTree.Tag.NOT, node.cond),
make.Throw(expo),
null);
}
}
}