1. 程式人生 > >基於註解處理器開發自動生成getter和setter方法的外掛

基於註解處理器開發自動生成getter和setter方法的外掛

昨天無意中,逛到了lombok的網站,並看到了首頁的5分鐘視訊,視訊中的作者只是在實體類中寫了幾個欄位,就可以自動編譯為含setter、getter、toString()等方法的class檔案。看著挺新奇的,於是自己研究了一下原理,整理下發出來。

1.何處下手

視訊中作者的流程為:

(1)編寫Java檔案,在類上寫@Data註解

@Data
public class Demo {
    private String name;
    private double abc;
}

(2)javac編譯,lombok.jar是lombok的jar包。

javac -cp lombok.jar Demo.java

(3)javap檢視Demo.class類檔案

javap Demo

Demo.class:

public class Demo {
  public Demo();
  public java.lang.String getName();
  public void setName(java.lang.String);
  public double getAbc();
  public void setAbc(double);
}

可以看到Demo.class內部竟然多了很多未定義的setter、getter方法,而視訊作者主要使用的就是註解+編譯,那麼我們就從這方面入手。

2.必備知識

2.1 註解

註解,相信大部分人都用過,不少人還會自定義註解,並會利用反射等搞點小東西。但本文所講的並非是利用註解加反射在執行期自定義行為,而是在編譯期。

自定義註解離不開四大元註解。

@Retention:註解保留時期

保留型別 說明
SOURCE 只保留到原始碼中,編譯出來的class不存在
CLASS 保留到class檔案中,但是JVM不會載入
RUNTIME 一直存在,JVM會載入,可用反射獲取

@Target:用於標記可以應用於哪些型別上

元素型別 適用場合
ANOTATION_TYPE 註解型別宣告
PACKAGE
TYPE 類,列舉,介面,註解
METHOD 方法
CONSTRUCTOR 構造方法
FIELD 成員域,列舉常量
PARAMETER 方法或構造器引數
LOCAL_VARIABLE 區域性變數
TYPE_PARAMETER 型別引數
TYPE_USE 型別用法

@Documented:作用是能夠將註解中的元素包含到 Javadoc 中

@Inherited:繼承。假設註解A使用了此註解,那麼類B使用了註解A,類C繼承了類B,那麼類C也使用了註解A。(這裡的使用是為了區分易理解,實際為被註解

2.1 註解處理器

註解處理器就是 javac 包中專門用來處理註解的工具。所有的註解處理器都必須繼承抽象類AbstractProcessor然後重寫它的幾個方法。

註解處理器是執行在它自己的JVM中。javac 啟動一個完整Java虛擬機器來執行註解處理器,這意味著你可以使用任何你在其他java應用中使用的的東西。其中抽象方法process是必須要重寫的,再該方法中註解處理器可以遍歷所有的原始檔,然後通過RoundEnvironment類獲取我們需要處理的註解所標註的所有的元素,這裡的元素可以代表包,類,介面,方法,屬性等。再處理的過程中可以利用特定的工具類自動生成特定的.java檔案或者.class檔案,來幫助我們處理自定義註解。

一個普通的註解處理器檔案如下:

package com.example;

import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
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.TypeElement;

public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add("com.example.MyAnnotation");
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

}
  • init(ProcessingEnvironment processingEnv) :所有的註解處理器類都必須有一個無參建構函式。然而,有一個特殊的方法init(),它會被註解處理工具呼叫,以ProcessingEnvironment作為引數。ProcessingEnvironment 提供了一些實用的工具類Elements, TypesFiler
  • process(Set<? extends TypeElement> annoations, RoundEnvironment env) :這類似於每個處理器的main()方法。你可以在這個方法裡面編碼實現掃描,處理註解,生成 java 檔案。使用RoundEnvironment 引數,你可以查詢被特定註解標註的元素。
  • getSupportedAnnotationTypes():在這個方法裡面你必須指定哪些註解應該被註解處理器註冊。注意,它的返回值是一個String集合,包含了你的註解處理器想要處理的註解型別的全稱。換句話說,你在這裡定義你的註解處理器要處理哪些註解。
  • getSupportedSourceVersion() : 用來指定你使用的 java 版本。通常你應該返回SourceVersion.latestSupported() 。不過,如果你有足夠的理由堅持用 java 6 的話,你也可以返回SourceVersion.RELEASE_6

