编译时注解可以用来动态生成代码. 使用 SOURCE 类型注解的代码会在编译时被解析, 生成新的 java 文件, 然后和原来的 java 文件一起编译成字节码. 由于不使用反射功能, 编译时注解不会拖累性能, 因而被许多框架使用, 比如 Butter Knife, Dragger2 等.
一些基本概念
在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解。
编译时注解跟运行时注解到底区别在什么地方?其实说大也不大,主要是考虑到性能上面的问题。运行时注解主要是完全依赖于反射,反射的效率比原生的慢,所以在内存比较少,CPU比较烂的机器上会有一些卡顿现象出现。而编译时注解完全不会有这个问题,因为它在我们编译过程(java->class)中,通过一些注解标示,去动态生成一些类或者文件,所以跟我们的APK运行完全没有任何关系,自然就不存在性能上的问题。所以一般比较著名的开源项目如果采用注解功能,通常采用编译时注解
注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。这里,我假设你已经了解什么是注解及如何自定义注解。如果你还未了解注解的话,可以查看官方文档。注解处理器在 Java 5 的时候就已经存在了,但直到 Java 6 (发布于2006看十二月)的时候才有可用的API。过了一段时间java的使用者们才意识到注解处理器的强大。所以最近几年它才开始流行。
一个特定注解的处理器以 java 源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java文件)作为输出。那意味着什么呢?你可以生成 java 代码!这些 java 代码在生成的.java文件中。因此你不能改变已经存在的java类,例如添加一个方法。这些生成的 java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。
Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。
实现一个简单的lombok
实现一个简单的lombok主要分为两个步骤,第一是实现Processor接口处理注解,第二是注册注解处理器。
1.创建我们自定义的注解
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MySetterGetter {
}
2.实现Processor接口
通过实现Processor接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor类实现自定义注解处理器。实现抽象方法process处理我们想要的功能。
首先pom文件先引入依赖(jdk文件夹的tools.jar工具类),实现注解时会使用到tools工具类里面的一些方法
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>D:/tool/jdk1.8.0_111/lib/tools.jar</systemPath>
</dependency>
实现接口,需要先引入tools.jar工具包。
@SupportedAnnotationTypes(value = {"com.example.zcs.annotation.lombokTest.MySetterGetter"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CustomProcessor extends AbstractProcessor {
/**
* AST
*/
private JavacTrees trees;
/**
* 操作修改AST
*/
private TreeMaker treeMaker;
/**
* 符号封装类,处理名称
*/
private Names names;
/**
* 打印信息
*/
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
trees = JavacTrees.instance(processingEnv);
final Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
treeMaker = TreeMaker.instance(context);
names = Names.instance(context);
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("begin");
System.out.println("start annotation process .999999999999999999999999");
final Set<? extends Element> dataAnnotations = roundEnv.getElementsAnnotatedWith(com.example.zcs.annotation.lombokTest.MySetterGetter.class);
dataAnnotations.stream().map(element -> trees.getTree(element)).forEach(
tree -> tree.accept(new TreeTranslator() {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
// print method name
System.out.println("-------------2");
messager.printMessage(Diagnostic.Kind.NOTE, jcMethodDecl.toString());
super.visitMethodDef(jcMethodDecl);
}
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
System.out.println("-------------1");
final Map<Name, JCTree.JCVariableDecl> treeMap =
jcClassDecl.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
.map(tree -> (JCTree.JCVariableDecl) tree)
.collect(Collectors.toMap(JCTree.JCVariableDecl::getName, Function.identity()));
treeMap.forEach((k, var) -> {
messager.printMessage(Diagnostic.Kind.NOTE, "var:" + k);
System.out.println("-------------3");
try {
// add getter
jcClassDecl.defs = jcClassDecl.defs.prepend(getter(var));
// add setter
jcClassDecl.defs = jcClassDecl.defs.prepend(setter(var));
// jcClassDecl.defs.prepend(setter(var));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
});
super.visitClassDef(jcClassDecl);
}
})
);
return true;
}
/**
* 自定义setter
*/
private JCTree setter(JCTree.JCVariableDecl var) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
// 方法级别public
final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
final Name varName = var.getName();
Name methodName = methodName(varName, "set");
// 方法体
ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
jcStatements.append(treeMaker.Exec(treeMaker.Assign(
treeMaker.Select(treeMaker.Ident(names.fromString("this")), varName),
treeMaker.Ident(varName)
)));
final JCTree.JCBlock block = treeMaker.Block(0, jcStatements.toList());
// 返回值类型void
JCTree.JCExpression returnType =
treeMaker.Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));
List<JCTree.JCTypeParameter> typeParameters = List.nil();
// 参数
final JCTree.JCVariableDecl paramVars = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER,
List.nil()), var.name, var.vartype, null);
final List<JCTree.JCVariableDecl> params = List.of(paramVars);
List<JCTree.JCExpression> throwClauses = List.nil();
// 重新构造一个方法, 最后一个参数是方法注解的默认值,这里没有
return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
null);
}
/**
* 构造驼峰命名
*/
private Name methodName(Name varName, String prefix) {
return names.fromString(prefix + varName.toString().substring(0, 1).toUpperCase()
+ varName.toString().substring(1));
}
/**
* 构造getter
*/
private JCTree getter(JCTree.JCVariableDecl var) {
// 方法级别
final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
// 方法名称
final Name methodName = methodName(var.getName(), "get");
// 方法内容
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), var.getName())));
final JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
// 返回值类型
final JCTree.JCExpression returnType = var.vartype;
// 没有参数类型
List<JCTree.JCTypeParameter> typeParameters = List.nil();
// 没有参数变量
List<JCTree.JCVariableDecl> params = List.nil();
// 没有异常
List<JCTree.JCExpression> throwClauses = List.nil();
// 构造getter
return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
null);
}
}
3.注册注解处理器
方式一:
我们还需要将我们自定义的注解处理器进行注册。新建res文件夹,目录下新建META-INF文件夹,目录下新建services文件夹,目录下新建javax.annotation.processing.Processor文件,然后将我们自定义注解处理器的全类名写到此文件:
方式二:
上面这种注册的方式有点麻烦,谷歌帮我们写了一个注解处理器来生成这个文件,通过引入依赖的方法。
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<version>0.10</version>
<optional>true</optional>
</dependency>
然后添加注解(这种使用注解进行注册的方式我没有使用过,有兴趣的同学可以使用)
@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
...
4.在pom文件里还要做一个编译插件的配置,不然会编译失败。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<compilerArgument>
-proc:none
</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
没有这个配置编译时会报错:提示服务配置文件不正确, 或构造处理程序对象j
几个配置参数的意思:
-proc:{none,only}、-procpath、-processor
这三个命令是用来自定义“Annotation Processor”的,即你可以自定义注释,比如@Hello,@First 等,解析这些注释就需要"Annotation Processor"。
- -processor <CustomProcessor> :自定义注释处理器的类
- -procpath:注释处理器的查找目录。
- -proc:only:只运行注释处理器,而不编译源文件。
- -proc:none:不使用注释处理器,只编译源文件。
5.代码写完,可以进行编译了,打成jar包引入到别的项目就可以使用了。
使用mvn clean package 命令打了jar包。
将jar包重命名,并放到一个临时目录,方便引入
6.在别的项目中引入jar包,并对注解进行使用。
首先在pom文件引入jar包,注意jar包目录要改成自己的
<dependency>
<groupId>com.zcs</groupId>
<artifactId>lombok</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>C:/Users/zhangchangsi/Desktop/lombok/zcs-lombok.jar</systemPath>
</dependency>
使用我们的注解声明在类上面。
@MySetterGetter
public class UserInfo {
private String name;
private int age;
private boolean flag;
public static void main(String[] args) {
UserInfo userInfo = new UserInfo();
userInfo.setName("zcs");
System.out.println(userInfo.getName());
System.out.println(userInfo);
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
'}';
}
}
运行main方法可以正常运行:
编译后的java类也能正常生成set/get方法
到此就说明我们自己定义的编译时注解已经可以实现自动生成get/set方法了。