
Java核心知识体系2:注解机制详解
1 Java注解基础
注解是JDK1.5版本开始引入的一个特性,用于对程序代码的说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
它主要的作用有以下四方面:
- 生成javadoc文档,通过在代码里面标识元数据生成javadoc文档。
- 编译期的检查,通过标识的元数据让编译器在编译期间对代码进行验证。
- 编译时动态处理,编译时通过代码中标识的元数据动态处理,比如动态生成代码。
- 运行时动态处理,运行时通过代码中标识的元数据动态处理,比如使用反射技术注入实例。
注解的常见分类有三种:
- Java自带的标准注解,包括 @Override、@Deprecated和@SuppressWarnings,分别代表 方法重写、某个类或方法过时、以及忽略警告,用这些注解标明后编译器就会进行检查。
- 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented 等6种
- @Retention:指定其所修饰的注解的保留策略
- @Document:该注解是一个标记注解,用于指示一个注解将被文档化
- @Target:用来限制注解的使用范围
- @Inherited:该注解使父类的注解能被其子类继承
- @Repeatable:该注解是Java8新增的注解,用于开发重复注解
- 类型注解(Type Annotation):该注解是Java8新增的注解,可以用在任何用到类型的地方
- 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
接下来我们通过这三种分类来逐一理解注解。
1.1 Java内置注解
我们先从Java内置注解开始说起,先看下下面的代码:
Java 1.5开始自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings:
-
@Override
:表示当前类中的方法定义将覆盖父类中的方法 -
@Deprecated
:表示该代码段被弃用,但是可以使用,只是编译器会发出警告而已 -
@SuppressWarnings
:表示关闭编译器的警告信息
我们再具体看下这几个内置注解,同时通过这几个内置注解中的元注解的定义来引出元注解。
1.1.1 内置注解 - @Override
我们先来看一下这个注解类型的定义:
从它的定义我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,
若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
1.1.2 内置注解 - @Deprecated
这个注解的定义如下:
从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。
1.1.3 内置注解 - @SuppressWarnings
这个注解我们也比较常用到,先来看下它的定义:
它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:
参数 | 作用 | 原描述 |
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制装箱、拆箱操作时候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制映射相关的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制启用注释的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制过期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制确在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模块没有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制与隐藏变数的区域变数相关的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略没有完整的switch语句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字符 | to suppress warnings relative to non-nls string literals |
null | 忽略对null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics时忽略没有指定相应的类型 | to suppress warnings relative to un-specific types when using |
restriction | 抑制与使用不建议或禁止参照相关的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable类中没有声明serialVersionUID变量 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正确的静态访问方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制没有进行类型检查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制没有权限访问的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制没被使用过的代码的警告 | to suppress warnings relative to unused code |
1.2 元注解
上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:@Target,@Retention,@Documented,@Inherited, 在JDK 1.8中提供了两个新的元注解 @Repeatable和@Native。
1.2.1 元注解 - @Target
Target注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方) 。
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:
- packages、types(类、接口、枚举、注解类)
- 类成员(方法、构造方法、成员变量、枚举值)
- 方法参数和本地变量(如循环变量、catch参数)
在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中。枚举信息如下:
1.2.2 元注解 - @Retention & @RetentionTarget
Reteniton注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时) 。
Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中。枚举如下:
我们测试下这三种策略,在定义注解类的时候什么区别:
上面已经定义好了三个注解类,我们再用这三个注解类再去注解方法,如下:
通过执行 javap -verbose RetentionTest命令获取到的RetentionTest 的 class 字节码内容如下。
从 RetentionTest 的字节码内容我们可以得出以下两点结论:
- 编译器并没有记录下 sourcePolicy() 方法的注解信息
- 编译器使用 RuntimeInvisibleAnnotations 去记录 classPolicy()方法的注解信息
- 编译器使用 RuntimeVisibleAnnotations 去记录 runtimePolicy()方法的注解信息
1.2.3 元注解 - @Documented
Documented注解的作用如下:使用 javadoc 工具为类生成帮助文档,并确认是否保留注解信息。
以下代码在使用Javadoc工具可以生成 @DocAnnotation注解信息。
1.2.4 元注解 - @Inherited
Inherited注解的作用:被它修饰的Annotation将具有继承特性。父类使用了被@Inherited修饰的Annotation,则子类将自动具备该注解。
我们来测试下这个注解:
- 定义@Inherited注解:
- 使用这个注解
- 输出
虽然Customer类没有显示地被注解@InheritedAnnotation,但是它的父类UserInfo被注解,而且@InheritedAnnotation被@Inherited注解,因此Customer类自动继承注解
1.2.4 元注解 - @Repeatable (Java8)
Repeatable是可重复使用的意思,允许在同一声明的类型(类,属性,或方法)中,可以多次使用同一个注解
JDK8之前要想实现注解重复使用,需要组合模式,编写和可读性都不是很好
由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。
Java 8中的做法:
不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点
1.2.5 元注解 - @Native (Java8)
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
1.3 注解与反射接口
定义注解后,如何获取注解中的内容呢?反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。我们看下具体的相关接口
- boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器。 - T getAnnotation(Class annotationClass)
返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。 - Annotation[] getAnnotations()
返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组。 - T[] getAnnotationsByType(Class annotationClass)
返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。getAnnotationsByType方法与 getAnnotation的区别在于,getAnnotationsByType会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。 - T getDeclaredAnnotation(Class annotationClass)
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回null - T[] getDeclaredAnnotationsByType(Class annotationClass)
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释 - Annotation[] getDeclaredAnnotations()
返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。
1.4 自定义注解
当我们理解了内置注解, 元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来, 代码很简单:
- 定义自己的注解
- 使用注解
- 用反射接口获取注解信息
在 FruitInfoUtil 中进行测试:
- 测试的输出
2 理解注解的原理
2.1 Java 8 提供了哪些新的注解
- @Repeatable
- ElementType.TYPE_USE
- ElementType.TYPE_PARAMETER
ElementType.TYPE_USE(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查)包含了ElementType.TYPE(类、接口(包括注解类型)和枚举的声明)和ElementType.TYPE_PARAMETER(类型参数声明), 不妨再看个例子
2.2 注解支持继承吗?
注解是不支持继承的
不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口. 虽然反编译后发现注解继承了Annotation接口,请记住,即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface。区别于注解的继承,被注解的子类继承父类注解可以用@Inherited:如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。
3 注解的应用场景
自定义注解和AOP - 通过切面实现解耦
笔者曾经在 《基于AOP的动态数据源切换》 这篇文章中有个典型的例子,就是使用AOP切面来对多数据源进行使用场景的切换,下面展示下如何通过注解实现解耦的。
- 自定义Annotation,映射的目标范围为 类型和方法。
- 编写AOP实现,切面代码,以实现对注解的PointCut,切点拦截
- 测试,在Control中写三个测试方法
- 执行效果
除此之外,我们可以看到很多日志管理、权限管理,也都是也是通过类似的注解机制来实现的,通过注解+AOP来最终实现模块之间的解耦,以及业务与系统层面的解耦 。
文章转载自公众号: 架构与思维
