大家對Java中的註解(Annotation)應該都不陌生吧,JDK1.5就引進來了,它本質上只是一種元數據,和配置文件一樣。利用反射在運行時解析處理能夠實現各種靈活強大的功能,比如Spring就將其作用發揮得淋漓盡致。至於用法,這裏就不說了,我的其它文章裏面很多地方有用到過,可以參考一下。
一、運行時註解與編譯時註解
我們看到的大部分註解,它們都是在代碼運行時才使用的,所以一般定義成這樣
@Retention(RetentionPolicy.RUNTIME)
表示註解信息保留到代碼運行時階段。如果你足夠細心,一定也見過這種
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
它表示註解信息只保留在源文件,即Xxx.java,編譯之後就丟失了。所以,這類註解在編譯時就需要解析和處理了。
二、編譯時註解的使用方法
怎麼在編譯階段使用我們自定義的註解呢?很幸運,JDK1.6之後,Java提供了APT,即Annotation Processing Tool,基於SPI機制(什麼是SPI?與API又有什麼不同呢?之前文章已經詳細介紹,點這裏查看),讓我們可以擴展自己的編譯處理器。
APT的使用步驟
1、定義一個繼承javax.annotation.processing.AbstractProcessor的類,實現process方法,重寫其它方法或者使用指定註解配置一些其它信息。
1)實現process方法,這個是核心,處理邏輯都寫在這裏。public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv),其參數和返回值含義如下
- 參數annotations:請求處理的註釋類型,包含了該類處理的所有註解信息。
- 參數roundEnv:有關當前和以前round的環境信息,可以從這裏拿到可以處理的所有元素信息。
- 返回值:
true
——則這些註釋已聲明並且不要求後續Processor處理它們;false——則這些註釋未聲明並且可能要求後續 Processor處理它們。
2)配置該類可處理的註解,兩種方式可選
- 類上面使用註解,比如
@SupportedAnnotationTypes("cn.zhh.Getter")
public class GetterProcessor extends AbstractProcessor {
// ...
}
- 重寫getSupportedAnnotationTypes方法,比如
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(SuppressWarnings.class.getCanonicalName());
}
3)配置支持的源碼版本,兩種方式可選
- 類上面使用註解,比如
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
// ...
}
- 重寫getSupportedSourceVersion方法,比如
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
2、將處理器類註冊到Processor接口的實現裏面。
在源碼的根目錄下新建META-INF/services/文件夾,然後新建一個javax.annotation.processing.Processor文件,裏面寫上註解處理器的全類名,每個單獨一行。
這是SPI的常規操作:這樣子註冊後,Java就可以在運行時找到Processor接口的這個實現類了。
除了這種方式,Google也提供了更簡單的方式,即在處理器類上面加一個註解AutoService就可以了,我沒試過,大家可以瞭解一下。
3、在需要使用的項目引入,編譯時指定annotationProcessor。
一般上,註解處理器項目是獨立打成jar包或者作爲maven工程引入到具體需要使用的項目裏面的,因爲它不需要也不能再進行編譯(編譯它的時候你又指定了自己作爲註解處理器,會報找不到類的異常)。如果你非要放到同一個項目裏,然後分開編譯也是可以的。
- 1)javac命令可以指定註解處理器。
- 2)maven工程的compile插件可以指定註解處理器。
- 3)IDEA可以在配置裏面指定註解處理器。
三、APT的用途
市面上比較廣泛的用法目前有兩種
- querydsl(http://www.querydsl.com/):在Model上面加個註解,編譯時生成它們的查詢對象。比如MyBatis我們也可以在編譯時根據DO類生成Example、Mapper等文件。
- lombok(https://projectlombok.org/):在編譯時生成POJO的getter、setter、toString等方法。
第一個比較簡單,畢竟只是生成新的源文件。第二個就牛逼了,直接修改源文件編譯生成的語法樹,隱式增加新的代碼。
基於這兩個厲害的項目,我們來寫兩個小demo。
1、小試牛刀:編譯時日誌打印出帶有指定註解的Java類信息
1)新建一個maven工程apt-core1作爲註解處理器項目,pom文件添加compile插件並設置compilerArgument參數(避免編譯時使用自身的annotationProcessor)。
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<proc>none</proc><!--等同於:<compilerArgument>-proc:none</compilerArgument>-->
</configuration>
</plugin>
</plugins>
</build>
2)自定義一個用於處理SuppressWarnings註解的處理器。
至於爲什麼是@SuppressWarnings,其實沒什麼意思,隨便玩一下。
/**
* SuppressWarnings處理器
*
* @author Zhou Huanghua
* @date 2020/1/5 14:19
*/
public class SuppressWarningsProcessor extends AbstractProcessor {
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
roundEnv.getElementsAnnotatedWith(SuppressWarnings.class).forEach(element -> {
logger.info(String.format("element %s has been processed.", element.getSimpleName()));
});
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(SuppressWarnings.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
3)註冊到META-INF/services/下的javax.annotation.processing.Processor文件裏。
cn.zhh.SuppressWarningsProcessor
4)到此,註解處理器項目開發完成了,最後install一下到本地maven倉庫,工程完整結構如下
5)新建一個maven工程apt-demo1作爲實際使用的項目,pom文件添加compile插件並設置annotationProcessorPaths參數。
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>cn.zhh</groupId>
<artifactId>apt-core1</artifactId>
<version>1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
6)定義一個類,上面添加SuppressWarnings註解。
/**
* 學生類
*
* @author Zhou Huanghua
*/
@SuppressWarnings("unchecked")
public class Student implements Serializable {
}
7)最後,編譯工程apt-demo1,可以看到我們在註解處理器裏面編寫的打印日誌內容出來了。
2、大顯身手:編譯時爲類的屬性生成getter方法
1)新建一個maven工程apt-core2作爲註解處理器項目,pom文件添加compile插件並設置compilerArgument參數(避免編譯時使用自身的annotationProcessor)。此外,因爲編譯時需要修改語法樹,所以添加了sun的tools依賴(JDK有但不會默認引入)。
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<proc>none</proc><!--等同於:<compilerArgument>-proc:none</compilerArgument>-->
</configuration>
</plugin>
</plugins>
</build>
2)自定義一個用於類上面的編譯時註解Getter。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
}
3)自定義一個用於處理Getter註解的處理器。
代碼涉及比較底層的語法樹修改,這方面是難點...
/**
* Getter處理器
*
* @author Zhou Huanghua
* @date 2020/1/5 14:19
*/
@SupportedAnnotationTypes("cn.zhh.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public synchronized boolean process(Set<? extends TypeElement> annotationSet, RoundEnvironment roundEnv) {
// 處理帶Getter註解的元素
roundEnv.getElementsAnnotatedWith(Getter.class).forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
jcVariableDeclList.forEach(jcVariableDecl -> {
logger.info(String.format("%s has been processed.", jcVariableDecl.getName()));
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
}
/**
* 獲取新方法名,get + 將第一個字母大寫 + 後續部分, 例如 value 變爲 getValue
*
* @param name
* @return
*/
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
4)註冊到META-INF/services/下的javax.annotation.processing.Processor文件裏。
cn.zhh.GetterProcessor
5)到此,註解處理器項目開發完成了,最後install一下到本地maven倉庫,工程完整結構如下
6) 新建一個maven工程apt-demo2作爲實際使用的項目。因爲代碼裏面需要使用註解處理器項目的註解,所以將其依賴引進來。此外,增加compile插件並且設置annotationProcessors。(注意這裏與上個demo的差異)
<dependencies>
<dependency>
<groupId>cn.zhh</groupId>
<artifactId>apt-core2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessors>
<annotationProcessor>
cn.zhh.GetterProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
7)新建一個定義了id屬性的Student類,上面添加註解Getter。
/**
* 學生類
*
* @author Zhou Huanghua
*/
@Getter
public class Student implements Serializable {
/** id */
private Long id = 1L;
}
8)編譯apt-demo2項目,然後反編譯生成的Student.class文件,發現getId方法存在了。至此說明在編譯時增加屬性的getter方法做到了。
public class Student implements Serializable {
private Long id = 1L;
public Long getId() {
return this.id;
}
public Student() {
}
}
總結:利用APT,我們可以在編譯階段自動生成一些重複的模板代碼,以提高開發效率。