昨天無意中,逛到了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層代碼,用本文所述知識足可以打造一款適合自己的代碼生成器,節約時間,提高開發效率。
附贈