说起Java注解,可能大家并不陌生。但是注解的意义是什么呢?到底是干嘛的?我们又如何自定义注解?可能有的同学(指我自己)已经做过很长时间Java开发,甚至在Spring Boot中天天使用各种注解,但是却不知道注解的意义和机制。
今天就对Java的注解来一个总结和分享。
1,注解是干嘛的?
Java注解(Annotation)是一种用于向Java程序中添加元数据的标记,它们是Java SE 5.0版本引入的。
就像我们在日常生活中给各个物品分类,或者是整理的时候,我们会对物品贴标签一样。
例如我们在初高中做化学实验的时候,我们可以看到化学实验室中有着各种各样存放着化学试剂的瓶瓶罐罐,以及各种容器,不过在实验室中,每一个装着化学试剂的瓶子上都贴着标签,标注着其中的化学试剂是什么,有了这些标签,我们可以快速、正确地找到我们所需要的化学试剂,完成我们想要的操作。
事实上,在Java中注解就是标签,只不过,注解是给Java的类、字段、方法或者方法参数贴的标签而已。
这样,可以在程序运行或者编译代码的时候找到这些“标签”,并根据需求完成相应的操作。
相信大多数同学都使用过Spring框架以及Spring Boot进行开发,我们常常使用@Component
注解标注类,使其在IoC容器初始化的时候,把这些类实例化为Bean
并注册到容器中。Spring框架怎么知道要把哪些类实例化为Bean
呢?就是通过去“看”哪些类上面有相关的注解(@Component
、@Service
等等)。可见打上了这些注解,就像是给这个类贴上了个标签,Spring框架在启动时,就可以找到这些“贴了标签”的类了!
2,注解的类型
在Java中,注解也分为几种类型,我们常常从注解的作用目标类型和注解的生命周期两个角度来认识注解的分类。
(1) 作用目标类型
注解的作用目标类型其实就指的是这个注解是标注在什么东西上面的。
前面我们把注解比作标签,那就可以说作用目标类型就表示这个标签贴在什么上面。在Java中,我们常常把“标签”贴在类、方法、成员变量和方法参数上。
所以说,注解根据作用目标类型分为下面几种类型:
- 类型注解:标注在一个类上的注解,例如Spring框架中的
@Component
注解 - 字段注解:标注在类中成员变量上的注解,例如Spring框架常常使用的
@Autowired
自动装配注解 - 方法注解:标注在类中方法上的注解,例如
@Override
标记的方法就表示这个方法是重写了父类的方法(或者实现接口方法的方法) - 参数注解:标注在一个方法形参上的注解,例如Spring MVC中,我们用
@RequestParam
注解表示一个参数是需要在请求URL中获取的 - 构造方法注解:标注在类中构造函数上的注解,例如
@Autowired
自动装配注解也可以用在构造函数上,使得Spring框架通过这个类的构造函数完成依赖注入 - 元注解:标注在注解类上的注解,例如下面我们自定义注解时会接触到的
@Target
注解,用于指定这个自定义注解的作用目标类型
(2) 生命周期
贴好了标签,我们当然也需要在适当的时候去识别或者读取标签。
所以注解的生命周期指的是在程序的哪个阶段可以读取和处理这个注解的信息,换句话讲,就是什么时候去读取这个标签。在Java中,我们常常在程序运行或者编译的时候去读取“标签”。
所以,根据生命周期我们将注解分为下面几类:
- 源代码注解:这种注解只是存在于源代码中,编译后不会保留
- 编译时注解:在编译时生效,并被编译器读取的注解,例如
@Deprecated
注解表示某个方法是弃用的方法,这样编译器编译时会显示警告 - 运行时注解:在程序运行时生效,这可能也是我们见得最多的一类注解了,例如
@Component
注解就是在Spring框架运行的时候去读取到的,通常我们通过反射的方式去读取运行时注解
3,自定义一个注解
我们首先需要定义注解类,通过@interface
关键字去定义注解类。
例如我现在想定义一个类型注解,这个注解用于标注一个类的中文名。(可以说我现在想做个标签,标签贴在类上面,标签上写这个类的中文名)
定义注解类如下:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* 注解类的属性:中文名
*/
String chineseName();
}
这样,我们就完成了一个自定义的注解了!上述代码有下列要点:
- 通过
@Target
注解标注我们这个自定义注解的作用目标类型,传入ElementType
枚举,其中:ElementType.TYPE
作用于类、接口、枚举类上的注解ElementType.FIELD
作用于类的属性上ElementType.METHOD
作用于方法上ElementType.PARAMETER
作用于方法的参数ElementType.CONSTRUCTOR
作用于构造函数ElementType.ANNOTATION_TYPE
作用于注解类
- 通过
@Retention
注解标注我们这个自定义注解的生命周期,传入RetentionPolicy
枚举,其中:RetentionPolicy.SOURCE
在源码层面,作为纯注释RetentionPolicy.CLASS
在编译阶段有效RetentionPolicy.RUNTIME
在运行阶段有效
- 注解类也是一个类,所以注解类和其它类一样可以有自己的属性(字段),上述注解类中就有一个属性
chineseName
,只不过注解中属性要写成方法的形式
需要说明的是,自定义的注解作用目标类型可以是多个,例如我需要上述注解既能够标注在类上面也可以标注在方法上,就将@Target
中参数改如下:
@Target({
ElementType.TYPE, ElementType.METHOD})
4,读取注解
在运行的时候,我们通过反射的方式读取注解。
(1) 读取类注解
我们新建两个类,并将我们上述自定义注解标注上去试试:
@Name(chineseName = "人类")
public class Person {
// 省略内容
}
@Name(chineseName = "猫类")
public class Cat {
// 省略内容
}
好的,我们现在就给这两个类“贴好标签”了!并且分别在标签的chineseName
这一栏写上了“人类”和“猫类”。
现在在主方法就可以读取这个注解及其内容:
package com.gitee.swsk33.annotationdemo;
import com.gitee.swsk33.annotationdemo.annotation.Name;
import com.gitee.swsk33.annotationdemo.model.Cat;
import com.gitee.swsk33.annotationdemo.model.Person;
public class Main {
public static void main(String[] args) {
// 获取Person和Cat上面的注解以及其属性内容
Name personAnnotation = Person.class.getAnnotation(Name.class);
Name catAnnotation = Cat.class.getAnnotation(Name.class);
// 读取两个注解的属性
System.out.println(personAnnotation.chineseName());
System.out.println(catAnnotation.chineseName());
}
}
结果:
可见获取一个类上的注解很简单,通过得到类的Class
对象,调用其getAnnotation
方法即可,方法中传入要读取的注解的类型即可,上述就要读取两个类上标注的Name
注解,因此传入这个类型。
事实上,在Java中我们对类和对象的概念非常熟悉了,在上述我们声明了注解类Name
,然后在Person
和Cat
类上面标注了注解,这里Person
和Cat
类上面标注的注解就是Name
注解类的实例(对象)。
(2) 读取方法注解
读取方法注解的方式也不难,我们通过反射的方式获取到一个类中所有的方法对象,就可以获取其注解了!
假设现在我想给类中的方法“贴标签”并写上该方法的中文名呢?
首先把上述Name
注解的@Target
属性改成方法注解的,如下:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* 注解类的属性:中文名
*/
String chineseName();
}
然后定义猫类如下,并在其方法上标注注解:
package com.gitee.swsk33.annotationdemo.model;
import com.gitee.swsk33.annotationdemo.annotation.Name;
public class Cat {
@Name(chineseName = "奔跑方法")
public void run() {
System.out.println("run");
}
@Name(chineseName = "吃饭方法")
public void eat() {
System.out.println("eat");
}
public void sleep() {
System.out.println("sleep");
}
}
然后在主类中,通过反射的方式获取猫类中所有的方法对象,再根据方法对象获取注解:
package com.gitee.swsk33.annotationdemo;
import com.gitee.swsk33.annotationdemo.annotation.Name;
import com.gitee.swsk33.annotationdemo.model.Cat;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
// 获取Cat类所有方法
Method[] methods = Cat.class.getDeclaredMethods();
// 遍历方法并获取到方法上的Name注解
for (Method method : methods) {
// 获取该方法上标注的Name类型注解
Name methodNameAnnotation = method.getAnnotation(Name.class);
if (methodNameAnnotation == null) {
System.out.println("方法" + method.getName() + "上面没有注解!");
continue;
}
// 读取注解对象的chineseName属性值
System.out.println("方法" + method.getName() + "的中文名是:" + methodNameAnnotation.chineseName());
}
}
}
运行结果:
可见这和获取类上的注解是一样的,反射得到的方法Method
对象也是有getAnnotation
方法的,如果某个方法上面没有这种类型的注解,就会返回null
。
(3) 读取字段注解
假设现在我想给类中的字段“贴标签”并写上该字段的中文名呢?
同样地,修改Name
注解类的@Target
,使其作用于字段:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* 注解类的属性:中文名
*/
String chineseName();
}
然后定义猫类如下:
package com.gitee.swsk33.annotationdemo.model;
import com.gitee.swsk33.annotationdemo.annotation.Name;
public class Cat {
@Name(chineseName = "猫的名字")
private String name;
@Name(chineseName = "猫的品种")
private String type;
private int age;
}
在主类中,通过反射的方式获取字段对象并获取注解即可,这和读取方法注解类似:
package com.gitee.swsk33.annotationdemo;
import com.gitee.swsk33.annotationdemo.annotation.Name;
import com.gitee.swsk33.annotationdemo.model.Cat;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
// 获取Cat类所有字段
Field[] fields = Cat.class.getDeclaredFields();
// 遍历所有字段并获取字段上的Name注解
for (Field field : fields) {
// 获取该字段上标注的Name类型注解
Name fieldAnnotation = field.getAnnotation(Name.class);
if (fieldAnnotation == null) {
System.out.println("字段" + field.getName() + "上面没有注解!");
continue;
}
// 读取注解对象的chineseName属性值
System.out.println("字段" + field.getName() + "的中文名是:" + fieldAnnotation.chineseName());
}
}
}
结果:
可见,获取注解的方式并不难,通过反射得到对应的类、方法、字段对象,再调用getAnnotation
方法即可!
事实上,获取构造函数注解等也是一样的,这里不再赘述了。
5,注解属性赋值
上述在打注解的时候,我们也同时设定了对应的属性值,就像是在贴标签时在上面写上相应的内容一样。
注解的属性赋值还有一些要点,我们可以注意一下。
(1) 设定属性默认值
假设在上述标注@Name
注解时,我们没有指定其中chineseName
的话,就会报错:
事实上,我们可以给注解的chineseName
属性指定默认值,修改Name
注解类如下:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* 注解类的属性:中文名
*/
String chineseName() default "没名字";
}
在chineseName
属性后面加上default
可以指定当不设定chineseName
属性值时的默认值,上述就设定了默认值没名字
。
这时,标注注解不设定chineseName
属性的值,也不会报错:
这样的话,如果读取这个name
字段上面的注解的chineseName
属性时,那就会得到没名字
。
(2) 多属性值设定
假设再在@Name
注解类中加一个字段englishName
如下:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* 注解类的属性:中文名
*/
String chineseName() default "没名字";
/**
* 注解类属性:英文名
*/
String englishName() default "undefined";
}
那么使用这个注解的时候,就可以同时指定两个属性值:
package com.gitee.swsk33.annotationdemo.model;
import com.gitee.swsk33.annotationdemo.annotation.Name;
public class Cat {
// 省略其它字段
@Name(chineseName = "猫的品种", englishName = "catType")
private String type;
}
注解后面括号中,通过属性名 = 值
的形式指定即可,多个属性之间使用英文逗号,
隔开。
(3) 默认属性
大家发现,我们自定义的注解使用时,传入属性值需要指定属性名,但是为什么@Target
注解就可以省略属性名呢?
可以看看@Target
注解源码:
相信大家能够恍然大悟了:在注解中可以指定一个默认属性叫做value
,如果注解类中有且只有这个默认属性,那么传入值的时候就可以省略属性名直接传值了!
修改上述@Name
注解类如下:
package com.gitee.swsk33.annotationdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Name的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
String value();
}
然后使用时就可以这么写了:
@Name("猫的品种")
private String type;
这等效于:
@Name(value = "猫的品种")
private String type;
当然,获取到这个注解对象的时候,也只需要读取value
属性:
// 获取注解对象
Name fieldAnnotation = field.getAnnotation(Name.class);
// 读取注解的value属性值
System.out.println("字段" + field.getName() + "的中文名是:" + fieldAnnotation.value());
(4) 数组属性传值
我们再仔细观察一下@Target
注解,发现其value
属性是数组类型的:
那为什么传入参数的时候可以不传数组呢?
这是因为如果你传入的数组中只有一个元素,那就可以直接写你传入的元素即可!
例如@Target(ElementType.FIELD)
,我们传入的数组只有ElementType.FIELD
这个元素,那么就不用写成数组形式了!当然,这和@Target({ ElementType.FIELD })
是等效的。