Java中的注解(Annotations)是在 JDK 1.5 中引入的,这次引入大大增强了Java语言的元数据功能,使开发者能够以更加简洁和灵活的方式向代码中添加元信息。
⨳ JDK 1.5:首次引入注解机制,包括常见的内置注解和元注解,支持定义自定义注解以及通过反射解析注解。
⨳ JDK 1.6:引入了注解处理器API,用于在编译期间处理注解。这允许开发者创建注解处理器来生成代码、进行验证等。
⨳ JDK 1.8:通过引入JSR 308(类型注解),注解可以用于任何使用类型的地方,如泛型类型参数、局部变量类型、类型转换等.
⨳ ...
元注解
元注解(Meta-Annotations)是用于修饰其他注解的注解。它们定义了自定义注解的行为和应用范围,控制注解的生命周期、目标、可重复性等。
@Retention 注解保留策略
@Retention 指定注解的保留策略,即注解在什么阶段被保留。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
@Retention 注解接受一个 RetentionPolicy 枚举值,来指定注解的保留策略。
| RetentionPolicy.SOURCE | RetentionPolicy.CLASS | RetentionPolicy.RUNTIME |
|---|---|---|
| 注解只在源代码中保留,编译时会被丢弃,不会在字节码中存在 | 注解在编译时保留在字节码中,但不会在运行时可见(默认值) | 注解在运行时保留,可以通过反射获取注解信息 |
@Target 注解应用目标
@Target 指定注解可以应用的目标(如类、方法、字段等)。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation interface
* can be applied to.
* @return an array of the kinds of elements an annotation interface
* can be applied to
*/
ElementType[] value();
}
@Target 接受一个 ElementType 枚举数组作为参数:
| TYPE | FIELD | METHOD | PARAMETER | CONSTRUCTOR | LOCAL_VARIABLE | ANNOTATION_TYPE | PACKAGE | TYPE_PARAMETER | TYPE_USE |
|---|---|---|---|---|---|---|---|---|---|
| 类、接口 | 字段 | 方法 | 方法参数 | 构造函数 | 局部变量 | 注解类型 | 包声明 | 泛型类型参数 | 使用类型 |
其中 ElementType.TYPE_PARAMETER 和 ElementType.TYPE_USE 都是 JDK1.8 引入的,特别是ElementType.TYPE_USE 可以让注解应用于任何使用类型的地方,包括泛型、类型转换、实现类或接口等,几乎所有涉及类型的位置都可以使用。
@Inherited 注解是否可以被继承
@Inherited 用于指定某个注解类型是否可以被自动继承。如果一个注解被 @Inherited 修饰,那么当它被应用在一个类上时,这个注解会自动应用于该类的子类。需要注意的是,@Inherited 只对类有效,不适用于接口、方法、字段、参数等。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
@Documented 注解是否包含在JavaDoc中
@Documented 是一个元注解,用于指定一个自定义注解是否会出现在生成的JavaDoc文档中。换句话说,如果一个注解类型被 @Documented 标记,那么在使用该注解的地方,其注解信息将会包含在JavaDoc生成的API文档中。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
一些重要的注解(如 @Deprecated、@Override)通常希望它们的使用能够被文档化,以便用户清楚地了解它们的影响和作用。
内置注解
Java 提供了一些内置注解(也称为标准注解),这些注解在 Java 语言中已经定义,并且可以直接用于代码中。内置注解通常用于编译时检查、编译器指示或提高代码可读性。
@Override 重写
@Override 表示一个方法是重写了父类或接口中的方法。如果该注解所标注的方法没有正确地重写父类或接口中的方法,编译器会报错。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Deprecated 废弃的
@Deprecated 表示某个类、方法、或字段已经不建议使用,可能会在未来版本中被移除。使用被标注为 @Deprecated 的代码时,编译器会发出警告。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
String since() default "";
boolean forRemoval() default false;
}
@SuppressWarnings 压制警告
@SuppressWarnings 指示编译器忽略特定的警告信息。 参数是一个或多个警告类型的字符串,如 "unchecked"、"deprecation" 等。
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
...
还有几个常用的内置就不一一举例了。
⨳ @SafeVarargs:Java 7 引入,表示使用了泛型可变参数的方法是类型安全的。只能用于 final 或 static 方法,或构造函数。
⨳ @FunctionalInterface:Java 8 引入,用于标识一个接口是函数式接口(即只包含一个抽象方法的接口)。如果接口中有多于一个的抽象方法,编译器会报错。
⨳ @Native:用于表示一个字段可以在本地代码(如C或C++)中使用,通常与 native 方法结合使用。
可以看到内置注解更多的是“说明性质”的,帮助开发者理解和维护代码,“功能性”不大。
自定义注解
看了这么多内置注解,也了解修饰其他注解的元注解,注解应该不难写出。
注解格式
创建自定义注解的基本步骤如下:
- 定义注解: 使用
@interface关键字定义一个注解类型。 - 元注解: 结合使用元注解如
@Retention、@Target、@Documented、@Inherited来控制注解的行为和作用范围。 - 注解属性: 可以在注解中定义属性,类似于接口中的方法。
如下,创建了一个 Author 注解,可以标注在类上,也可以标志在方法上,用来说明是谁在那一天写的代码。
package com.cango.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Author {
String name();
String date();
}
注意,一旦注解有属性了,使用注解的时候,属性必须有值,当然也可以使用默认值。
属性类型 属性名() default 默认值;
应用注解
应用注解很简单,加到想要加的位置即可,
package com.cango.annotation;
@Author(name = "Cango", date = "2024-08-08")
public class MyClass {
}
因为 @Author 有两个属性,所以需要使用 key=value 的形式给属性赋值,如果属性只有一个,直接写 value 即可。
@Author("Cango")
反射解析注解
还记得《反射篇》讲的 Class、Filed、Method、Constructor 都实现了注解元素 AnnotatedElement 吗?
通过它提供的统一处理注解的能力,很容易就可以在反射时获取注解,解析注解。
Class<MyClass> clazz = MyClass.class;
// 获取类上的注解
if(clazz.isAnnotationPresent(Author.class)){
Author clazzAuthor = clazz.getAnnotation(Author.class);
System.out.println(clazz.getSimpleName()+" 是 "+clazzAuthor.name()+" 写的,日期是 "+clazzAuthor.date());
}
// 获取方法上的注解
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Author.class)) {
Author methodAuth = method.getAnnotation(Author.class);
System.out.println(method.getName()+" 是 "+methodAuth.name()+" 写的,日期是 "+methodAuth.date());
}
}
输出结果:
MyClass 是 Cango 写的,日期是 2024-08-08
sayHello 是 King 写的,日期是 2024-08-10
因为反射可以在程序运行期间获取注解,获取注解的属性值,那根据这些属性值进行不同的行为,是不是就特别方便了。
注解处理器
如果说反射解析注解是应用于程序运行期间,那注解处理器(Annotation Processor)就是应用于程序编译期间。
注解处理器通过扫描源代码中的注解,并根据注解信息生成新的代码或文件,执行编译时任务。
首先,定义一个自定义注解 @BuilderProperty ,用于根据类成员属性生成一个Builder类:
package com.cango.annotation.processor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BuilderProperty {
}
注意,Retention 是仅限于源代码中保留的 SOURCE。
自定义注解处理器
注解处理器基于JSR 269规范,通常实现javax.annotation.processing.AbstractProcessor类。
package com.cango.annotation.processor;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
// 定义一个BuilderProcessor类,继承自AbstractProcessor
public class BuilderProcessor extends AbstractProcessor {
// 重写process方法,该方法在注解处理时被调用
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 遍历所有注解
for (TypeElement annotation : annotations) {
// 遍历所有被该注解标注的元素
for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
// 获取被注解标注的元素的类名
String className = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
// 生成Builder类的类名
String builderClassName = className + "Builder";
try {
// 创建Builder类的Java文件
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
try (Writer writer = builderFile.openWriter()) {
// 写入包名
writer.write("package " + processingEnv.getElementUtils().getPackageOf(element).getQualifiedName() + ";\n");
// 写入Builder类的定义
writer.write("public class " + builderClassName.substring(builderClassName.lastIndexOf('.') + 1) + " {\n");
// 遍历被注解标注的类的所有字段
for (Element field : ElementFilter.fieldsIn(element.getEnclosingElement().getEnclosedElements())) {
// 如果字段被BuilderProperty注解标注
if (field.getAnnotation(BuilderProperty.class) != null) {
// 获取字段名和字段类型
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
// 写入字段定义
writer.write(" private " + fieldType + " " + fieldName + ";\n");
// 写入字段设置方法
writer.write(" public " + builderClassName.substring(builderClassName.lastIndexOf('.') + 1) + " " + fieldName + "(" + fieldType + " " + fieldName + ") {\n");
writer.write(" this." + fieldName + " = " + fieldName + ";\n");
writer.write(" return this;\n");
writer.write(" }\n");
}
}
// 写入Builder类的结束符
writer.write("}\n");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
}
这个 BuilderProcessor会在编译期间扫描类中的@BuilderProperty注解,并生成一个Builder类。
注册注解处理器
创建文件 META-INF/services/javax.annotation.processing.Processor,并在其中添加注解处理器的完全限定名:
这不是不是很像 SPI(Service Provider Interface),文件名为接口,文件内容是实现类,然后通过 ServiceLoader 来加载。
但这就有个问题,SPI 一般用于发现已经编译好的类如数据库驱动 java.sql.Driver,如果 ServiceLoader 尝试加载注解处理器,但这些处理器尚未编译成功,就会导致编译错误。
那怎么办呢?将注解处理器先编译即可。
可以将注解处理器作为一个单独的 Module 来打包。也可以使用 Google 开源的一个小插件 auto-service 即可:
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.1.1</version>
<!-- 只在编译时需要 -->
<scope>provided</scope>
</dependency>
auto-service 可以自动生成META-INF/services 的文件, 也就不需要创建配置文件了。因为编译的时候META-INF/services 还没有配置注解处理器,也就不会加载异常了。
这里采用第二种方式:
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor
使用注解处理器
随便创建一个 Person 类,使用 @BuilderProperty 标志在其属性上:
package com.cango.annotation.processor;
public class Person {
@BuilderProperty
private String name;
@BuilderProperty
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
编译项目后,注解处理器将在编译时运行,生成相应的Builder类。以下是生成的PersonBuilder类:
public class PersonBuilder {
private String name;
private int age;
public PersonBuilder name(String name) {
this.name = name;
return this;
}
public PersonBuilder age(int age) {
this.age = age;
return this;
}
}
现在你应该知道 lombok 是怎么实现的了吧。
总结
使用注解不能局限于如内置注解的“标识”上,还可以简化代码和减少样板代码,如 Spring 框架中的依赖注入注解(@Autowired)可以消除繁琐的setter方法;如 @Transactional注解用于声明事务性操作,而不需要显式地编写事务管理代码。
当然这少不了反射的支持,有一点需要注意,反射和注解处理在运行时会带来一定的性能开销,尤其是在频繁使用反射的情况下。
当然还可以使用注解处理器在编译期间生成代码,这一点了解一下就行。