天天看點

實戰:javac插入式注解處理器

一套程式設計語言中編譯子系統的優劣,很大程度上決定了程式運作性能的好壞和編碼效率的高低,尤其在Java語言中,運作期即時編譯與虛拟機執行子系統非常緊密地互相依賴、配合運作。了解JDK如何編譯和優化代碼,有助于我們寫出适合JDK自優化的程式。看過javac源碼,我們就知道,當我們的編譯器在把java檔案編譯為位元組碼的時候,會對java源程式做各方面的校驗,在本文的實戰中,我們将會使用注解處理器API來編寫一款擁有自己編碼風格的校驗工具,為Javac編譯器添加一個額外的功能,在編譯程式時檢查程式名是否符合上述對類(或接口)、方法、字段的命名要求。

前提

Java程式命名應當符合下列格式的書寫規範。

  • 類(或接口):符合駝式命名法,首字母大寫。
  • 方法:符合駝式命名法,首字母小寫。
  • 字段: 類或執行個體變量:符合駝式命名法,首字母小寫。常量:要求全部由大寫字母或下劃線構成,并且第一個字元不能是下劃線。

代碼實作

我們實作注解處理器的代碼需要繼承抽象類javax.annotation.processing.AbstractProcessor,這個抽象類中隻有一個必須覆寫的abstract方法:“process()”,它是Javac編譯器在執行注解處理器代碼時要調用的過程,我們可以從這個方法的第一個參數“annotations”中擷取到此注解處理器所要處理的注解集合,從第二個參數“roundEnv”中通路到目前這個Round中的文法樹節點,每個文法樹節點在這裡表示為一個Element。

注解處理器除了process()方法及其參數之外,還有兩個可以配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了這個注解處理器對哪些注解感興趣,可以使用星号“*”作為通配符代表對所有的注解都感興趣,後者指出這個注解處理器可以處理哪些版本的Java代碼。

每一個注解處理器在運作的時候都是單例的,如果不需要改變或生成文法樹的内容,process()方法就可以傳回一個值為false的布爾值,通知編譯器這個Round中的代碼未發生變化,無須構造新的JavaCompiler執行個體,在這次實戰的注解處理器中隻對程式命名進行檢查,不需要改變文法樹的内容,是以process()方法的傳回值都是false。

注解處理器NameCheckProcessor.java

package cn.tf.jvm.part10;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

// 可以用"*"表示支援所有Annotations
@SupportedAnnotationTypes("*")
// 隻支援JDK 1.8的Java代碼
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {

private NameChecker nameChecker;

/**
 * 初始化名稱檢查插件
 */
@Override
public void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    nameChecker = new NameChecker(processingEnv);
}

/**
 * 對輸入的文法樹的各個節點進行進行名稱檢查
 */
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (!roundEnv.processingOver()) {
        for (Element element : roundEnv.getRootElements())
            nameChecker.checkNames(element);
        }
        return false;
    }
}

           

從上面代碼可以看出,NameCheckProcessor能處理基于JDK 1.8的源碼,它不限于特定的注解,對任何代碼都“感興趣”,而在process()方法中是把目前Round中的每一個RootElement傳遞到一個名為NameChecker的檢查器中執行名稱檢查邏輯。

然後來看NameChecker.java,它通過一個繼承于javax.lang.model.util.ElementScanner6的NameCheckScanner類,以Visitor模式來完成對文法樹的周遊,分别執行visitType()、visitVariable()和visitExecutable()方法來通路類、字段和方法,這3個visit方法對各自的命名規則做相應的檢查,checkCamelCase()與checkAllCaps()方法則用于實作駝式命名法和全大寫命名規則的檢查。

package cn.tf.jvm.part10;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementScanner6;
import javax.lang.model.util.ElementScanner8;

import java.util.EnumSet;

import static javax.lang.model.element.ElementKind.*;
import static javax.lang.model.element.Modifier.*;
import static javax.tools.Diagnostic.Kind.WARNING;

/**
 * 程式名稱規範的編譯器插件:<br>
 * 如果程式命名不合規範,将會輸出一個編譯器的WARNING資訊
 */
public class NameChecker {
private final Messager messager;

NameCheckScanner nameCheckScanner = new NameCheckScanner();

NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}

