【参考资料】
注解处理器(编译期|RetentionPolicy.SOURCE)https://blog.zenfery.cc/archives/78.html
当注解的@Retention为CLASS时,此注解为编译时注解。
如果一个元素上面使用了编译时注解,那么我们可以在编译阶段,解析该元素上的编译时注解,生成自己想要的.java源代码文件。
解析编译时注解需要用到注解处理器(APT,Annotation Processing Tool)。
以如下两个类为例
@CreateInterface("IntSuffix") public class Teacher {
//教书 private void teach(){ System.out.println("teach..."); }
//行走 public void walk(){ System.out.println("walking"); } } | public class Doctor {
//诊断 private void diagnose(){ System.out.println("diagnose..."); }
//行走 public void walk(){ System.out.println("walking"); } } |
对于每个类,我们想抽取其中的public方法生成一个接口,这个接口的名称为原类名+ CreateInterface注解的value参数值。(所以实际上只有Teacher类会生成相应的接口)
@Target(ElementType.TYPE) // 注解使用目标为类 @Retention(RetentionPolicy.CLASS) // 编译时注解 public @interface CreateInterface { String value() default "Interface";// 生成对应接口的后缀 } |
编写
那么我们该如何编写注解处理器呢?
编写注解处理器所使用的包为javax.annotation.processing,主要用到
AbstractProcessor抽象类
此类实现了Processor接口的init()、getSupportOption()、getSupportSourceVersion()等方法,帮助我们完成了环境初始化工作。我们写自定义注解处理器,主要就是继承此类,并重写他的process()方法。
Filer接口
用于创建新文件(.java源文件、.class字节码文件、辅助资源文件)
Messager接口
...
要实现上面的需求,我们的注解处理器可以按照如下逻辑来处理:
循环每一个需要编译处理的类(即Teacher、Doctor),找出有注解@GenerateInterface标识的类(即Teacher)。
找到Teacher类中所有的public方法。
根据类名和方法名,使用Filer对象生成源码类。
具体实现代码如下
//注解处理器 @SupportedAnnotationTypes("com.zenfery.example.anno.CreateInterface") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class CreateInterfaceProcessor extends AbstractProcessor {
private Filer filer; // private Messager messager;
private int r = 1;// 循环次数
@Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); // 初始化Filer和Messager this.filer = processingEnv.getFiler(); // this.messager = processingEnv.getMessager(); }
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("-------------------注解处理器第" + (r++) + "次处理开始-------------------");
// 获取所有输入的元素//应该是一个.java文件是一个元素 Set<? extends Element> elements = roundEnv.getRootElements();
System.out.println("输入的所有元素有:"); for (Element e : elements) { System.out.println(">>> " + e.getSimpleName()); }
// 获取使用了注解@GenerateInterface的元素//这里就是Teacher类//类是TypeElement Set<? extends Element> genElements = roundEnv.getElementsAnnotatedWith(CreateInterface.class); System.out.println("使用了注解@GenerateInterface的元素有:");
//对于每一个元素 for (Element e : genElements) {
System.out.println(">>> " + e.getSimpleName());
//获取该元素上的注解 CreateInterface gi = e.getAnnotation(CreateInterface.class); //获取注解的参数值//生成的带后缀的接口名 String className=e.getSimpleName() + gi.value();
//用于拼接源代码的字符串 String classString = "package com.zenfery.example.bean;\n" + "public interface " + className + " {\n";
// 获取该元素的所有子元素//因为Teacher类中只定义了方法,所以这里就是获取到所有的方法//方法是ExecutableElement List<? extends Element> genElementAlls = e.getEnclosedElements();
//获取该类的非构造函数的public方法(在此处忽略方法的返回类型和参数的判断) for (Element e1 : genElementAlls) {
if (!e1.getSimpleName().toString().equals("<init>") && e1 instanceof ExecutableElement && isPublic(e1)) { classString += " void " + e1.getSimpleName() + "();\n"; }
}
classString += "}";
// 写到.java文件中 try { JavaFileObject jfo = filer.createSourceFile("com.zenfery.example.bean." + className, e); Writer writer = jfo.openWriter(); writer.flush(); writer.append(classString); writer.flush(); writer.close();
} catch (IOException ex) { ex.printStackTrace(); } }
return true; }
// 判断元素的修饰符是否为public public boolean isPublic(Element e) { // 获取元素的修饰符Modifier,注意此处的Modifier非反射中的java.lang.reflect.Modifier,是javax.lang.model.element中的 Set<Modifier> modifiers = e.getModifiers(); for (Modifier m : modifiers) { if (m.equals(Modifier.PUBLIC)) return true; } return false; }
} |
使用
代码编写好以后,如何使用注解处理器呢?在Java8之前,有专门的APT工具来使用注解处理器,在Java8以后,APT工具已经集成到javac命令中,所以我们可以直接使用javac来使用注解处理器
处理前目录结构为
1、 编译注解处理器源代码,生成注解处理器.class文件
javac -encodingutf-8 -d bin\ src\com\zenfery\example\anno\*.javasrc\com\zenfery\example\anno\proc\*.java
编译注解和注解处理器.java文件,在bin目录下生成.class文件
2、 使用注解处理器
javac -encodingutf-8 -d bin\ -cp bin\ -processorcom.zenfery.example.anno.proc.CreateInterfaceProcessor src\com\zenfery\example\anno\*.javasrc\com\zenfery\example\bean\*.java
其中-cp指定注解处理器.class文件的路径,-processor指定注解处理器
可以看出,注解处理器循环执行了三次。
第一次,对Teacher和Doctor类进行处理,并生成Teacher类对应的接口类TeacherIntSuffix;
第二次,对第一次生成的类TeacherIntSuffix再做处理,这一次将不再产生新的类。
第三次,未能发现新生成的类,执行结束。
处理完成以后,会在bin\com\zenfery\example\bean目录下生成TeacherIntSuffix.java文件,如下所示
回到Processor中,在解析使用了注解的源代码时,需要使用到Element类,Element用于代表源代码中的一个元素,可以分为TypeElement(类或接口等)、VariableElement(成员变量)、ExecuteableElement(成员方法),他们之间的层次关系如下所示
可以通过element.getEnclosedElements()获取所有子元素,getEnclosingElement()获取父元素
element. getSimpleName()获取元素的名称,element.asType()获取元素的类型名称
以解析如下源代码为例
@TestAnno("class") public class Test {
@TestAnno("field") public int id;
@TestAnno("method") public String get(){ return ""; } } |
使用如下注解处理器进行解析
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("-------------------注解处理器第" + (r++) + "次处理开始-------------------");
// 获取所有输入的元素//应该是一个.java文件是一个元素 Set<? extends Element> elements = roundEnv.getRootElements();
System.out.println("输入的所有元素有:"); for (Element e : elements) { System.out.println(">>> " + e.getSimpleName()); }
// 获取使用了注解@TestAnno的元素 Set<? extends Element> genElements = roundEnv.getElementsAnnotatedWith(TestAnno.class); System.out.println("使用了注解@TestAnno的元素有:");
//对于每一个元素 for (Element e : genElements) {
//判断该元素的类型 if(e instanceof TypeElement) System.out.println("TypeElement");//可以通过e.getEnclosedElements()获取所有子Element else if(e instanceof VariableElement) System.out.println("VariableElement");//可以通过e.getEnclosingElement()获取父Element else if(e instanceof ExecutableElement) System.out.println("ExecutableElement");
//获取该元素的名称 System.out.println("name = " + e.getSimpleName()); //获取该元素的类型名称 System.out.println("type = "+e.asType());
TestAnno annotation = e.getAnnotation(TestAnno.class); System.out.println("anno value = "+annotation.value());
}
return true; } |
输出日志如下