目录
1、Java 注解的介绍
Java 注解是元数据的一种形式,注解可以用来给应用程序提供数据,不过这些数据并不是程序本身的一部分。注解对所注解的代码逻辑没有直接影响。
注解有很多用途,其中包括:
- 作为编译器的信息载体——编译器可以使用注解来检测错误或压制警告。
- 作为编译时和部署时处理的信息载体——软件工具可以处理注解信息,用来生成代码、XML文件等等。
- 作为运行时处理的信息载体——有些注解可以在运行时进行检查。
注解的本质:
注解的本质就是一个继承了 Annotation 接口的接口,
一个注解只不过是一种特殊的注释而已,如果没有解析它的代码,它连注释都不如
。
注解可以在哪些地方使用?
注解可以用来声明:类、字段、方法和其他程序元素。一般情况下,按照惯例,每个注解都独占一行。从 Java SE 8 发行版开始,注解除了用作声明外,也可以应用于内置类型检查。下面是一些例子:
// 创建类的实例
new @Interned MyObject();
// 类型转换
myString = (@NonNull String) str;
// implements 语句
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
// 抛出异常
void monitorTemperature() throws @Critical TemperatureException { ... }
以上形式的注解称为类型注解。
2、如何定义一个 Java 注解?
如果需要在每个类的主体开始部分提供一些额外的信息,传统上使用注释的做法如下:
public class Generation3List extends Generation2List {
// Author: swadian2008
// Date: 3/17/2022
// Current revision: 6
// Last modified: 4/12/2022
// By: swadian2008
// Reviewers: Alice, Bill, Cindy
// class code goes here
}
如果需要使用注解来添加相同的信息,那么首先需要定义一个注解,定义注解的语法如下:
@interface ClassPreamble {
String author();
String date();
int currentRevision() default 1;
String lastModified() default "N/A";
String lastModifiedBy() default "N/A";
// 注意数组的使用
String[] reviewers();
}
注解定义看起来很像是接口的定义
,只不过关键字 interface 前面带有 at 符号(@),@ = AT,表示注解类型,可以理解为注解是一种特殊形式的接口。
// 很像接口,比如最终的应用也需要具体的代码去实现,不然注解就没有任何作用
注解的主体中包含了很多注解元素,它们看起来像方法
,这些元素还可以定义属于自己的默认值。
3、在 Java 中预定义的一些注解
在 Java SE API 中预定义了一些注解。这些注解有的是由 Java 编译器使用,有的用来作为定义其他注解的元注解。
1)
由 Java 编译器使用的预定义注解
比如 @Deprecated,@Override,@SuppressWarnings,@SafeVarargs,@FunctionalInterface 等注解。
// 定义在 java.lang 中的三大内置注解:
@Override //重写标记:标记注解,编译结束后被丢弃
@Deprecated //被废除标记:标记注解,编译结束后被丢弃
@SuppressWarnings //压制编译器检查
@SafeVarargs 注解:当应用于方法或构造函数时,断言代码不会对其可变参数执行不安全的操作。当使用此注解时,与可变参数相关的未检查警告将被压制。
@FunctionalInterface 注解:指示声明的接口是一个函数式接口。
2)
Java 元注解:
应用于其他注解的注解称为元注解。在 java.lang.annotation 包中定义了以下几种元注解:@Target、@Documented、@Retention、@Inherited、@Repeatable
@Target 注解
:用来标记另一个注解,限制注解的使用方式和适用对象,@Target 指定以下元素类型之一作为它的值:
// JDK 19
ElementType.ANNOTATION_TYPE // 可应用于注解
ElementType.CONSTRUCTOR // 可应用于构造器
ElementType.FIELD // 可应用于字段或属性
ElementType.LOCAL_VARIABLE // 可应用于局部变量
ElementType.METHOD // 可以用于方法级注解
ElementType.PACKAGE // 可应用于包声明
ElementType.PARAMETER // 可应用于方法参数
ElementType.TYPE // 可以用于类
@Documented 注解:
@Documented 注解指示该注解应该被 javadoc 工具进行记录,默认情况下,javadoc 工具不记录注解信息,但声明注解时如果标记了 @Documented,那么被标记的注解就会被 javadoc 工具处理,注解信息也会被记录在生成的文档中。
使用 @Documented 标记的注解生成的文档效果展示如图:
@Retention 注解:
指定注解的生命周期,比如是编译期还是运行时有效:
基本参数如下:
RetentionPolicy.SOURCE:被标记的注解仅保留在源代码级别,并被编译器忽略。
RetentionPolicy.CLASS:被标记的注解在编译时被编译器保留,但被Java虚拟机(JVM)忽略,运行时不使用
RetentionPolicy.RUNTIME:被标记的注解由JVM保留,因此在运行时环境可以使用它。
@Inherited 注解:
指示被标记的注解具有可继承性(默认情况下注解不能从父类继承)。也就是说该注解修饰了一个类,而该类的子类将自动继承父类的注解。该注解仅应用于对的类声明。
代码示例:被 @Inherited 标记的注解具有继承性
/**
* 注解A——@Inherited标记
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserCaseA {
public String usernameA() default "UserCaseA的默认值";
}
/**
* 注解B
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserCaseB {
public String usernameB() default "UserCaseB的默认值";
}
/**
* 父类A
*/
@UserCaseA
public class SuperA {
}
/**
* 父类B
*/
@UserCaseB
public class SuperB {
}
/**
* 子类A
*/
public class SubA extends SuperA {
}
/**
* 子类B
*/
public class SubB extends SuperB {
}
public class Test {
public static void main(String[] args) {
// 被 @Inherited 标记的 @UserCaseA
Class<SubA> subAClass = SubA.class;
System.out.println("subAClass have @UserCaseA ?:" + subAClass.isAnnotationPresent(UserCaseA.class));
// 无 @Inherited 标记的 @UserCaseB
Class<SubB> subBClass = SubB.class;
System.out.println("subBClass have @UserCaseB ?:" + subBClass.isAnnotationPresent(UserCaseB.class));
}
}
执行程序后,将输出以下结果:
// 没有被 @Inherited 标记的注解不能被子类继承
subAClass have @UserCaseA ?:true
subBClass have @UserCaseB ?:false
@Repeatable 注解
:在 Java SE 8 中引入的,表示标记的注解可以多次应用于相同的声明或类型。
4、Java Java SE 8 中新增的类型注解
在 Java SE 8 发布之前,注解只能用于声明。从 Java SE 8 发行版开始,注解还可以在使用类型的任何地方使用。比如前面提到的使用类型的几个例子,创建(new)、类型转换、实现语句和异常抛出语句:
// 创建类的实例
new @Interned MyObject();
// 类型转换
myString = (@NonNull String) str;
// implements 语句
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
// 抛出的异常声明
void monitorTemperature() throws @Critical TemperatureException { ... }
创建 Java 类型注解的目的,是为了改进 Java 的程序分析,确保更强的类型检查。
Java SE 8 发行版并没有提供类型检查的框架,但是允许开发人员编写(或引入)类型检查框架,这些框架实现了一个或多个可插拔的模块(这些模块可以与 Java 编译器一起使用)。
// 编译期提供更强的类型检查
例如,如果希望确保程序中的某个变量永远不会被赋值为 null;或者想要避免触发NullPointerException。那么可以编写一个自定义插件来检查这一点,然后修改代码注解该变量,表明它永远不会被赋值为 null。变量声明可能是这样的:
@NonNull String str;
当编译代码时(包括在命令行上编译 NonNull 模块),如果编译器检测到潜在的问题,就会会发出警告,该警告可以让你及时的修改代码从而避免产生错误。
合理的使用类型注解和可插拔类型检查器,可以让开发人员编写更加强大、更不容易出错的代码。不过在许多情况下,都不必编写自己的类型检查模块,因为已经有第三方为你做了类似的工作。
// 使用第三方框架
5、Java Java SE 8 中新增的可重复注解
在某些情况下,可能希望将相同的注解应用于声明或类型检查。那么在Java SE 8发行版中,就可以通过重复注解来实现。
例如,想要编写一个计时器,在每月的最后一天和每个周五晚上的 11:00 运行doPeriodicCleanup() 方法。那么就需要创建 @Schedule 注解并将其应用于 doPeriodicCleanup() 方法两次。示例代码如下所示:
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }
1)声明一个可重复的注解
//声明一个可重复注解的步骤
注解必须使用 @Repeatable 元注解进行标记。下面的例子定义了一个自定义的 @Schedule可重复注解:
import java.lang.annotation.Repeatable;
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
@Repeatable() 注解括号中的值,是 Java 编译器为存储重复注解而生成的注解容器的类型。在本例中,包含的注解类型是 Schedules,因此重复的 @Schedule 注解会存储在 @Schedules 注解中。
// Schedules 是注解的容器类型
如果一个注解没有声明它是可重复的,那么就不能在同一个地方重复的使用它,否则将导致编译时错误。
2)声明重复注解的容器注解
容器注解必须包含数组类型的元素,数组类型的元素类型必须是可重复的注解类型。对于包含注解类型 @Schedule 的 @Schedules 的声明如下:
public @interface Schedules {
Schedule[] value();
}
3)如何查找注解?
Reflection API (反射)中有几个方法可用来查找注解。比如获取单个注解的方法:AnnotatedElement.getAnnotation(Class<T>)。如果存在多个注解,则可以通过获取它们的容器注解来获取它们,比如使用一次返回多个注解的方法: AnnotatedElement.getAnnotationsByType(Class<T>)。
Class 类中提供了以下一些 API 用于获取定义在类,属性或方法上的注解:
public <A extends Annotation> A getAnnotation(Class<A> annotationClass):返回指定的注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判定当前元素是否被指定注解修饰
public Annotation[] getAnnotations():返回所有的注解
public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass):返回本元素的指定注解
public Annotation[] getDeclaredAnnotations():返回本元素的所有注解,但不包括父类继承而来的
6、如何解析一个注解?
解析注解的两种方式:
- 编译期扫描——适用于JDK内置的几个注解
- 运行期反射——适用于自定义注解
接下来主要介绍通过运行期反射来获取自定义注解的操作。
1)首先定义一个注解
自定义注解可以选择性的使用元注解进行修饰,这样可以更加具体的指定注解的生命周期、作用范围等信息
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效,通过反射可以获取注解的配置值
public @interface UserCase {// 注解就是一个标签,使用@interface定义
/**
* 注解定义元素的方式,可以设置默认值
* @return
*/
public String id();
public String descrption() default "no descrption";
}
2)通过反射获取注解的值
通过获取类的字节码,封装为 Class 类,从而可以使用 Class 类中的方法:
public class AnnotationTest {
/**
* 使用注解作用在方法上
* @param password
* @return
*/
@UserCase(id = "8888",descrption = "替代默认值")
public String getPassword(String password){
return password;
}
/**
* 通过反射获取默认值
* @param args
*/
public static void main(String[] args) {
// 获取类的字节码
Class<AnnotationTest> cl = AnnotationTest.class;
// 获取类中所有的方法,包括私有方法
Method[] declaredMethods = cl.getDeclaredMethods();
if(Objects.nonNull(declaredMethods)){
for(Method method : declaredMethods){
// 判断方法上是不是包含有指定的注解
if(method.isAnnotationPresent(UserCase.class)){
// 获取方法上的指定注解
UserCase annotation = method.getAnnotation(UserCase.class);
if(Objects.nonNull(annotation)){
// 获取并打印注解的配置信息
System.out.println(annotation.id()+":"+annotation.descrption());
}
}
}
}
}
}
上述程序的执行结果:
至此,Java 中的注解介绍完毕。