/**
 * 對Java程式命名進行檢查,根據《Java語言規範》第三版第6.8節的要求,Java程式命名應當符合下列格式:
 *
 * <ul>
 * <li>類或接口:符合駝式命名法,首字母大寫。
 * <li>方法:符合駝式命名法,首字母小寫。
 * <li>字段:
 * <ul>
 * <li>類、執行個體變量: 符合駝式命名法,首字母小寫。
 * <li>常量: 要求全部大寫。
 * </ul>
 * </ul>
 */
public void checkNames(Element element) {
     nameCheckScanner.scan(element);
}

/**
 * 名稱檢查器實作類,繼承了JDK 1.6中新提供的ElementScanner6<br>
 * 将會以Visitor模式通路抽象文法樹中的元素
 */
private class NameCheckScanner extends ElementScanner8<Void, Void> {

/**
 * 此方法用于檢查Java類
 */
@Override
public Void visitType(TypeElement e, Void p) {
    scan(e.getTypeParameters(), p);
    checkCamelCase(e, true);
    super.visitType(e, p);
    return null;
}

/**
 * 檢查方法命名是否合法
 */
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
    if (e.getKind() == METHOD) {
        Name name = e.getSimpleName();
    if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
        messager.printMessage(WARNING, "一個普通方法 “" + name + "”不應當與類名重複,避免與構造函數産生混淆", e);
        checkCamelCase(e, false);
    }
    super.visitExecutable(e, p);
    return null;
}

/**
 * 檢查變量命名是否合法
 */
@Override
public Void visitVariable(VariableElement e, Void p) {
    // 如果這個Variable是枚舉或常量,則按大寫命名檢查,否則按照駝式命名法規則檢查
    if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
        checkAllCaps(e);
    else
        checkCamelCase(e, false);
    return null;
}

/**
 * 判斷一個變量是否是常量
 */
private boolean heuristicallyConstant(VariableElement e) {
    if (e.getEnclosingElement().getKind() == INTERFACE)
        return true;
    else if (e.getKind() == FIELD && e.getModifiers().containsAll(EnumSet.of(PUBLIC, STATIC, FINAL)))
        return true;
    else {
        return false;
    }
}

/**
 * 檢查傳入的Element是否符合駝式命名法,如果不符合,則輸出警告資訊
 */
private void checkCamelCase(Element e, boolean initialCaps) {
    String name = e.getSimpleName().toString();
    boolean previousUpper = false;
    boolean conventional = true;
    int firstCodePoint = name.codePointAt(0);
    
    if (Character.isUpperCase(firstCodePoint)) {
        previousUpper = true;
        if (!initialCaps) {
            messager.printMessage(WARNING, "名稱“" + name + "”應當以小寫字母開頭", e);
            return;
        }
    } else if (Character.isLowerCase(firstCodePoint)) {
        if (initialCaps) {
            messager.printMessage(WARNING, "名稱“" + name + "”應當以大寫字母開頭", e);
            return;
        }
    } else
        conventional = false;
    
    if (conventional) {
    int cp = firstCodePoint;
    for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
    cp = name.codePointAt(i);
    if (Character.isUpperCase(cp)) {
        if (previousUpper) {
            conventional = false;
            break;
        }
        previousUpper = true;
    } else
        previousUpper = false;
    }
}

if (!conventional)
    messager.printMessage(WARNING, "名稱“" + name + "”應當符合駝式命名法(Camel Case Names)", e);
}

/**
 * 大寫命名檢查,要求第一個字母必須是大寫的英文字母,其餘部分可以是下劃線或大寫字母
 */
private void checkAllCaps(Element e) {
    String name = e.getSimpleName().toString();
    
    boolean conventional = true;
    int firstCodePoint = name.codePointAt(0);
    
    if (!Character.isUpperCase(firstCodePoint))
           conventional = false;
    else {
    boolean previousUnderscore = false;
    int cp = firstCodePoint;
    for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
    cp = name.codePointAt(i);
    if (cp == (int) '_') {
        if (previousUnderscore) {
            conventional = false;
            break;
        }
        previousUnderscore = true;
    } else {
        previousUnderscore = false;
    if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
        conventional = false;
        break;
    }
    }
  }
}

if (!conventional)
    messager.printMessage(WARNING, "常量“" + name + "”應當全部以大寫字母或下劃線命名,并且以字母開頭", e);
}
}
}
           

