面试官:什么是 Java 注解?

时间:2022-11-19 16:44:59

面试官:什么是 Java 注解?

哈喽,我是狗哥。随着开发经验的累积,我越发觉得基础真的非常重要。比如:大部分框架 (如 Spring) 都使用了注解简化代码并提高编码的效率,掌握注解是一名 JAVA 程序员必备的技能。

但我发现很多工作 2、3 年的同学居然还没写过自定义注解,问起注解的原理也是一脸懵。我是很震惊的,你们咋理解代码的?基于此,今天我们就来一起学习下注解。

国际惯例,先上脑图:

面试官:什么是 Java 注解?

01 什么是注解?

Java 注解(Annotation),相信大家没用过也见过。个人理解,注解就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,从而做相对应的处理。

注解跟注释很像,区别是注释是给人看的(想想自己遇到那些半句注释没有的业务代码,还是不是很难受?);而注解是给程序看的,它可以被编译器读取。

1.1 注解的作用

注解大多时候与反射或者 AOP 切面结合使用,它的作用有很多,比如标记和检查,最重要的一点就是简化代码,降低耦合性,提高执行效率。比如我司就是通过自定义注解 + AOP 切面结合,解决了写接口重复提交的问题。

简单描述下我司防止重复提交注解的逻辑:请求写接口提交参数 —— 参数拼接字符串生成 MD5 编码 —— 以 MD5 编码加用户信息拼接成 key,set Redis 分布式锁,能获取到就顺利提交(分布式锁默认 3 秒过期),不能获取就是重复提交了,报错。

面试官:什么是 Java 注解?

如果每加一个写接口,就要写一次以上逻辑的话,那程序员会疯的。所以,有大佬就使用注解 + AOP 切面的方式解决了这个问题。只要在写接口 Controller 方法上加这个注解即可解决,也方便维护。

1.2 注解的语法

