02 Java 注解的分类
上面介绍注解的语法和使用,我们遇到了 @Target、@Retention
等没见过的注解,你可能有点懵。但没关系,听我说道说道。Java 中有 @Override、@Deprecated
和 @SuppressWarnings
等内置注解;也有 @Target、@Retention、@Documented、@Inherited
等修饰注解的注解,称之为元注解。
2.1 内置注解
Java 定义了一套自己的注解,其中作用在代码上的是:
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
- @SuppressWarnings - 用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
JDK7 之后又加了 3 个,这几个的用法,我也用得很少。就不过多介绍了,感兴趣的小伙伴自行百度分别是:
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface SafeVarargs {}
- @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {}
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { Class<? extends Annotation> value(); }
2.2 元注解
元注解就是修饰注解的注解,分别有:
2.2.1 @Target
用来指定注解的作用域(如方法、类或字段),其中 ElementType 是枚举类型,其定义如下,也代表可能的取值范围
public enum ElementType { /**标明该注解可以作用于类、接口(包括注解类型)或enum声明*/ TYPE, /** 标明该注解可以作用于字段(域)声明,包括enum实例 */ FIELD, /** 标明该注解可以作用于方法声明 */ METHOD, /** 标明该注解可以作用于参数声明 */ PARAMETER, /** 标明注解可以作用于构造函数声明 */ CONSTRUCTOR, /** 标明注解可以作用于局部变量声明 */ LOCAL_VARIABLE, /** 标明注解可以作用于注解声明(应用于另一个注解上)*/ ANNOTATION_TYPE, /** 标明注解可以作用于包声明 */ PACKAGE, /** * 标明注解可以作用于类型参数声明(1.8新加入) * @since 1.8 */ TYPE_PARAMETER, /** * 类型使用声明(1.8新加入) * @since 1.8 */ TYPE_USE }
PS:如果 @Target 无指定作用域,则默认可以作用于任何元素上。等同于:
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
2.2.2 @Retention
用来指定注解的生命周期,它有三个值,对应 RetentionPolicy
中的三个枚举值,分别是:源码级别(source),类文件级别(class)或者运行时级别(runtime)
- SOURCE:只在源码中可用
- CLASS:注解在 class 文件中可用,但会被 VM 丢弃(该类型的注解信息会保留在源码里和 class 文件里,在执行的时候,不会加载到虚拟机中),PS:当注解未定义 Retention 值时,默认值是 CLASS,如 Java 内置注解,@Override、@Deprecated、
@SuppressWarnning
等
- RUNTIME:在源码,class,运行时均可用,因此可以通过反射机制读取注解的信息(源码、class 文件和执行的时候都有注解的信息),如
SpringMvc
中的@Controller、@Autowired、@RequestMapping
等。此外,我们自定义的注解也大多在这个级别。
2.2.2.1 理解 @Retention
这里引申一下话题,要想理解 @Retention 就要理解下从 java 文件到 class 文件再到 class 被 jvm 加载的过程了。下图描述了从 .java 文件到编译为 class 文件的过程:
其中有一个注解抽象语法树的环节,这个环节其实就是去解析注解然后做相应的处理。
所以重点来了,如果你要在编译期根据注解做一些处理,你就需要继承 Java 的抽象注解处理器 AbstractProcessor,并重写其中的 process () 方法。
一般来说只要是注解的 @Target 范围是 SOURCE 或 CLASS,我们就要继承它;因为这两个生命周期级别的注解等加载到 JVM 后,就会被抹除了。
比如,lombok 就用 AnnotationProcessor
继承了 AbstractProcessor
,以实现编译期的处理。这也是为什么我们使用 @Data
就能实现 get、set
方法的原因。
2.2.3 @Documented
执行 javadoc 的时候,标记这些注解是否包含在生成的用户文档中。
2.2.4 @Inherited
标记这个注解具有继承性,比如 A 类被注解 @Table 标记,而 @Table 注解被 @Inherited 声明(具备继承性);继承于 A 的子类,也继承 @Table 注解。
//声明 Table 注解,有继承性 @Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Table { }
03 自定义注解
好啦,说了这么多理论。大家也听累了,我也聊累了。那怎么自定义一个注解并让它起作用呢?下面我将带着你们看看我司的防止重复提交的注解是怎么实现的?当然,由于设计内部的东西,我只会写写伪代码。思路在前面介绍过了,为方便阅读我拿下来,大家理解就行。
需求是:同一用户,三秒内重复提交一样的参数,就会报异常阻止重复提交,否则正常提交处理写请求。
3.1 定义注解
首先,定义注解必须是 @interface 修饰;其次,有四个考虑的点:
- 注解的生命周期 @Retention,一般都是 RUNTIME 运行时。
- 注解的作用域 @Target,作用于写请求,也就是 controller 方法上。
- 是否需要元素,用分布式锁实现,必须要有锁的过期时间。给定默认值,也支持自定义。
- 是否生成 javadoc @Documented,这个注解无脑加就对了。
基于此,我司的防止重复提交的自定义注解就出来了:
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface BanReSubmitLock { /** * 锁定时间,默认单位(秒)默认时间(3秒) */ long lockTime() default 3L; }
3.2 AOP 切面处理
@Aspect @Component public class BanRepeatSubmitAop { @Autowired private final RedisUtils redisUtils; @Pointcut("@annotation(com.nasus.framework.web.annotation.BanReSubmitLock)") private void banReSubmitLockAop() { } @Around("banReSubmitLockAop()") public Object aroundApi(ProceedingJoinPoint point) throws Throwable { // 获取 AOP 切面方法签名 MethodSignature signature = (MethodSignature) point.getSignature(); // 方法 Method method = signature.getMethod(); // 获取目标方法上的 BanRepeatSubmitLock 注解 BanReSubmitLock banReSubmitLock = method.getAnnotation(BanReSubmitLock.class); // 根据用户信息以及提交参数,创建 Redis 分布式锁的 key String lockKey = createReSumbitLockKey(point, method); // 根据 key 获取分布式锁对象 Lock lock = redisUtils.getReSumbitLock(lockKey); // 上锁 boolean result = lock.tryLock(); // 上锁失败,抛异常 if (!result) { throw new Exception("请不要重复请求"); } // 其他处理 ... } /** * 生成 key */ private String createReSumbitLockKey(ProceedingJoinPoint point, Method method) { // 拼接用户信息 & 请求参数 ... // MD5 处理 ... // 返回 } }
可以看到这里利用了 AOP 切面的方式获取被 @NoReSubmitLock 修饰的方法,并借此拿到切点(被注解修饰方法)的参数、用户信息等等,通过 MD5 处理,最终尝试上锁。
3.3 使用
public class TestController { // NoReSubmitLock 注解修饰 save 方法,防止重复提交 @NoReSubmitLock public boolean save(Object o){ // 保存逻辑 } }
使用也非常简单,只需要一个注解就可以完成大部分的逻辑;如果不用注解,每个写接口的方法都要写一遍防止重复提交的逻辑的话,代码非常繁琐,难以维护。通过这个例子相信你也看到了,注解的作用。
04 总结
本文介绍了注解的作用主要是标记、检查以及解耦;介绍了注解的语法;介绍了注解的元素以及传值方式;介绍了 Java 的内置注解和元注解,最后通过我司的一个实际例子,介绍了注解是如何起作用的?
注解是代码的特殊标记,可以在程序编译、类加载、运行时被读取并做相关处理。其对应 RetentionPolicy 中的三个枚举,其中 SOURCE、CLASS 需要继承 AbstractProcessor (注解抽象处理器),并实现 process () 方法来处理我们自定义的注解。而 RUNTIME 级别是我们常用的级别,结合 Java 的反射机制,可以在很多场景优化代码。