Java基础注解(二)
注解(Annotation)又称 Java 标注,是 JDK5.0
引入的一种注释机制。注解是一种元数据,注解本身对被标注的代码没有任何直接的影响,它为程序提供了一些额外的数据信息,注解通常使用在如下场景
- 为编译器提供信息:编译器可通过注解去探测错误或者压制警告,如
@Deprecated
@SuppressWarnings("unchecked")
- 编译时处理:在程序编译时,一些软件工具可通过程序上标注的注解信息去生成一些代码,如Project Lombok的
@Getter
@Setter
- 运行时处理:有些注解可以在运行时保留,经常结合反射API执行一些逻辑处理。
使用示例
代码中通过 @
字符标注一个注解,紧随其后的是注解名,如@Override
,Override是注解名
@Override
void mySuperMethod() {
...
}
注解可以包含一些属性,使用时属性可以命名或者匿名,属性有对应的值
Java
@Author(
name = "Benjamin Franklin",
date = "3/27/2003"
)
class MyClass {
...
}
@SuppressWarnings(value = "unchecked")
void myMethod() {
...
}
当注解只有一个命名属性时,使用时则可以省略属性名赋值
@SuppressWarnings("unchecked")
void myMethod() {
...
}
当注解没有任何属性时,圆括号则可以省略
@Override
void mySuperMethod() {
...
}
注解可以应用在Java类、方法、字段以及某些程序单元上
// 应用在局部变量上
myString = (@NonNull String) str
// 应用在类声明上
@RestController
public class ContainerInstanceController {
}
// 多个注解应用在方法上
@AssistAnnotation
@RateLimiterAnnotation("CreateContainerGroup")
@RequestMapping(params = {"Action=CreateContainerGroup"})
public CreateContainerGroupResp createContainerInstance(@ContainerGroupParam CreateKciParam param) {
return containerInstanceService.createContainerInstance(param);
}
// 应用在字段声明上
@SkipMappingValueAnnotation
private String podUUID;
注解定义
在Java中通过@interface
来声明一个注解类型
public @interface 注解名称{
属性类型 属性名称() default 属性默认值;
}
如自定义注解RateLimiterAnnotation
public @interface RateLimiterAnnotation {
String value();
long ttl() default 0L;
}
由于interface在Java中是用于定义接口的,注解通过在interface前添加@
进行声明,因此其属性必须是方法式签名,而不是普通的属性字段,如果属性有默认值则通过紧随其后的default关键字定义。在Java中注解是一种特殊类型的interface,其Class中方法isInterface一定返回true。
public static void main(String[] args) {
System.out.println(RateLimiterAnnotation .class.isAnnotation()); // true
System.out.println(RateLimiterAnnotation .class.isInterface()); // true
}
所有的注解都默认隐式继承java.lang.annotation.Annotation。
元注解
应用到其它注解上的注解则称为元注解,这些元注解主要定义在java.lang.annotation中,主要包括:
- @Retention
- @Target
- @Inherited
- @Documented
- @Repeatable
元注解就是用来约束注解的注解,@Documented注解通常用于控制Javcdoc对注解类型的API文档生成,不用关注,定义注解时添加上就行。
保留策略
@Retention定义注解保留策略(生命周期概念)
RetentionPolicy.SOURCE
只保留在java源文件中,编译器编译后,class文件中不会保留。RetentionPolicy.CLASS
默认值,保留在class文件中,但是JVM运行时被忽略。RetentionPolicy.RUNTIME
保留在class文件中,JVM运行时也保留。
分别创建三种保留策略的注解,进行验证
@Retention(RetentionPolicy.SOURCE)
public @interface RetentionSourceAnnotation {
}
@Retention(RetentionPolicy.CLASS)
public @interface RetentionClassAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface RetentionRuntimeAnnotation {
}
创建个测试类,将三个注解都作用与类中的name字段上
public class TestAnnotationRetentionDemo {
@RetentionSourceAnnotation
@RetentionClassAnnotation
@RetentionRuntimeAnnotation
private String name;
}
编译TestAnnotationRetentionDemo源代码
javac -d target/classes -sourcepath src/main/java \
src/main/java/com/example/train/annotation/TestAnnotationRetentionDemo.java
通过idea等工具查看生成的字节码文件TestAnnotationRetentionDemo.class
public class TestAnnotationRetentionDemo {
@RetentionRuntimeAnnotation
@RetentionClassAnnotation
private String name;
}
字节码文件中,没有出现保留策略为RetentionPolicy.SOURCE
的注解RetentionSourceAnnotation,那如何验证@RetentionRuntimeAnnotation
与@RetentionClassAnnotation
的区别?
反编字节码文件
javap -v target/classes/com/example/train/annotation/TestAnnotationRetentionDemo.class
运行时通过反射验证
运行main方法,此时JVM运行起来了,通过反射获取字段name上的注解信息并打印public static void main(String[] args) throws NoSuchFieldException { // 通过反射获取字段信息 Field name = TestAnnotationRetentionDemo.class.getDeclaredField("name"); // 获取作用于name字段上的全部注解信息 Annotation[] annotations = name.getDeclaredAnnotations(); if (annotations != null && annotations.length > 0) { for (Annotation annotation : annotations) { System.out.println(annotation.toString()); } } }
最终只输出了
@com.example.train.annotation.RetentionRuntimeAnnotation()
三种保留策略应用场景是什么?
RetentionPolicy.SOURCE
这种保留策略的注解使用很广泛,经常用于开发一些工具插件、Java类库,作用就是用来自动生成代码。
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}
将注解Setter应用到如下包含非常多属性字段的Student类上,我们不希望直接在源代码中直接编写大量的setXxx方法,那样不容易维护,代码量也非常庞大。
@Setter
public class Student {
private String name;
private int age;
...
}
我们希望使用Javac编译代码后,字节码文件中能自动生成每个字段的set方法,如下Student.class内容代码。
public class Student {
private String name;
private int age;
...
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
...
}
在Java中有一种注解处理技术,Annotation Processing Tool(APT)
,它可以在编译期基于源代码文件,生成新的代码内容,编译完成后最终输出满足要求的字节码文件(偷梁换柱)。基于javax.annotation.processing.AbstractProcessor编写一个自定义的注解处理器Processor,在javac编译代码时,将编写好的注解处理器进行注册使用即可,使用这种注解技术的框架类库非常多,如JPA
,Project Lombok
。
参考资料:
https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
https://www.baeldung.com/java-annotation-processing-builder
RetentionPolicy.CLASS
那这种保留策略的注解使用范围肯定比RetentionPolicy.SOURCE更广,除了上面说的通过API自动生成代码,这种类型注解还保留在class文件中,因此可以通过编写自定义ClassLoader来定制化处理字节码内容,最终生成Class元数据。
RetentionPolicy.RUNTIME
通常我们定义的处理业务逻辑的注解都应该是这种保留策略,因为应用必须在JVM中运行起来了,才能实现我们的逻辑处理。
作用范围
@Target
用于约束注解的使用范围,即可以标注在哪些目标元素上。
Target的作用范围,请参考java.lang.annotation.ElementType
。
- 如果定义注解时不指定Target,则注解可以作用于类中各个元素上
- 常用的Target取值是类、构造方法、普通方法、字段
- 元注解是用来约束其它注解的,因此元注解的作用目标范围是注解类型,即
ElementType.ANNOTATION_TYPE
定义注解时,使用元注解@Target
限定了作用范围,那编译器会严格检查,不符合时会编译错误。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetMethodAnnotation {
}
TargetMethodAnnotation的作用目标是方法上,直接作用于Class上,会编译错误。
// 编译错误,@TargetMethodAnnotation not applicable to type
@TargetMethodAnnotation
public class TestAnnotationTargetDemo {
}
注解的Target应该使用ElementType.Type
,才能作用于类上,或者Target指定多个目标范围。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface TargetMethodAnnotation {
}
继承
@Inherited
标记的注解表明注解类型可以从父类继承,通过Class中方法getAnnotations方法获取从父类中继承的注解。
// 有@Inherited注解,表示该注解可以从父类继承
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritedAnnotation {
}
// 不可继承的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NonInheritedAnnotation {
}
将两个注解分别作用于父类声明上
// 父类
@InheritedAnnotation
@NonInheritedAnnotation
public class TestAnnotationInheritedParent {
}
public class TestAnnotationInheritedChild extends TestAnnotationInheritedParent {
}
通过反射获取TestAnnotationInheritedChild上注解,注意: 反射是JVM运行中才能用的技术,因此注解的保留策略必须是Retention.RUNTIME
。
// com.example.train.annotation.TestAnnotationInheritedChild
public static void main(String[] args) {
// 通过.class语法获取Class对象
Class clazz = TestAnnotationInheritedChild.class;
// 获取注解,包括从父类继承的注解
Annotation[] annotations = clazz.getAnnotations();
if (annotations != null && annotations.length > 0) {
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
程序最终输出 @com.example.train.annotation.InheritedAnnotation()
,@Inherited
类型的注解可以从直接父类或者间接父类中继承。
重复注解
通过元注解@Repeatable
修饰的注解,可以多次作用于某个具体的元素类型上。
场景:自定义一个定时调度任务的注解@Schedule
,现在有个任务期望它周期性的在每月第一天执行,同时天23点执行。
public class TaskSchedule {
@Schedule(dayOfMonth = 1) // 每月第一天执行
@Schedule(hour = 23) // 每天23点执行
public void autoCleanResource() {
}
}
@Schedule
注解定义如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Schedule {
int dayOfMonth() default 1;
int hour() default 0;
}
上面这个注解@Schedule
就是一个可重复作用于方法上的注解,这种场景在应用开发中非常常见,在Java8之前,编译器会自动处理,但是Java8之后要求严格,必须自定义一个容器注解来存储可重复作用的注解,否则编译TaskSchedule类时,编译器会报错:Schedule dose not have a valid Repeatable annotation。
如何定义容器注解?
通常按规范在Schedule名称后以复数形式命名,即Schedules
,注意@Retention、@Target必须与注解@Schedule保持一致,然后内部有一个value属性,类型则是@Schedule的数组形式。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Schedules {
Schedule[] value();
}
然后修改@Shcedule
注解,通过元注解@Repeatable
明确其容器注解是@Schedules
。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Schedules.class)
@Documented
@Inherited
public @interface Schedule {
int dayOfMonth() default 1;
int hour() default 0;
}
TaskSchedule此时编译不会有任何错误提示。
public class TaskSchedule {
@Schedule(dayOfMonth = 1)
@Schedule(hour = 23)
public void autoCleanResource() {
}
}
查看编译后的代码TaskSchedule.class,内容如下
public class TaskSchedule {
public TaskSchedule() {
}
@Schedules({@Schedule(
dayOfMonth = 1
), @Schedule(
hour = 23
)})
public void autoCleanResource() {
}
}
自定义注解实践
在应用开发中,我们经常定义的注解保留策略通常是RetentionPolicy.RUNTIME
,然后在程序运行过程中,通过反射API中的一些方法可以检索获取类、方法或字段上的注解信息,获取注解信息的API方法主要由java.lang.reflect.AnnotatedElement接口定义,而Class、Field、Method则是AnnotatedElement的方法实现者。
获取元素注解API
获取注解相关的主要方法
// 获取元素直接声明的注解,不包括继承的注解信息
Annotation[] getDeclaredAnnotations();
// 获取元素注解,包括从父类继承的注解信息
Annotation[] getAnnotations();
// 获取元素指定类型的注解,包括从父类继承的注解信息
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 获取元素直接指定类型的注解,不包括从父类继承的注解信息
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass){
...
}
// 这个方法与getAnnotation区别就是Repeatable注解,该方法会去容器注解查找
// 而getAnnotation(Schedule.class)直接返回null
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
...
}
// 与getDeclaredAnnotation类似,多了Repeatable注解的查找
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
...
}
// 判断元素上是否存在指定类型的注解,包括父类上注解
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
...
}
现在主要验证下getAnnotation(Class
public class TestAnnotationReflectApi {
public static void main(String[] args) throws NoSuchMethodException {
// 通过.class获取Class对象
Class clazz = TaskSchedule.class;
// 获取autoCleanResource方法信息
Method method = clazz.getDeclaredMethod("autoCleanResource");
System.out.println("====getDeclaredAnnotations 打印开始");
Annotation[] annotations = method.getDeclaredAnnotations();
print(annotations);
System.out.println("====getDeclaredAnnotations 打印结束");
System.out.println("====getAnnotation 开始打印");
Schedule schedule = method.getAnnotation(Schedule.class);
if (Objects.nonNull(schedule)) {
System.out.println(schedule.toString());
}
System.out.println("====getAnnotation 打印结束");
annotations = method.getAnnotationsByType(Schedule.class);
System.out.println("====getAnnotationsByType 打印开始");
print(annotations);
System.out.println("====getAnnotationsByType 打印结束");
}
private static void print(Annotation[] annotations) {
if (annotations != null && annotations.length > 0) {
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
}
程序运行后,输出如下日志
====getDeclaredAnnotations 打印开始
@com.example.train.annotation.Schedules(value=[@com.example.train.annotation.Schedule(hour=0, dayOfMonth=1), @com.example.train.annotation.Schedule(hour=23, dayOfMonth=1)])
====getDeclaredAnnotations 打印结束
====getAnnotation 开始打印
====getAnnotation 打印结束
====getAnnotationsByType 打印开始
@com.example.train.annotation.Schedule(hour=0, dayOfMonth=1)
@com.example.train.annotation.Schedule(hour=23, dayOfMonth=1)
====getAnnotationsByType 打印结束
总结:重复注解需要使用getAnnotationsByType
方法获取。
自定义注解应用
自定义一个作用于字段上的注解,辅助程序对Bean字段内容长度检查。
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.FIELD)
public @interface LengthLimitAnnotation {
int max() default 1000;
int min() default 0;
}
将LengthLimitAnnotation应用到Container类上
public class Container {
@LengthLimitAnnotation(max = 20, min = 1)
private String name;
@LengthLimitAnnotation(max = 50, min = 5)
private String image;
private String restartPolicy;
// get/set方法省略
...
}
创建Container实例对象
Container container = new Container();
container.setImage("http://hub.registry.example.com/example/nginx:v1.0");
container.setName("2345436523456543456754323456543234565434565434");
程序通过对象字段上的注解进行属性内容检查,是否在注解约束的范围内。
public class TestLengthLimitAnnotation {
public static void main(String[] args) throws IllegalAccessException {
Container container = new Container();
container.setImage("http://hub.registry.example.com/example/nginx:v1.0");
container.setName("2345436523456543456754323456543234565434565434");
// 获取class对象
Class clazz = container.getClass();
// 获取对象所有字段
Field[] fields = clazz.getDeclaredFields();
if (fields == null || fields.length == 0) {
return;
}
// 检查字段长度是否符合范围
for (Field f : fields) {
checkFieldLength(f, container);
}
}
private static void checkFieldLength(Field field, Object obj) throws IllegalAccessException {
if (Objects.isNull(field)) {
return;
}
// 获取字段上标记的注解LengthLimitAnnotation
LengthLimitAnnotation limitAnnotation = field.getDeclaredAnnotation(LengthLimitAnnotation.class);
// 没有注解,表示该字段无须进行内容长度检查
if (Objects.isNull(limitAnnotation)) {
return;
}
// 如果私有属性你,强制访问
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
// 获取字段值
String value = (String) field.get(obj);
int valueLen = 0;
if (Objects.nonNull(value) && value.length() > 0) {
valueLen = value.length();
}
// 获取字段上注解约束的范围长度
int max = limitAnnotation.max();
int min = limitAnnotation.min();
String name = field.getName();
// 不满足要求,抛出运行时异常
if (valueLen > max) {
throw new RuntimeException(name + " length can't exceed " + max);
}
if (valueLen < min) {
throw new RuntimeException(name + " length can't less than " + min);
}
}
}
程序运行,抛出错误
Exception in thread "main" java.lang.RuntimeException: name length can't exceed 20
at com.example.train.annotation.TestLengthLimitAnnotation.checkFieldLength(TestLengthLimitAnnotation.java:56)
at com.example.train.annotation.TestLengthLimitAnnotation.main(TestLengthLimitAnnotation.java:26)