以我司防止重复提交的自定义注解,介绍下注解的语法。它的定义如下:

  1. // 声明 NoRepeatSubmit 注解 
  2. @Target(ElementType.METHOD) // 元注解 
  3. @Retention(RetentionPolicy.RUNTIME) // 元注解 
  4. public @interface NoRepeatSubmit { 
  5.  
  6.  /** 
  7.      * 锁定时间,默认单位(秒) 
  8.      */ 
  9.  long lockTime() default 3L; 
  10.  

Java 注解使用 @interface 修饰,我司的 NoRepeatSubmit 注解也不例外。此外,还使用两个元注解。其中 @Target 注解传入 ElementType.METHOD 参数来标明 @NoRepeatSubmit 只能用于方法上,@Retention(RetentionPolicy.RUNTIME) 则用来表示该注解生存期是运行时,从代码上看注解的定义很像接口的定义,在编译后也会生成 NoRepeatSubmit.class 文件。

1.3 注解的元素

定义在注解内部的变量,称之为元素。注解可以有元素,也可以没有元素。像 @Override 就是无元素的注解,@SuppressWarnings 就属于有元素的注解。

  1. @Target(ElementType.METHOD) 
  2. @Retention(RetentionPolicy.SOURCE) 
  3. public @interface Override { 
  1. @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) 
  2. @Retention(RetentionPolicy.SOURCE) 
  3. public @interface SuppressWarnings { 
  4.     String[] value(); 

带元素的自定义注解:

  1. @Target({ElementType.METHOD}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Documented 
  4. public @interface NoRepeatSubmit { 
  5.      
  6.     /** 
  7.      * 锁定时间,默认单位(秒) 
  8.      */ 
  9.     long lockTime() default 2L; 
  10.      

1.3.1 注解元素的格式

  1. // 基本格式 
  2. 数据类型 元素名称(); 
  3.  
  4. // 带默认值 
  5. 数据类型 元素名称() default 默认值; 

1.3.2 注解元素的数据类型

注解元素支持如下数据类型:

  1. 所有基本类型(int,float,boolean,byte,double,char,long,short) 
  2.  
  3. String 
  4.  
  5. Class 
  6.  
  7. enum 
  8.  
  9. Annotation 
  10.  
  11. 上述类型的数组 

声明注解元素时可以使用基本类型但不允许使用任何包装类型,同时注解也可以作为元素的类型,也就是嵌套注解。

1.3.3 编译器对元素默认值的限制

遵循规则:

元素要么具有默认值,要么在使用注解时提供元素的值。

对于非基本类型的元素,无论是在源代码中声明,还是在注解接口中定义默认值,都不能以 null 作为值。

1.4 注解的使用

注解是以 @注释名 的格式在代码中使用,比如:以下常见的用法。

  1. public class TestController { 
  2.      
  3.     // NoRepeatSubmit 注解修饰 save 方法,防止重复提交 
  4.     @NoRepeatSubmit 
  5.     public static void save(Object o){ 
  6.         // 保存逻辑 
  7.     } 
  8.  
  9.     // 一个方法上可以有多个不同的注解 
  10.     @Deprecated 
  11.     @SuppressWarnings("uncheck"
  12.     public static void getDate(){ 
  13.          
  14.     } 

在 save 方法上使用 @NoRepeatSubmit (我司自定义注解),加上之后,编译期会自动识别该注解并执行注解处理器的方法,防止重复提交;

而对于 @Deprecated 和 @SuppressWarnings (“uncheck”),则是 Java 的内置注解,前者意味着该方法是过时的,后者则是忽略指定的异常检查。

02 Java 注解的分类

上面介绍注解的语法和使用,我们遇到了 @Target、@Retention 等没见过的注解,你可能有点懵。但没关系,听我说道说道。Java 中有 @Override、@Deprecated 和 @SuppressWarnings 等内置注解;也有 @Target、@Retention、@Documented、@Inherited 等修饰注解的注解,称之为元注解。

2.1 内置注解

Java 定义了一套自己的注解,其中作用在代码上的是:

@Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

  1. @Target(ElementType.METHOD) 
  2. @Retention(RetentionPolicy.SOURCE) 
  3. public @interface Override { 
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 
  4. public @interface Deprecated { 
  • @SuppressWarnings - 用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。
  1. @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) 
  2. @Retention(RetentionPolicy.SOURCE) 
  3. public @interface SuppressWarnings { 
  4.     String[] value(); 

JDK7 之后又加了 3 个,这几个的用法,我也用得很少。就不过多介绍了,感兴趣的小伙伴自行百度分别是:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) 
  4. public @interface SafeVarargs {} 
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target(ElementType.TYPE) 
  4. public @interface FunctionalInterface {} 
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target(ElementType.ANNOTATION_TYPE) 
  4. public @interface Repeatable { 
  5.     Class<? extends Annotation> value(); 

2.2 元注解

元注解就是修饰注解的注解,分别有:

2.2.1 @Target

用来指定注解的作用域(如方法、类或字段),其中 ElementType 是枚举类型,其定义如下,也代表可能的取值范围

  1. public enum ElementType { 
  2.     /**标明该注解可以作用于类、接口(包括注解类型)或enum声明*/ 
  3.     TYPE, 
  4.  
  5.     /** 标明该注解可以作用于字段(域)声明,包括enum实例 */ 
  6.     FIELD, 
  7.  
  8.     /** 标明该注解可以作用于方法声明 */ 
  9.     METHOD, 
  10.  
  11.     /** 标明该注解可以作用于参数声明 */ 
  12.     PARAMETER, 
  13.  
  14.     /** 标明注解可以作用于构造函数声明 */ 
  15.     CONSTRUCTOR, 
  16.  
  17.     /** 标明注解可以作用于局部变量声明 */ 
  18.     LOCAL_VARIABLE, 
  19.  
  20.     /** 标明注解可以作用于注解声明(应用于另一个注解上)*/ 
  21.     ANNOTATION_TYPE, 
  22.  
  23.     /** 标明注解可以作用于包声明 */ 
  24.     PACKAGE, 
  25.  
  26.     /** 
  27.      * 标明注解可以作用于类型参数声明(1.8新加入) 
  28.      * @since 1.8 
  29.      */ 
  30.     TYPE_PARAMETER, 
  31.  
  32.     /** 
  33.      * 类型使用声明(1.8新加入) 
  34.      * @since 1.8 
  35.      */ 
  36.     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 注解?

其中有一个注解抽象语法树的环节,这个环节其实就是去解析注解然后做相应的处理。

所以重点来了,如果你要在编译期根据注解做一些处理,你就需要继承 Java 的抽象注解处理器 AbstractProcessor,并重写其中的 process () 方法。

一般来说只要是注解的 @Target 范围是 SOURCE 或 CLASS,我们就要继承它;因为这两个生命周期级别的注解等加载到 JVM 后,就会被抹除了。

比如,lombok 就用 AnnotationProcessor 继承了 AbstractProcessor,以实现编译期的处理。这也是为什么我们使用 @Data 就能实现 get、set 方法的原因。

面试官:什么是 Java 注解?

2.2.3 @Documented

执行 javadoc 的时候,标记这些注解是否包含在生成的用户文档中。

2.2.4 @Inherited

标记这个注解具有继承性,比如 A 类被注解 @Table 标记,而 @Table 注解被 @Inherited 声明(具备继承性);继承于 A 的子类,也继承 @Table 注解。

  1. //声明 Table 注解,有继承性 
  2. @Inherited 
  3. @Target(ElementType.TYPE) 
  4. @Retention(RetentionPolicy.RUNTIME) 
  5. public @interface Table { 
  6.  
  7. }  

03 自定义注解

好啦,说了这么多理论。大家也听累了,我也聊累了。那怎么自定义一个注解并让它起作用呢?下面我将带着你们看看我司的防止重复提交的注解是怎么实现的?当然,由于设计内部的东西,我只会写写伪代码。思路在前面介绍过了,为方便阅读我拿下来,大家理解就行。

面试官:什么是 Java 注解?

需求是:同一用户,三秒内重复提交一样的参数,就会报异常阻止重复提交,否则正常提交处理写请求。

3.1 定义注解

首先,定义注解必须是 @interface 修饰;其次,有四个考虑的点:

  • 注解的生命周期 @Retention,一般都是 RUNTIME 运行时。
  • 注解的作用域 @Target,作用于写请求,也就是 controller 方法上。
  • 是否需要元素,用分布式锁实现,必须要有锁的过期时间。给定默认值,也支持自定义。
  • 是否生成 javadoc @Documented,这个注解无脑加就对了。

基于此,我司的防止重复提交的自定义注解就出来了:

  1. @Documented 
  2. @Target({ElementType.METHOD}) 
  3. @Retention(RetentionPolicy.RUNTIME) 
  4. public @interface BanReSubmitLock { 
  5.      
  6.     /** 
  7.      * 锁定时间,默认单位(秒)默认时间(3秒) 
  8.      */ 
  9.     long lockTime() default 3L; 

3.2 AOP 切面处理

  1. @Aspect 
  2. @Component 
  3. public class BanRepeatSubmitAop { 
  4.  
  5.  @Autowired 
  6.     private final RedisUtils redisUtils; 
  7.  
  8.     @Pointcut("@annotation(com.nasus.framework.web.annotation.BanReSubmitLock)"
  9.     private void banReSubmitLockAop() { 
  10.     } 
  11.  
  12.     @Around("banReSubmitLockAop()"
  13.     public Object aroundApi(ProceedingJoinPoint point) throws Throwable { 
  14.   // 获取 AOP 切面方法签名  
  15.         MethodSignature signature = (MethodSignature) point.getSignature(); 
  16.   // 方法 
  17.         Method method = signature.getMethod(); 
  18.   // 获取目标方法上的 BanRepeatSubmitLock 注解 
  19.         BanReSubmitLock banReSubmitLock = method.getAnnotation(BanReSubmitLock.class); 
  20.   // 根据用户信息以及提交参数,创建 Redis 分布式锁的 key 
  21.         String lockKey = createReSumbitLockKey(point, method); 
  22.         // 根据 key 获取分布式锁对象 
  23.   Lock lock = redisUtils.getReSumbitLock(lockKey); 
  24.   // 上锁 
  25.   boolean result = lock.tryLock(); 
  26.   // 上锁失败,抛异常 
  27.         if (!result) { 
  28.             throw new Exception("请不要重复请求"); 
  29.         } 
  30.   // 其他处理 
  31.   ... 
  32.     } 
  33.   
  34.  /** 
  35.      * 生成 key 
  36.      */ 
  37.  private String createReSumbitLockKey(ProceedingJoinPoint point, Method method) { 
  38.   // 拼接用户信息 & 请求参数 
  39.   ... 
  40.    
  41.   // MD5 处理 
  42.   ... 
  43.    
  44.   // 返回 
  45.  } 
  46.   

可以看到这里利用了 AOP 切面的方式获取被 @NoReSubmitLock 修饰的方法,并借此拿到切点(被注解修饰方法)的参数、用户信息等等,通过 MD5 处理,最终尝试上锁。

3.3 使用

  1. public class TestController { 
  2.      
  3.     // NoReSubmitLock 注解修饰 save 方法,防止重复提交 
  4.     @NoReSubmitLock 
  5.     public boolean save(Object o){ 
  6.         // 保存逻辑 
  7.     } 
  8. }     

使用也非常简单,只需要一个注解就可以完成大部分的逻辑;如果不用注解,每个写接口的方法都要写一遍防止重复提交的逻辑的话,代码非常繁琐,难以维护。通过这个例子相信你也看到了,注解的作用。

04 总结

本文介绍了注解的作用主要是标记、检查以及解耦;介绍了注解的语法;介绍了注解的元素以及传值方式;介绍了 Java 的内置注解和元注解,最后通过我司的一个实际例子,介绍了注解是如何起作用的?

注解是代码的特殊标记,可以在程序编译、类加载、运行时被读取并做相关处理。其对应 RetentionPolicy 中的三个枚举,其中 SOURCE、CLASS 需要继承 AbstractProcessor (注解抽象处理器),并实现 process () 方法来处理我们自定义的注解。而 RUNTIME 级别是我们常用的级别,结合 Java 的反射机制,可以在很多场景优化代码。

05 参考链接

bilibili.com/video/BV1p4411P7V3

mp.weixin.qq.com/s/BPKvLbdCyuWijkD-si75Dw

blog.csdn.net/javazejian/article/details/71860633

原文链接:https://mp.weixin.qq.com/s/sJd8FRY2r_CjvLGM1viTfQ