在JAVA 1.6后,JDK提供了一种方式,可以让我们修改编译过程,在编译期融入我们自己编译逻辑。它提供了一组编译器的插入式注解处理器的标准API再编译期间对注解进行处理,我们可以利用这些API,读取、修改、添加抽象语法树中的任意元素。如果因为这些注解对语法树进行了修改,编译器会重新进行词法、语法的分析处理,直到所有的插入式注解没有对语法树进行修改为止。
下面我们通过一个实例来讲解下。我们大家都知道java中有个类库叫做lombok,他的作用就是帮助我们减少一些模板代码,例如属性中的set、get方法的生成等,他通过在加注解的方式,在编译期,动态的为属性添加了set、get方法,利用的就是上述所讲的功能做的。今天我们也实现以下这个功能。
一.定义注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
}
既然是插入式注解
那么注解肯定是必不可少的。我们定义一个Data
类型的注解,希望可以在编译期,给打上这个注解的类,生产set、get方法。
二. 定义处理器类
@SupportedAnnotationTypes("jsr.Data")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataProcessor extends AbstractProcessor {
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
roundEnv.getElementsAnnotatedWith(Data.class).stream().map(element -> trees.getTree(element)).forEach(t -> t.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
jcClassDecl.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE)).map(tree -> (JCTree.JCVariableDecl) tree).forEach(jcVariableDecl -> {
//添加get方法
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
//添加set方法
jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
}));
return true;
}
/**
* 创建get方法
*
* @param jcVariableDecl
* @return
*/
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
//方法的访问级别
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
//方法名称
Name methodName = getMethodName(jcVariableDecl.getName());
//设置返回值类型
JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;
ListBuffer statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
//设置方法体
JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
List methodGenericParams = List.nil();
List parameters = List.nil();
List throwsClauses = List.nil();
//构建方法
return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
}
/**
* 创建set方法
*
* @param jcVariableDecl
* @return
*/
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
try {
//方法的访问级别
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
//定义方法名
Name methodName = setMethodName(jcVariableDecl.getName());
//定义返回值类型
JCTree.JCExpression returnMethodType = treeMaker.Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));
ListBuffer statements = new ListBuffer<>();
statements.append(treeMaker.Exec(treeMaker.Assign(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()))));
//定义方法体
JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
List methodGenericParams = List.nil();
//定义入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariableDecl.name, jcVariableDecl.vartype, null);
//设置入参
List parameters = List.of(param);
List throwsClauses = List.nil();
//构建新方法
return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
} catch (Exception e) {
System.err.println(e);
}
return null;
}
private Name getMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
private Name setMethodName(Name name) {
String s = name.toString();
return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
@SupportedAnnotationTypes("jsr.Data") 标示此处理器可以处理的注解。如果是“*”代表所有的注解都可以处理。@SupportedSourceVersion(SourceVersion.RELEASE_8)标示此处理器可以处理那些版本的JAVA代码。init方法是初始化的方法,可以在里面初始化javac运行期间的一些对象。例如JavacTrees是带处理的抽象语法树对象,TreeMaker中提供了一些操作抽象语法树节点的方法,Names提供了创建标识符的方法。process方法是真正处理抽象语法树的方法,这个方法的返回值是布尔类型。如果返回true,说明修改了语法树,那么就需要重新进行解析和填充符号步骤。如果返回false,说明没有进行过修改,例如只是检查等操作,这样就不需要重新解析了。process方法里面所做的事情,就是根据属性名称,动态的创建set方法(makeSetterMethodDecl)和get方法(makeGetterMethodDecl),然后将创建的方法添加到这个类中的方法中(jcClassDecl.defs)随后进行编译,至于动态创建方法的逻辑中的没一步基本上都有注释,相信大家都能看懂,就不详细介绍了。这样,就在编译期间生成了属性对应的方法,这也就是lombok的原理所在。
三. 如何加载处理器
- 通过javac命令的“-processor”参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号进行分割。
- 通过JAVA SPI加载。在resources目录下添加META-INF/services目录内添加文件名为
javax.annotation.processing.Processor
文件内容是jsr.DataProcessor
也就是我们自定义的这个处理器。关于JAVA SPI的内容,可以参考我的另外一篇文章揭秘JDK SPI,通过pom中的下面配置,将此信息打入JAR包。
src/main/resources
META-INF/**/*
org.apache.maven.plugins
maven-compiler-plugin
3.1
1.8
org.apache.maven.plugins
maven-resources-plugin
2.6
process-META
prepare-package
copy-resources
target/classes
${basedir}/src/main/resources/
**/*
四. 使用
客户端使用如下
package jsr;
@Data
public class App {
private String value;
public static void main(String[] args) {
App app = new App();
app.setValue("hello world");
System.out.println(app.getValue());
}
}
编译后的文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package jsr;
public class App {
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public App() {
}
public static void main(String[] args) {
App app = new App();
app.setValue("hello world");
System.out.println(app.getValue());
}
}
可见我们的编译时注解生效了。
五.项目源码
项目源码
六. 参考资料
深入理解Java虚拟机_JVM高级特性与最佳实践
Lombok原理分析与功能实现