注解
Java注解
定义
注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。
注解的生命周期有 3 种策略,定义在 RetentionPolicy 枚举中。
- SOURCE:在源文件中有效,被编译器丢弃。
- CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。
- RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。
截止到 Java 9,注解的类型一共有 11 种,定义在 ElementType 枚举中。
- TYPE:用于类、接口、注解、枚举
- FIELD:用于字段(类的成员变量),或者枚举常量
- METHOD:用于方法
- PARAMETER:用于普通方法或者构造方法的参数
- CONSTRUCTOR:用于构造方法
- LOCAL_VARIABLE:用于变量
- ANNOTATION_TYPE:用于注解
- PACKAGE:用于包
- TYPE_PARAMETER:用于泛型参数
- TYPE_USE:用于声明语句、泛型或者强制转换语句中的类型
- MODULE:用于模块
使用:对于运行时注解,一般在对应代码中利用反射方式获取某个对象的注解信息,然后做处理。
运行时注解
设计注解JsonField,用于标记某类需要序列化,并指定Json序列化时的key名称。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
public String value() default "";
}
那么,利用这个注解的实现代码如下:
public class JsonSerializer {
public static String serialize(Object object) throws IllegalAccessException {
Class<?> objectClass = object.getClass();
Map<String, String> jsonElements = new HashMap<>();
for (Field field : objectClass.getDeclaredFields()) {
// 获取field
field.setAccessible(true);
if (field.isAnnotationPresent(JsonField.class)) {
jsonElements.put(getSerializedKey(field), (String) field.get(object));
}
}
return toJsonString(jsonElements);
}
private static String getSerializedKey(Field field) {
// 根据Filed反射获得注解
String annotationValue = field.getAnnotation(JsonField.class).value();
if (annotationValue.isEmpty()) {
return field.getName();
} else {
return annotationValue;
}
}
private static String toJsonString(Map<String, String> jsonMap) {
String elementsString = jsonMap.entrySet()
.stream()
.map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
.collect(Collectors.joining(","));
return "{" + elementsString + "}";
}
}
应该还是很好懂的。
编译时注解
以上是运行时注解的处理流程,我们还知道像Lombok有类似@Data这类注解,这些是编译时注解,那么编译时注解应该如何处理呢?
注解编译期处理流程最关键的一个类就是Processor ,它是注解处理器的接口类,我们所有需要对编译期处理注解的逻辑都需要实现这个Processor接口,当然,AbstractProcessor 抽象类帮我们写好了大部分都流程,所以我们只需要实现这个抽象类就可以很方便的定义一个注解处理器;
注解处理器的处理步骤:
在java编译器中构建;
- 编译器开始执行未执行过的注解处理器;
- 循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性;
- 生成对应的类,并写入文件;
- 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)。
定义处理器
首先继承AbstractProcessor 抽象类就可以定义一个处理器,通过@SupportedAnnotationTypes("*")
注解定义某个处理器能处理的注解类型,这里是*,意思就是处理所有注解。通过@SupportedSourceVersion(SourceVersion.RELEASE_11)
来定义这个处理器支持处理的版本。
当然这两个接口都可以通过重新抽象类中对应的方法来重新实现,而不通过注解简单指定,其中@SupportedSourceVersion
推荐重写为获取最新版本:
@Override
public SourceVersion getSupportedSourceVersion() {
//设置为能够支持最新版本
return SourceVersion.latestSupported();
}
init()
方法,一般不用动,但这个方法传入了ProcessingEnvironment 对象,可以根据自己的需求来重新编写,实现通过编译参数指定注解功能。
接下来就是最重要的process()
环节,process方法提供了两个参数,第一个是我们请求处理注解类型的集合(也就是我们通过重写getSupportedAnnotationTypes方法所指定的注解类型),第二个是有关当前和上一次循环的信息的环境。返回值表示这些注解是否由此 Processor 声明 如果返回 true,则这些注解不会被后续 Processor 处理; 如果返回 false,则这些注解可以被后续的 Processor 处理。
这部分具体怎么写,直接看后面的范例。
注册处理器
不是说实现了AbstractProcessor类就会生效,由于注解处理器是在编译期执行的,而且它是作为一个Jar包的形式来生效,所以我们需要将注解处理器作为一个单独的Module来打包。 然后在需要使用到注解处理器的Module引用。
在resource/META-INF.services文件夹下创建一个名为javax.annotation.processing.Processor的文件;里面的内容就是你的注解处理器的全限定类名。
设置编译期间禁止处理 Process,之所以这样做是因为,如果你不禁止Process,ServiceLoader就会去加载你刚刚设置的注解处理器,但是因为是在编译期,Class文件被没有被成功加载,所以会抛异常。
<compilerArgument>-proc:none</compilerArgument>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
注解处理器打包成功,就可以提供给别的Module使用了。
除此之外,更推荐用一个开源组件快速定义处理器:@AutoService
@AutoService 是Google开源的一个小插件,它可以自动的帮我们生成META-INF/services 的文件,也就不需要你去手动的创建配置文件了。当然,上面的
例如下面,使用@AutoService(Processor.class),他会自动帮我们生成对应的配置文件。
@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
}
具体使用参考:https://www.cnblogs.com/strongmore/p/13284444.html
范例
@Target(ElementType.METHOD) // 注解用在方法上
@Retention(RetentionPolicy.SOURCE) // 尽在Source处理期间可用,运行期不可用
public @interface BuildProperty {
}
@SupportedAnnotationTypes("org.example.BuildProperty") // 只处理这个注解;
public class SzzBuildProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzBuildProcessor.process ;");
for (TypeElement annotation : annotations) {
// 获取所有被该注解 标记过的实例
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
// 按照需求 检查注解使用的是否正确 以set开头,并且参数只有一个
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
// 打印注解使用错误的case
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty 注解必须放到方法上并且是set开头的单参数方法", element));
if (setters.isEmpty()) {
continue;
}
Map<String ,List<Element>> groupMap = new HashMap();
// 按照全限定类名分组。一个类创建一个Build
setters.forEach(setter ->{
// 全限定类名
String className = ((TypeElement) setter
.getEnclosingElement()).getQualifiedName().toString();
List<Element> elements = groupMap.get(className);
if(elements != null){
elements.add(setter);
}else {
List<Element> newElements = new ArrayList<>();
newElements.add(setter);
groupMap.put(className,newElements);
}
});
groupMap.forEach((groupSetterKey,groupSettervalue)->{
//获取 类名SimpleName 和 set方法的入参
Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
try {
// 组装XXXBuild类。并创建对应的类文件
writeBuilderFile(groupSetterKey,setterMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
// 返回false 表示 当前处理器处理了之后 其他的处理器也可以接着处理,返回true表示,我处理完了之后其他处理器不再处理
return true;
}
private void writeBuilderFile(
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.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}