Java里面随处可见annotation(注解),RetentionPolicy 指示了注解使用的情况:
而 CLASS 则是用于 compile 编译阶段的注解。一个注解的处理器,以Java代码(或编译过的字节码)作为输入,生成Java文件。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
可以自己实现一些类似groovy语法糖的功能(lombok框架修改bytecode为类生成新方法getter/setter、或者使用生成新的辅助类等);减少机械的、冗余代码的管理,使得代码更简洁便于阅读。
先来了解下整个过程,javac 从 ServiceLoader 获取一个 Processor 标注处理类,判断是否为符合条件的标注,再收集类的相关信息,然后使用 Filer 创建新的类。 Java Annotation Processing and Creating a Builder , java annotation processor 主要涉及到如下三部分:
Service:
通过google的auto-service来注册服务,最终会在 META-INF/services/ 生成名称为 javax.annotation.processing.Processor 的文件,内容为当前被标注的类名。
项目的目录结构如下:
package com.github.winse.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface BuilderProperty { }
package com.github.winse.processor; import com.github.winse.annotation.BuilderProperty; import com.google.auto.service.AutoService; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ExecutableType; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * @see BuilderProperty */ @SupportedAnnotationTypes("com.github.winse.annotation.BuilderProperty") @SupportedSourceVersion(SourceVersion.RELEASE_8) @AutoService(Processor.class) public class BuilderProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { Set<? extends Element> annotationElements = roundEnv.getElementsAnnotatedWith(annotation); Map<Boolean, List<Element>> annotationMethods = annotationElements.stream() .collect(Collectors.partitioningBy(element -> ((ExecutableType) element.asType()).getParameterTypes().size() == 1 && element.getSimpleName().toString().startsWith("set"))); List<Element> setters = annotationMethods.get(true); List<Element> otherMethods = annotationMethods.get(false); otherMethods.forEach(element -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@BuildProperty must be applied to a setXxx method with a single argument", element)); if (setters.isEmpty()) { continue; } String className = ((TypeElement) setters.get(0).getEnclosingElement()).getQualifiedName().toString(); Map<String, String> setterMap = setters.stream().collect(Collectors.toMap( setter -> setter.getSimpleName().toString(), setter -> ((ExecutableType) setter.asType()).getParameterTypes().get(0).toString() )); try { writeBuilderType(className, setterMap); } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); } } return true; } private void writeBuilderType(String className, Map<String, String> setterMap) throws IOException { String packageName = null; int lastDot = className.lastIndexOf("."); if (lastDot > 0) { packageName = className.substring(0, lastDot); } String simpleClassName = className.substring(lastDot + 1); String builderClassName = className + "Builder"; String builderSimpleClassName = builderClassName.substring(lastDot + 1); JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName); try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { if (packageName != null) { out.printf("package %s;/n", packageName); out.println(); } out.printf("public class %s {/n", builderSimpleClassName); out.println(); out.printf(" private %s object = new %s();/n", simpleClassName, simpleClassName); out.println(); out.printf(" public %s build() {/n", simpleClassName); out.printf(" return object;/n"); out.printf(" }/n"); out.println(); setterMap.entrySet().forEach(setter -> { String methodName = setter.getKey(); String argumentType = setter.getValue(); out.printf(" public %s %s(%s value){/n", builderSimpleClassName, methodName, argumentType); out.printf(" object.%s(value);/n", methodName); out.printf(" return this;/n"); out.printf(" }/n"); out.println(); }); out.printf("}/n"); } } }
我使用的是4.7的版本,4.7以上版本可以直接使用 annotationProcessor 来添加标注处理器。(其他版本可以使用 apt 来处理)
plugins { id "net.ltgt.apt" version "0.10" } sourceSets.main.java.srcDirs += ['build/generated/source/apt/main'] dependencies { compile rootProject annotationProcessor project(':compiler') }
这是一个POJO类,BuilderProcessor处理器会根据BuilderProperty注解来生成PersonBuilder类。
package com.github.winse.example; import com.github.winse.annotation.BuilderProperty; public class Person { private int age; private String name; @BuilderProperty public void setAge(int age) { this.age = age; } @BuilderProperty public void setName(String name) { this.name = name; } public int getAge() { return age; } public String getName() { return name; } }
在 gradle 面板中选择子项目 :example
,然后选择 Tasks 下的 build 任务进行构建。构建完后在 example/build/generated/source/apt
目录下生成了对应的 Builder 代码。
package com.github.winse.example; public class PersonBuilder { private Person object = new Person(); public Person build() { return object; } public PersonBuilder setName(java.lang.String value){ object.setName(value); return this; } public PersonBuilder setAge(int value){ object.setAge(value); return this; } }
不会调试说明还没有真正的入门。并且没有调试的情况下,解决异常、错误也是一件异常痛苦的事情。注解处理器生成代码是在编译阶段来生成代码的,所以调试的选项配置添加到 javac 。而 gradle 提供了一种相对简单的方式来进行。
参考
具体步骤如下:
在命令行运行构建
添加调试参数后,gradle 会 暂停等待远程调试 ,相当于添加了 JVM 调试参数。 Gradle properties
hello-annotation-processor/example>gradle clean build --no-daemon -Dorg.gradle.debug=true 或者 hello-annotation-processor>gradle example:clean example:compileJava --no-daemon -Dorg.gradle.debug=true
注: –no-daemon 不加也是可以的,但是运行该次构建后不会停止。
远程调试
通过环境变量
example>set GRADLE_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 example>gradle clean build Listening for transport dt_socket at address: 5005
修改 ~/.gradle/gradle.properties
这种方式不推荐,因为它是全局的。
org.gradle.daemon=false org.gradle.debug=true
或者
org.gradle.daemon=true org.gradle.jvmargs=-XX:MaxPermSize=4g -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006 $ gradle --daemon
Then attach your debugger client to port 5006, set your breakpoint, then run your test.
注:该配置放到项目目录下没用。
–END