B站视频链接:https://www.bilibili.com/video/BV1p4411P7V3/?p=3
1、注解简介
Annotation是jdk5.0
开始引入的新技术。注解的格式:@注解名
,比如@Override
。
Annotation的作用:
- 注解不是程序本身,可以对程序作出解释,或者强调被注解标注的方法或属性;
- 可以被其他程序读取(比如编译器读取);
- 注解还可以检查程序是否满足某些约束。
Annotation的格式:
- 注解是以”@注解名”在代码中存在的,还可以添加一些参数值,例如
@SuppressWarnings(value="unchecked")
。
Annoation在哪里使用:
可以附加在package、class、method、field等上面,相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问。
2、内置注解
@Override:定义java.lang.Override中,此注解仅适用于修饰方法,表示子类打算覆写父类里对应的方法;
- @Deprecated:定义在java.lang.Deprecated,此注释可以用于修饰方法、属性和类,表示不鼓励程序员使用这些元素,通常是因为它将要被废弃或者存在风险或者有更好的选择;
@SuppressWarnings:定义在java.lang.SuppressWarnings中,用来抑制编译时的告警信息,最常用的就是
@SuppressWarnings(value="unchecked")
,丰哥说代码里最好不要出现这个;3、元注解
元注解的作用是负责注解其他注解,Java定义了4个标准的meta-annotation类型,他们被用来提供对其他annotation类型做说明。4个标准的元注解为:
@Target:用于描述注解的使用范围,即被描述的注解可以用在什么地方;
- @Retention:表示需要在什么级别保存该注解信息,用于描述注解的生命周期;
- @Documented:说明该注解将被包含在javadoc中;
-
3.1 @Target
以Lombok为例,@Data注解就是作用在类上的,@Setter注解是作用在类、方法、属性上的,@Target注解就是描述注解的使用范围,有以下五个位置:
ElementType.TYPE :可以作用在类、接口和枚举类上;
- ElementType.METHOD :可以作用在方法上;
- ElementType.FIELD :可以作用在成员变量上;
- ElementType.CONSTRUCTOR :可以作用在构造器上;
ElementType.LOCAL_VARIABLE :可以作用在局部变量上。
3.2 @Retention
@Retention表示注解的生命周期,即注解被保留的阶段,有三种枚举值:
RetentionPolicy.SOURCE :在源文件中有效(即源文件保留),编译时编译器会直接丢弃这种策略的注解;
- RetentionPolicy.CLASS : 在class文件中有效(即class保留),当运行Java程序时, JVM不会保留注解。这是默认值;
- RetentionPolicy.RUNTIME : 在运行时有效(即运行时保留),当运行 Java 程序时, JVM会保留注解。程序可以通过反射获取该注解。
3.3 @Documented
用的很少。3.4 @Inherited
@Inherited也是用来修饰其他的Annotation的,被修饰过的Annotation将具有继承性。。。
例子:
- @xxx是我自定义的注解,我现在使用@xxx注解在Base类上使用….
- 使用@Inherited修饰@xxx注解
- 当有类继承了Base类的时候,该实现类自动拥有@xxx注解
4、自定义注解
使用@Interface自定义注解时,自动继承了java.lang.annotation.Annotation接口。
需要注意以下几点:
- @Interface用来声明一个注解,格式为:public @interface 注解名{ 定义内容 },当在类内定义注解时不需要
public
; - 其中的每一个方法实际上是声明了一个配置参数;
- 方法的名称就是参数的名称;
- 返回值的类型就是参数的类型,返回值只能是基本类型;
- 可以通过
default
来声明参数的默认值; - 如果只有一个参数成员,注解里仅写这一个参数成员即可,不需要
value
;如果有多个参数成员,注解里是value
={value1, value2…},即value后是一个枚举类型的数组,也可以直接{value1, value2…}不需要value
关键字; - 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。
举个例子,Demo1里的注解有多种类型的入参。
注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface MyAnno {
/*
* 1、这个不是方法,而是注解里要传入的参数,参数类型 + 参数名;
* 2、default是当注解里没有传入参数时,默认指定的参数,一般跟0或者"",如果默认值为-1代表不存在
*/
String name() default "";
int age() default 0;
// 如果默认值为-1,代表不存在
int id() default -1;
// 参数可以是数组,即注解里传入多个value
String[] schools() default {};
}
使用:
@MyAnno(name="Jerry", schools = {"uestc", "swjtu"})
public class User {
@MyAnno(age = 27)
public void method(){}
}
当注解的入参只需要一个参数的时候,建议用value命名参数名,可以省略value=XXX(如果参数名不是value则不行),直接将参数传入注解中,如下:
注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface MyAnno2 {
// 仅有一个参数建议用value命名参数名
String value() default "";
}
使用:
public class User {
@MyAnno2("Jerry")
public void method2(){}
}
5、自定义注解的使用
上面介绍了如何自定义一个注解,那通过自定义注解如何在不侵入代码的前提下让注解生效呢?一般有两种方案:
- 注解 + 注解处理器
- 注解 + aop
第二种方案可以参考我这篇文章中的第二节使用demo:https://www.yuque.com/zhangjian-mbxkb/zu7dxg/xe9hy1
本节对第一种方案的使用做个demo。
5.1 需求澄清
有一个用户的实体类,类里有用户名称和用户密码,都是字符串类型,要求对用户名和用户密码长度做限制,这里采用注解 + 注解处理器的方案实现。
用户类:
package com.Jerry.annotitaion;
/**
* @Description:
* @Author: Jerry
* @Date: 2022-03-09 22:24
*/
public class UserInfo {
@Check(min = 2, max = 3, description = "用户名长度在2-10个字符之间")
private String name;
@Check(min = 6, max = 10, description = "用户密码长度在6-10个字符之间")
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
说明:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
/**
- @author 11048 */ @Target({ElementType.FIELD}) @Retention(value= RetentionPolicy.RUNTIME) public @interface Check { int max() default 0; int min() default 0; String description() default “”; } ``` 说明:
- 由于注解处理器是基于反射实现的,因此注解Check要在JVM运行期也要生效,因此Check注解的@Retention的值为RetentionPolicy.RUNTIME;
- Check注解支持三个参数,max代表字符串最大长度,min代表字符串最小长度,description是限制条件描述。
注解处理器CheckAnnoHandler:
package com.Jerry.annotitaion;
import java.lang.reflect.Field;
/**
* @Description:
* @Author: Jerry
* @Date: 2022-03-09 22:26
*/
public class CheckAnnoHandler {
public void handle(Object obj) throws IllegalArgumentException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field: fields) {
Check check = field.getAnnotation(Check.class);
if (check == null) {
continue;
} else {
field.setAccessible(true);
if ("class java.lang.String".equals(field.getGenericType().toString())) {
String value = String.valueOf(field.get(obj));
if (value != null) {
if (value.length() < check.min() || value.length() > check.max()) {
throw new IllegalArgumentException("argument must between min and max");
}
}
}
}
}
}
}
说明:
- 既然有了自定义注解,一定要有对应的注解处理器,自定义注解的作用才能生效;
- 在这个注解处理器实现中,使用到了反射的api在运行期间获取检查的UserInfo实例,通过获取属性field和对应的注解值来实现,需要对熟悉反射的api。
Main类:
package com.Jerry.annotitaion;
/**
* @Description:
* @Author: Jerry
* @Date: 2022-03-09 22:39
*/
public class Main {
public static void main(String[] args) {
CheckAnnoHandler checkAnnoHandler = new CheckAnnoHandler();
UserInfo userInfo = new UserInfo();
userInfo.setName("jerry");
userInfo.setPassword("121");
try {
checkAnnoHandler.handle(userInfo);
} catch (IllegalAccessException e) {
}
}
}
运行结果:
感觉还是没有注解 + aop的实现简洁,使用方不需要再显示地调用注解处理器,注解处理器的逻辑都在切面里实现。
6、注解相关问题
6.1 自定义的Java注解是如何生效的?
自定义注解后,需要定义这个注解的注解解析及处理器,在这个注解解析及处理器的内部,通过反射机制使用Class、Method、Field对象的getAnnotation()方法可以获取各自位置上的注解信息,基于反射api获取类实例中的信息及注解中传入的参数值,进而完成注解的逻辑,例如给属性赋值、查找依赖的对象实例等。
6.2 如果要自定义一个编译期生效的注解,如何实现?
自定义注解的生命周期在编译期的,声明这个注解时@Retention的值为RetentionPolicy.CLASS,需要明确的是此时注解信息保留在源文件和字节码文件中,在JVM加载class文件后,注解信息不会存在内存中。
6.3 有如下的一个自定义注解,在使用这个注解的时候,它的value值是存在哪的?
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
String value() default "";
}
注解在编译期间编译成了接口,这个接口继承了java.lang.annotation.Annotation接口,接口是不能直接实例化使用的,当在代码中使用这个注解,并使用getAnnotation方法获取注解信息时,JVM通过动态代理的方式生成一个实现了注解接口的代理对象实例,然后对该实例的属性赋值,value值就存在这个代理对象实例中。