Modify Java 8 final behaviour with Annotations【翻譯】

文章來源于博客《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來生成gettersetter方法(這些方法并沒有在你的源文件中寫出),你可能會(huì)奇怪你的IDE是從哪里找到這些gettersetter方法的。

我們都知道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)

原文及翻譯略:-)


外部參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • 本文章涉及代碼已放到github上annotation-study 1.Annotation為何而來 What:A...
    zlcook閱讀 29,786評(píng)論 15 116
  • JDK各個(gè)版本的新特性 對(duì)于很多剛接觸java語言的學(xué)者來說,要了解一門語言,最好的方式是從基礎(chǔ)的版本進(jìn)行理解,升...
    小莊bb閱讀 1,210評(píng)論 0 1
  • 銷售前不準(zhǔn)備就準(zhǔn)備失敗,失敗的準(zhǔn)備也意味著失敗,我們在拜訪客戶前就要為前期引起客戶共鳴做準(zhǔn)備, 1公司資源,銷售政...
    自在獨(dú)行121閱讀 488評(píng)論 0 0
  • “天要亮了 我將要去拈花 你會(huì)微笑嗎” 歲月枯槁 在夢里 我仿佛擁抱著 你清瘦的靈魂 可是 我正囚禁于 你的眼眸 ...
    與愁予閱讀 507評(píng)論 2 6

友情鏈接更多精彩內(nèi)容