前言
在代码中会使用到校验 @NotEmpty @NotNull @Size 的注解等以及在类上注解@Builder 就可以使其拥有建造者模式的功能,本文主要介绍注解基本概念以及自定义一个注解。
1. 注解的基础知识
1.1 注解的分类
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
-
编译器使用的注解,这类注解不会编译在.class文件中,它们在编译之后就被编译器扔掉了
- @Override 让编译器检查该方法是否正确的实现了覆写
- @SuppressWarnings 告诉编译器忽略此处代码产生的警告
-
工具处理.class文件使用的注解,源码级别,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
-
程序在运行时能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。
- 一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
1.2 注解的定义
注解案例:
@Target(ElementType.FIELD)
// @Target 定义多个范围
// @Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckSize {
int min() default 0;
int max() default Integer.MAX_VALUE;
String value() default "";
}
Java语言中使用 @interface 来定义注解
注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐),当你不设置一个默认值时在你注解时就必须给没有default默认值的数赋值。最常用的参数应当命名为value。@GetMapping(value="/api") 和 @GetMapping("/api") 效果一样,方便一些,可以省略value=。
元注解: 有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target : @Target注解用于定义一个注解的能够被应用于源码中的范围
- 类或接口:ElementType.TYPE;
- 字段:ElementType.FIELD;
- 方法:ElementType.METHOD;
- 构造方法:ElementType.CONSTRUCTOR;
- 方法参数:ElementType.PARAMETER。
- 注解:ElementType.ANNOTATION_TYPE
- 局部变量:ElementType.LOCAL_VARIABLE
- 包:ElementType.PACKAGE
@Retention 定义了注解的生命周期
- 仅编译期:RetentionPolicy.SOURCE;
- 仅class文件:RetentionPolicy.CLASS;
- 运行期:RetentionPolicy.RUNTIME。
如果@Retention不存在,则该Annotation默认为CLASS。因为通常我们自定义的Annotation都是RUNTIME,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)这个元注解
@Repeatable 使用@Repeatable这个元注解可以定义Annotation是否可重复。多次注解。
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
定义后的使用:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited 使用@Inherited定义子类是否可继承父类定义的Annotation。@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效。
也就是一个类被@Inherited注解之后,他的子类也默认也使用了此注解。对于接口之间的继承是无效的。
1.3 注解的处理
在我们注解了之后,在哪添加这个注解的逻辑呢。
前面说过注解的三种运行方式:编译,底层内部,运行时。主要就是运行时,所有主要讨论如何读取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)
// 判断 @NewCheck 是否存在在 username 字段上
username.isAnnotationPresent(NewCheck.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();
当一个注解(Annotation)注解在类上时,我们可以判断这个类是否被Annotation注解,然后再从中取值。亦或者可以直接取值,然后判断是否为空,然后做逻辑处理。
Class clazz = User.class;
if (clazz.isAnnotationPresent(CheckSize.class)) {
CheckSize checkSize = clazz.getAnnotation(CheckSize.class);
...
}
CheckSize checkSize= clazz.getAnnotation(CheckSize.class);
if(checkSize!=null){
...
}
当一个注解(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;
}
}
2. 自己定义一个注解
定义注解:
@Target(ElementType.FIELD) // 只能注解在字段上
@Retention(RetentionPolicy.RUNTIME) // 生命周期:运行时
public @interface CheckSize { // @interface 注解
int min() default 0; // 最小长度,默认0
int max() default Integer.MAX_VALUE; // 最大长度
String value() default ""; // value() 适合比较常用的值,此处我们只是定义了但没有校验逻辑
}
校验逻辑:
public class CheckSizeValid {
public static void check(User user) throws IllegalAccessException {
// objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段
for (Field field : user.getClass().getDeclaredFields()) {
// 通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常)
field.setAccessible(true);
// 获取 filed 定义的 @CheckSize 对象
CheckSize checkSize = field.getAnnotation(CheckSize.class);
// 如果存在
if (checkSize != null) {
Object obj = field.get(user);
if (obj instanceof String) {
String str = (String) obj;
if (str.length() < checkSize.min()) {
throw new IllegalArgumentException("illegal field " + field.getName() + " length too short");
} else if (str.length() > checkSize.max()) {
throw new IllegalArgumentException("illegal field " + field.getName() + " length too long");
}
}
}
}
}
}
现在注解有了,逻辑有了。现在我们可以通过直接通过一下方式来实现让逻辑生效。
public class UserTest {
public static void main(String[] args) throws IllegalAccessException {
User user = new User();
user.setUsername("user");
user.setPassword("jintianshigehaorizi");
CheckSizeValid.check(user);
System.out.println("user:" + user.toString());
}
}
结果
Exception in thread "main" java.lang.IllegalArgumentException: illegal field username length too short
at org.tustcs.wei.weibackend.basis.jdk.annotation.CheckSizeValid.check(CheckSizeValid.java:36)
at org.tustcs.wei.weibackend.basis.jdk.annotation.UserTest.main(UserTest.java:21)
如何校验的不那么直接呢?想想通用逻辑统一做如何处理,通过切面编程AOP吧。此时我们再定义一个注解,这个注解修饰方法,然后通过切面获得参数对象,通过反射获得类,然后之后的逻辑处理就和上面一样了。下面贴一点简略代码演示。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {
}
@Aspect
@Component
@Slf4j
public class AopCheck {
@Pointcut("@annotation(org.tustcs.wei.weibackend.basis.jdk.annotation.Valid)")
public void check() {
}
@Around("check()")
public void before(JoinPoint joinPoint) throws IllegalAccessException {
// 获取参数对象
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
Class<?> objectClass = args[i].getClass();
// ...
}
}
}
// 需要检验的地方添加注解
@Valid
public void printUser(User user) {
System.out.println(user.toString());
}
从使用的角度说已有现成的校验的轮子,可以通过实现 ConstraintValidator 接口来用。
3. 总结
定义一个注解关键的要素:注解的生命周期(大多数我们选择运行时有效),装饰什么(类,方法还是成员变量上?)。使用@interface 定义类。定义类似函数的成员变量,最后有defalut默认值,把最常用的设为value(),会在使用比较方便。
逻辑处理的时候通过反射获得参数,方法等判断是否有注解修饰,有修饰的话就取出进行逻辑判断处理。