大家对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,我们可以在编译阶段自动生成一些重复的模板代码,以提高开发效率。