基於註解處理器開發自動生成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
,Types
和Filer
。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層程式碼,用本文鎖所述知識足可以打造一款適合自己的程式碼生成器,節約時間,提高開發效率。
附贈