在BADLY_NAMED_CODE.java中包含多個不規範的命名,我們需要用到前面的兩個類來校驗下面這個檔案是否符合要求。

package cn.tf.jvm.part10;

    public class BADLY_NAMED_CODE {
        enum colors {
        red, blue, green;
    }

    static final int _FORTY_TWO = 66;

    public static int NOT_A_CONSTANT = _FORTY_TWO;

    protected void BADLY_NAMED_CODE() {
        return;
    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
    }
           

運作和測試

我們可以通過Javac指令的“-processor”參數來執行編譯時需要附帶的注解處理器,在相應的工程下src/java/mian目錄下執行以下指令編譯

javac -encoding UTF-8 cn/tf/jvm/part10/NameChecker.java
 javac -encoding UTF-8 cn/tf/jvm/part10/NameCheckProcessor.java
           

最後使用編譯好的檔案進行使用

javac -processor cn.tf.jvm.part10.NameCheckProcessor cn/tf/jvm/part10/BADLY_NAMED_CODE.java
           

執行結果如下:

cn\tf\jvm\part10\BADLY_NAMED_CODE.java:3: 警告: 名稱“BADLY_NAMED_CODE”應當符合駝式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:4: 警告: 名稱“colors”應當以大寫字母開頭
enum colors {
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“red”應當全部以大寫字母或下劃線命名,并且以字母開頭
red, blue, green;
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“blue”應當全部以大寫字母或下劃線命名,并且以字母開頭
red, blue, green;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“green”應當全部以大寫字母或下劃線命名,并且以字母開頭
red, blue, green;
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:8: 警告: 常量“_FORTY_TWO”應當全部以大寫字母或下劃線命名,并且以字母開頭
static final int _FORTY_TWO = 66;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:10: 警告: 名稱“NOT_A_CONSTANT”應當以小寫字母開頭
public static int NOT_A_CONSTANT = _FORTY_TWO;
  ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 一個普通方法 “BADLY_NAMED_CODE”不應當與類名重複,避免與構造函數産生混淆
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 名稱“BADLY_NAMED_CODE”應當以小寫字母開頭
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:16: 警告: 名稱“NOTcamelCASEmethodNAME”應當以小寫字母開頭
public void NOTcamelCASEmethodNAME() {
^
10 個警告
           

擴充

Javac編譯動作的入口是com.sun.tools.javac.main.JavaCompiler類,javac編譯入口,重要類源碼如下:

public void compile(List<JavaFileObject> sourceFileObjects,
                    List<String> classnames,
                    Iterable<? extends Processor> processors)
    throws IOException // TODO: temp, from JavacProcessingEnvironment
{
    if (processors != null && processors.iterator().hasNext())
        explicitAnnotationProcessingRequested = true;
    // as a JavaCompiler can only be used once, throw an exception if
    // it has been used before.
    if (hasBeenUsed)
        throw new AssertionError("attempt to reuse JavaCompiler");
    hasBeenUsed = true;

    start_msec = now();
    try {
        /**
         * 插入注解處理
         */
        initProcessAnnotations(processors);

        /**
         * 詞法分析、文法分析
         * parseFiles(sourceFileObjects) 分析源碼。擷取文法樹JCCompilationUnit 集合
         * 
         * 填充符号表
         * enterTrees() 抽象文法樹的頂局節點都先被放到待處理清單中并逐個處理清單中的節點。
         * 所有的類符号被輸入到外圍作用域的符号表中确定類的參數(對泛型類型而言)、超類型和接口
         * 如果需要添加預設構造器,将類中出現的符号輸入到類自身的符号表中。
         */
        // These method calls must be chained to avoid memory leaks
        delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);
        
        /**
         * 語義分析
         */
        delegateCompiler.compile2();
        delegateCompiler.close();
        elapsed_msec = delegateCompiler.elapsed_msec;
    } catch (Abort ex) {
        if (devVerbose)
            ex.printStackTrace();
    } finally {
        if (procEnvImpl != null)
            procEnvImpl.close();
    }
}
           

總結:NameCheckProcessor的實戰例子隻示範了JSR-269嵌入式注解處理器API中的一部分功能,基于這組API支援的項目還有用于校驗Hibernate标簽使用正确性的本質上與NameCheckProcessor所做的事情差不多)、自動為字段生成getter和setter方法的Lombok等。

參考資料:《深入了解Java虛拟機2》

繼續閱讀