關於getSupportedAnnotationTypes()getSupportedSourceVersion()這兩個方法,你也可以使用相應註解進行代替。程式碼如下:

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
....

不過為了相容Java6,最好是過載這倆方法。

3.開始編碼

知識我們已經學會,現在開始實戰。

3.1 自定義註解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Data {

}

3.2 自定義註解處理器

public class DataAnnotationProcessor extends AbstractProcessor {
    private Messager messager; //用於列印日誌
    private Elements elementUtils; //用於處理元素
    private Filer filer;  //用來建立java檔案或者class檔案

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }
    
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> set = new HashSet<>();
        set.add(Data.class.getCanonicalName());
        return Collections.unmodifiableSet(set);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE,"-----開始自動生成原始碼");
        try {
            // 識別符號
            boolean isClass = false;
            // 類的全限定名
            String classAllName = null;
            // 返回被註釋的節點
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Data.class);
            Element element = null;
            for (Element e : elements) {
                // 如果註釋在類上
                if (e.getKind() == ElementKind.CLASS && e instanceof TypeElement) {
                    TypeElement t = (TypeElement) e;
                    isClass = true;
                    classAllName = t.getQualifiedName().toString();
                    element = t;
                    break;
                }
            }
            // 未在類上使用註釋則直接返回,返回false停止編譯
            if (!isClass) {
                return true;
            }
            // 返回類內的所有節點
            List<? extends Element> enclosedElements = element.getEnclosedElements();
            // 儲存欄位的集合
            Map<TypeMirror, Name> fieldMap = new HashMap<>();
            for (Element ele : enclosedElements) {
                if (ele.getKind() == ElementKind.FIELD) {
                    //欄位的型別
                    TypeMirror typeMirror = ele.asType();
                    //欄位的名稱
                    Name simpleName = ele.getSimpleName();
                    fieldMap.put(typeMirror, simpleName);
                }
            }
            // 生成一個Java原始檔
            JavaFileObject sourceFile = filer.createSourceFile(getClassName(classAllName));
            // 寫入程式碼
            createSourceFile(classAllName, fieldMap, sourceFile.openWriter());
            // 手動編譯
            compile(sourceFile.toUri().getPath());
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR,e.getMessage());
        }
        messager.printMessage(Diagnostic.Kind.NOTE,"-----完成自動生成原始碼");
        return true;
    }

    private void createSourceFile(String className, Map<TypeMirror, Name> fieldMap, Writer writer) throws IOException {
        // 生成原始碼
        JavaWriter jw = new JavaWriter(writer);
        jw.emitPackage(getPackage(className));
        jw.beginType(getClassName(className), "class", EnumSet.of(Modifier.PUBLIC));
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //欄位
            jw.emitField(type, name, EnumSet.of(Modifier.PRIVATE));
        }
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //getter
            jw.beginMethod(type, "get" + humpString(name), EnumSet.of(Modifier.PUBLIC))
                    .emitStatement("return " + name)
                    .endMethod();
            //setter
            jw.beginMethod("void", "set" + humpString(name), EnumSet.of(Modifier.PUBLIC), type, "arg")
                    .emitStatement("this." + name + " = arg")
                    .endMethod();
        }
        jw.endType().close();
    }

    /**
     * 編譯檔案
     * @param path
     * @throws IOException
     */
    private void compile(String path) throws IOException {
        //拿到編譯器
        JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
        //檔案管理者
        StandardJavaFileManager fileMgr =
                complier.getStandardFileManager(null, null, null);
        //獲取檔案
        Iterable units = fileMgr.getJavaFileObjects(path);
        //編譯任務
        JavaCompiler.CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
        //進行編譯
        t.call();
        fileMgr.close();
    }

    /**
     * 駝峰命名
     *
     * @param name
     * @return
     */
    private String humpString(String name) {
        String result = name;
        if (name.length() == 1) {
            result = name.toUpperCase();
        }
        if (name.length() > 1) {
            result = name.substring(0, 1).toUpperCase() + name.substring(1);
        }
        return result;
    }

    /**
     * 讀取類名
     * @param name
     * @return
     */
    private String getClassName(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(name.lastIndexOf(".") + 1);
        }
        return result;
    }

    /**
     * 讀取包名
     * @param name
     * @return
     */
    private String getPackage(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(0, name.lastIndexOf("."));
        }else {
            result = "";
        }
        return result;
    }
}

