注解
1.注解概述
1.1 注解的概念
-
注解是JDK1.5的新特性。
-
注解相当一种标记,是类的组成部分,可以给类携带一些额外的信息。
-
标记(注解)可以加在包,类,字段,方法,方法参数以及局部变量上。
-
注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能。
-
注解(Annotation)相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无何种标记,看你的程序有什么标记,就去干相应的事,标记可以加在包、类,属性、方法,方法的参数以及局部变量上。
1.2 Java的注解可以分为三类
- 第一类是由编译器使用的注解
例如:- @Override:让编译器检查该方法是否正确地实现了覆写;
- @SuppressWarnings:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。
-
第二类是由工具处理.class文件使用的注解
比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。 -
第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。
例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为
value
的配置参数,对此参数赋值,可以只写常量,相当于省略了value
参数。如果只写注解,相当于全部使用默认值。
举个栗子,对以下代码:
public class Hello { @Check(min=0, max=100, value=55) public int n; @Check(value=99) public int p; @Check(99) // @Check(value=99) public int x; @Check public int y; }
@Check
就是一个注解。第一个@Check(min=0, max=100, value=55)
明确定义了三个参数,第二个@Check(value=99)
只定义了一个value参数,它实际上和@Check(99)
是完全一样的。最后一个@Check
表示所有参数都使用默认值。
1.3 注解的作用
注解的作用就是给程序带入参数。
以下几个常用操作中都使用到了注解:
-
生成帮助文档:@author和@version
-
@author:用来标识作者姓名。
-
@version:用于标识对象的版本号,适用范围:文件、类、方法。
- 使用@author和@version注解就是告诉Javadoc工具在生成帮助文档时把作者姓名和版本号也标记在文档中。如下图:
-
-
编译检查:@Override
-
@Override:用来修饰方法声明。
- 用来告诉编译器该方法是重写父类中的方法,如果父类不存在该方法,则编译失败。如下图
-
-
框架的配置(框架=代码+配置)
1.4 元注解
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
最常用的元注解是@Target
。使用@Target
可以定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:ElementType.TYPE;
- 字段:ElementType.FIELD;
- 方法:ElementType.METHOD;
- 构造方法:ElementType.CONSTRUCTOR;
- 方法参数:ElementType.PARAMETER。
例如,定义注解@Report
可用在方法上,我们必须添加一个@Target(ElementType.METHOD)
:
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
定义注解@Report
可用在方法或字段上,可以把@Target
注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }
:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
实际上@Target
定义的value
是ElementType[]
数组,只有一个元素时,可以省略数组的写法。
@Retention
另一个重要的元注解@Retention
定义了Annotation
的生命周期:
- 仅编译期:RetentionPolicy.SOURCE;
- 仅class文件:RetentionPolicy.CLASS;
- 运行期:RetentionPolicy.RUNTIME。
如果@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable
使用@Repeatable
这个元注解可以定义Annotation
是否可重复。这个注解应用不是特别广泛。
@Repeatable
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
经过@Repeatable
修饰后,在某个类型声明处,就可以添加多个@Report
注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
使用@Inherited
定义子类是否可继承父类定义的Annotation
。@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了@Report
:
@Report(type=1)
public class Person {
}
则它的子类默认也定义了该注解:
public class Student extends Person {
}
2.自定义注解
2.1 自定义注解的格式
public @interface 注解名{
}
如:定义一个名为Student的注解
public @interface Student {
}
2.2 注解的属性
-
属性的格式
- 格式1:数据类型 属性名();
- 格式2:数据类型 属性名() default 默认值;
-
属性定义示例
// 姓名 String name(); // 年龄 int age() default 18; // 爱好 String[] hobby();
-
属性适用的数据类型
* 八种数据数据类型(int,short,long,double,byte,char,boolean,float) * String,Class,注解类型,枚举类 * 以上类型的数组形式
2.3 自定义注解的步骤
我们总结一下定义Annotation的步骤:
- 第一步,用@interface定义注解:
public @interface Report {
}
- 第二步,添加参数、默认值:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。
- 第三步,用元注解配置注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
其中,必须设置@Target
和@Retention
,@Retention
一般设置为RUNTIME
,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited
和@Repeatable
。
3.使用自定义注解
3.1 定义注解
- 定义一个注解:Book
- 包含属性:String value() 书名
- 包含属性:double price() 价格,默认值为 100
- 包含属性:String[] authors() 多位作者
- 代码实现
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {
String value();
double price() default 100;
String[] authros();
}
3.2 使用注解
- 定义类在成员方法上使用Book注解
@Book(value = "mybook", authors = {"张三","李四"})
public class AnnotationDemo01 {
}
- 使用注意事项
- 如果属性有默认值,则使用注解的时候,这个属性可以不用赋值。
- 如果属性没有默认值,那么在使用注解时一定要给属性赋值。
4.读取注解
Java的注解本身对代码逻辑没有任何影响。根据@Retention
的配置:
- SOURCE类型的注解在编译期就被丢掉了;
- CLASS类型的注解仅保存在class文件中,它们不会被加载进JVM;
- RUNTIME类型的注解会被加载进JVM,并且在运行期可以被程序读取。
如何使用注解完全由工具决定。SOURCE
类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS
类型的注解主要由底层工具库使用,涉及到class
的加载,一般我们很少用到。只有RUNTIME
类型的注解不但要使用,还经常需要编写。
因此,我们只讨论如何读取RUNTIME
类型的注解。
因为注解定义后也是一种class
,所有的注解都继承自java.lang.annotation.Annotation
,因此,读取注解,需要使用反射API。
Java提供的使用反射API读取Annotation
的方法包括:
判断某个注解是否存在于Class
、Field
、Method
或Constructor
:
Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
例如:
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
使用反射API读取Annotation
:
- Class.getAnnotation(Class)
- Field.getAnnotation(Class)
- Method.getAnnotation(Class)
- Constructor.getAnnotation(Class)
例如:
// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
使用反射API读取Annotation
有两种方法。
-
方法一是先判断
Annotation
是否存在,如果存在,就直接读取:Class cls = Person.class; if (cls.isAnnotationPresent(Report.class)) { Report report = cls.getAnnotation(Report.class); ... }
-
第二种方法是直接读取
Annotation
,如果Annotation
不存在,将返回null
:Class cls = Person.class; Report report = cls.getAnnotation(Report.class); if (report != null) { ... }
读取方法、字段和构造方法的Annotation
和Class
类似。但要读取方法参数的Annotation
就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
要读取方法参数的注解,我们先用反射获取Method
实例,然后读取方法参数的所有注解:
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}
5.注解案例
5.1 案例说明
模拟Junit测试的@Test
5.2 案例分析
- 模拟Junit测试的注释@Test,首先需要编写自定义注解@MyTest,并添加元注解,保证自定义注解只能修饰方法,且在运行时可以获得。
- 然后编写目标类(测试类),然后给目标方法(测试方法)使用 @MyTest注解,编写三个方法,其中两个加上@MyTest注解。
- 最后编写调用类,使用main方法调用目标类,模拟Junit的运行,只要有@MyTest注释的方法都会运行。
5.3 案例代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}
public class TestMyTest {
@MyTest
public void tests01(){
System.out.println("test01");
}
public void tests02(){
System.out.println("test02");
}
@MyTest
public void tests03(){
System.out.println("test03");
}
}
public class AnnotationDemo05 {
public static void main(String[] args) throws Exception {
// 获得Class对象
Class c = TestMyTest.class;
Object obj = c.newInstance();
// 获得所有成员方法
Method[] methods = c.getMethods();
for(Method m:methods){
// 判断m方法是否使用了MyTest注解
if(m.isAnnotationPresent(MyTest.class)){
// 调用方法
m.invoke(obj);
}
}
}
}