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