在自定義註解處理器中,註釋非常詳細的說明了每一步的思路,首先是讀取被註釋的節點,判斷是否是類節點,然後生成Java原始檔,並使用javawriter框架寫入Java程式碼,最後手動編譯該java原始檔。

javawriter框架引用如下:

compile 'com.squareup:javawriter:2.5.1'

3.3 註解處理器的註冊

編碼結束後,還需要把註解處理器註冊到javac編譯器,所以需要提供一個 .jar 檔案。就像其他 .jar 檔案一樣,你將你已經編譯好的註解處理器打包到此檔案中。並且,在你的 .jar 檔案中,你必須打包一個特殊的檔案javax.annotation.processing.Processor到META-INF/services目錄下。因此你的 .jar 檔案目錄結構看起來就你這樣:

MyProcess.jar
    -com
        -example
            -MyProcess.class
    -META-INF
        -services
            -javax.annotation.processing.Processor

javax.annotation.processing.Processor 檔案的內容是一個列表,每一行是一個註解處理器的全稱。例如:

com.example.MyProcess

在IDE中,只需在resources目錄下新建META-INF/services/javax.annotation.processing.Processor檔案即可。

其它註冊方式

前面的註冊方式很底層,個人推薦使用。當處理的註解處理器過多時,這種方式不免過於繁瑣,所以另一種方式就是使用自動註冊註解處理器的框架。

新增對谷歌自動註冊註解庫的引用

implementation ‘com.google.auto.service:auto-service:1.0-rc4’

在註解處理器類前面宣告

@AutoService(Processor.class)

4.打包使用

此時我們把專案打包為jar包即可使用,下面演示下使用過程。

(1)寫個Demo.java

import cn.zyzpp.annotation.Data;

@Data
public class Demo {
    private String name;
    private double abc;
}

(2)編譯java檔案,在該Demo.java資料夾下開啟控制檯視窗,記得把打包的jar包一起放在此目錄。

javac -cp annotation-1.0-SNAPSHOT.jar Demo.java

(3)使用javap檢視編譯後的Demo.class

Compiled from "Demo.java"
public class Demo {
  public Demo();
  public double getAbc();
  public void setAbc(double);
  public java.lang.String getName();
  public void setName(java.lang.String);
}

再看此時的Demo.java程式碼

public class Demo {
  private double abc;
  private String name;
  public double getAbc() {
    return abc;
  }
  public void setAbc(double arg) {
    this.abc = arg;
  }
  public String getName() {
    return name;
  }
  public void setName(String arg) {
    this.name = arg;
  }
}

到此,我們正式開發出了自動生成getter、setter方法的外掛。有的人可能覺得這個也很沒多大用,用IDE快捷鍵就能辦到,對此我只能說你的舉一反三能力需要提高。知識已經教會你了,能做出什麼多姿多彩的框架就靠小夥伴們的智慧了。比如,我們一般都會新建Entity類,然後基於此新建Dao層,Service層程式碼,用本文鎖所述知識足可以打造一款適合自己的程式碼生成器,節約時間,提高開發效率。

附贈

IntelliJ IDEA lombok外掛的安裝和使用