文章來源于博客《Modify Java 8 final behaviour with Annotations》,作者Johan Kragt,我覺得寫的很好,所以嘗試翻譯。
本文主旨是使用Java的Annotation來動(dòng)態(tài)修改AST。
為什么我要寫這個(gè)項(xiàng)目(How I started this project)
不久前我去參加了一場由JDriven的Ties van de Ven 舉辦的名為“致命的Null(Death To Null)”的會(huì)議。會(huì)議內(nèi)容并沒過多涉及到null檢查和NullPointerExceptions異常,但是更關(guān)注于不變性(Immutability )以及如何讓你的軟件避免bug和NullPointerExceptions異常。有一句話讓我深思(大致意思是):“Java選了錯(cuò)誤的默認(rèn)方式。Java中所有的東西應(yīng)該被默認(rèn)為final,除非開發(fā)者顯式地注明它是可變的(mutable),而不是在只有當(dāng)我們需要 不可變變量(immutable variable)的才顯式標(biāo)注為final”。
在對(duì)注解(Annotation)了解不多的時(shí)候,我以為利用注解就可以很輕松實(shí)現(xiàn)它。但是我遇到的第一個(gè)困難就是注解不能修改程序源代碼,或者從源代碼編譯出的字節(jié)碼文件;它們只能產(chǎn)生新的源文件或者驗(yàn)證代碼。這使我迅速注意到了名為Lombok的庫,該庫卻實(shí)現(xiàn)了這個(gè)功能。如果你使用過Lombok來生成getter和setter方法(這些方法并沒有在你的源文件中寫出),你可能會(huì)奇怪你的IDE是從哪里找到這些getter和setter方法的。
我們都知道Java的編譯是一個(gè)遞歸過程。首先編譯器會(huì)將你的源代碼解析為一個(gè)AST,即抽象語法樹。然后注解處理器(annotation processors)將會(huì)開始運(yùn)行,它會(huì)基于你的注解來生成額外的代碼文件或者做一些額外的驗(yàn)證。編譯錯(cuò)誤,比如不合法的類或者不合法的方法調(diào)用,會(huì)在編譯的第三階段進(jìn)行。然而,如果一個(gè)注解處理器生成了新的源文件,那么編譯器將會(huì)在進(jìn)行第三步之前重復(fù)執(zhí)行第一步和第二步。
Lombok庫,以及我的驗(yàn)證性測試(proof of concept),將會(huì)用到一點(diǎn)hack技巧。在這里,并不需要利用注解處理器來生成新的文件,而是直接修改AST。這將可以讓處理器在不接觸源代碼或者字節(jié)碼的情況下改變代碼。
一個(gè)更詳細(xì)的解釋在這里。雖然它有點(diǎn)過時(shí),但還是能給你點(diǎn)幫助。
構(gòu)建一個(gè)驗(yàn)證性測試(Building a PoC)
該驗(yàn)證性測試將會(huì)創(chuàng)建一個(gè)新的maven項(xiàng)目。它有兩個(gè)子項(xiàng)目,其中一個(gè)包含自定義的注解以及注解處理器,另一個(gè)則包含一個(gè)測試類,它使用了這些注解。
我將創(chuàng)建兩個(gè)注解,@FinalizeVars是一個(gè)類型注解,它告訴處理器,將被該注解標(biāo)明的類中的所有變量都變?yōu)?code>final。@MutableVar則是一個(gè)字段和局部變量的注解,表示處理器在處理final時(shí)跳過這些量。
雖然可以給@MutableVar添加一些額外功能,比如注解了@MutableVar的變量將會(huì)被移除final限定,但是對(duì)于像@MutableVar final String t = “test”這樣的語句這么做顯然沒有什么意義。
代碼實(shí)現(xiàn)(The code)
注解(The annotations)
這些注解是非常簡單且標(biāo)準(zhǔn)的。
package nl.johannisk.finalizer.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface FinalizeVars {
}
FinalizeVars是類型注解(Types)。
下面這個(gè)注解對(duì)于實(shí)現(xiàn)此功能(即將類中的字段都變?yōu)?code>final)來說并不是完全必要的,但是我希望在程序員明確指出要將某些變量保留為可變時(shí),注解處理器也能夠處理(原句:“This Annotation is not entirely necessary for the functionality to work, but I didn’t want the annotation processor to change the functionality of the Java Compiler without the programmer explicitly telling it to do something strange.”):
package nl.johannisk.finalizer.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD})
public @interface MutableVar {
}
MutableVar是一個(gè)局部變量和字段的注解。如果局部變量和字段具有該注解,則注解處理器將會(huì)跳過這些字段。
注解處理器(The Processor)
在定義注解的那個(gè)項(xiàng)目里,按照如下路徑建立一個(gè)文本文件:resources/META-INF/services/javax.annotation.processing.Processor就可以讓編譯器使用注解處理器工作。(譯注:如果是在命令行中使用javac命令,則可以使用-processor參數(shù)后跟處理器的字節(jié)碼文件來指定處理器,如果沒有該參數(shù)命令,則有可能javac的版本過低。)
這個(gè)文本文件里必須寫明帶有完整限定的處理器類名,如nl.johannisk.finalizer.processor.FinalizerProcessor
實(shí)際的處理器具有兩個(gè)類。一個(gè)是處理器本身,還有一個(gè)是“訪問者”(或者稱為”轉(zhuǎn)換器”),用來遍歷和轉(zhuǎn)換AST中的每一個(gè)根元素。
處理器定義如下:
package nl.johannisk.finalizer.processor;
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 javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"nl.johannisk.finalizer.annotation.FinalizeVars",
"nl.johannisk.finalizer.annotation.MutableVar"})
public class FinalizerProcessor extends AbstractProcessor {
private Trees trees;
private TreeMaker treeMaker;
@Override
public void init(final ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
final JavacProcessingEnvironment javacProcessingEnvironment =
(JavacProcessingEnvironment) processingEnvironment;
this.trees = Trees.instance(processingEnvironment);
this.treeMaker =
TreeMaker.instance(javacProcessingEnvironment.getContext());
}
@Override
public boolean process(final Set<? extends TypeElement> annotations,
final RoundEnvironment roundEnvironment) {
if (!roundEnvironment.processingOver()) {
processRootElements(roundEnvironment.getRootElements());
}
return false;
}
private void processRootElements(final Set<? extends Element> rootElements) {
rootElements.forEach(this::processRootElement);
}
private void processRootElement(final Element element) {
JCTree tree = (JCTree) trees.getTree(element);
tree.accept(new FinalizerTranslator(treeMaker));
}
}
處理器需要繼承AbstractProcessor類,并且起碼要重寫process方法。
因?yàn)檫€需要用到AST樹,所以我還重寫了init方法。該方法先調(diào)用父類的init方法,然后將treeMaker初始化為訪問器。同時(shí)還將AST樹保存在tree字段中,以便process使用。(譯注:com.sun.tools.javac.processingJavacProcessingEnvironment、com.sun.tools.javac.treeTreeMaker是在%JAVA_HOME%/lib/tools.jar中定義的,請(qǐng)導(dǎo)入該包。com.sun.source.util.Trees則在標(biāo)準(zhǔn)庫中定義。Trees用于解析Java AST樹,而TreeMaker: 用于生成Java AST節(jié)點(diǎn)。)
process方法首先檢查是否已經(jīng)處理完畢,如果沒有(即第一次運(yùn)行)則獲得所有根元素并且用訪問者(visitor)訪問。訪問者會(huì)判斷當(dāng)前訪問的根元素是否需要被處理,然后一個(gè)新的訪問者(FinalizerTranslator) 將會(huì)被創(chuàng)建。
package nl.johannisk.finalizer.processor;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.tree.JCTree;
public class FinalizerTranslator extends TreeTranslator {
private final TreeMaker treeMaker;
private boolean shouldVisitVarDefinitions = false;
public FinalizerTranslator(final TreeMaker treeMaker) {
this.treeMaker = treeMaker;
}
@Override
public void visitClassDef(final JCTree.JCClassDecl classDeclaration) {
if (isFinalizeVarsAnnotation(classDeclaration.getModifiers())) {
shouldVisitVarDefinitions = true;
}
super.visitClassDef(classDeclaration);
}
@Override
public void visitVarDef(final JCTree.JCVariableDecl variableDeclaration) {
super.visitVarDef(variableDeclaration);
JCTree.JCModifiers modifiers = variableDeclaration.getModifiers();
if (shouldBeMadeFinal(variableDeclaration, modifiers)) {
variableDeclaration.mods = treeMaker.Modifiers(variableDeclaration.mods.flags | Flags.FINAL);
}
this.result = variableDeclaration;
}
private boolean shouldBeMadeFinal(final JCTree.JCVariableDecl variableDeclaration, final JCTree.JCModifiers modifiers) {
return !isMutableVarAnnotation(modifiers) &&
!isVolatile(variableDeclaration.getModifiers()) &&
shouldVisitVarDefinitions;
}
private boolean isMutableVarAnnotation(final JCTree.JCModifiers modifiers) {
return modifiers.toString().contains("@MutableVar()");
}
private boolean isFinalizeVarsAnnotation(final JCTree.JCModifiers modifiers) {
return modifiers.toString().contains("@FinalizeVars()");
}
private boolean isVolatile(final JCTree.JCModifiers modifiers) {
return (modifiers.flags & Flags.VOLATILE) > 0;
}
}
FinalizerTranslator即為訪問者。因?yàn)樽⒔鈱?huì)被用于類和變量聲明,所以對(duì)于這些元素的visit方法將被重寫。
首先,visitClassDef將會(huì)訪問類的定義內(nèi)容,因?yàn)檫@些注解被用于類。如果一個(gè)類被FinalizeVars注解,那么visitVarDefinition將會(huì)被設(shè)為true。否則,(如果一個(gè)類沒有注解FinalizeVars)該類中的變量聲明將不會(huì)被后續(xù)處理。
接下來,visitVarDef會(huì)訪問所有變量定義。注意這將會(huì)訪問到類中所有的變量定義,而不僅僅是被注解的那些。
如果(變量)被注解(@MutableVar)、且沒有被volatile修飾(因?yàn)樗cfinal不兼容),同時(shí)(這些變量所在的)類被@FinalizeVars注解,那么treeMaker將基于原來(變量的)修飾符創(chuàng)建一個(gè)新的修飾符,將final修飾符加入。
調(diào)試(Debugging)
原文及翻譯略:-)