前言

在Java编程中,我们经常会遇到一些重复性的工作,比如检查某个方法是否满足特定的条件、为某个类添加特定的元数据等。虽然我们可以通过编写额外的代码来完成这些工作,但这无疑会增加代码的复杂性和维护成本。而自定义注解正是解决这一问题的有效手段。通过定义注解,我们可以将那些与业务逻辑无关但又必须执行的代码(如检查、验证等)与业务逻辑代码分离开来,使代码更加清晰、简洁。

注解基本概念

  1. 什么是注解?

    注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

  2. 注解分类

    1. 标准注解

      Java自带的标准注解为我们提供了常用的编译检查和文档生成等功能,如:

      • @Override:用于标识一个方法覆盖父类方法。当子类中的方法使用了 @Override 注解,但实际上并没有覆盖父类中的方法时,编译器会发出错误警告。
      • @Deprecated:用于标记已过时的方法或类。当我们使用这些被标记为过时的方法或类时,编译器会发出提醒警告,建议不再使用它们。
      • @SuppressWarnings:用于抑制编译器产生特定的警告。通过为 @SuppressWarnings 注解提供参数(如”unchecked”、”deprecation”等),可以告诉编译器忽略这些类型的警告。
    2. 元注解

      元注解用于定义注解的属性,如作用目标、生命周期和继承性等,都位于java.lang.annotation包中,我们创建自定义注解时会用到4个标准元注解,如:

      • @Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
      • @Inherited:用于阐述某个被标注的类型是被继承的。使用了 @Inherited 修饰的注解类型被用于一个class时该class的子类也有了该注解。
      • @Retention:定义该注解的生命周期:某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。使用当前元注解可以对自定义注解的”生命周期”进行限制。
        • RetentionPolicy.RUNTIME 注解会在class字节码文件中存在,在运行时可以通过反射获取到。
        • RetentionPolicy.CLASS 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得。
        • RetentionPolicy.SOURCE 注解仅存在于源码中,在class字节码文件中不包含。
      • @Target:说明注解所修饰的对象范围:注解可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
        • ElementType.CONSTRUCTOR 作用于构造器
        • ElementType.FIELD 作用于域/属性
        • ElementType.LOCAL_VARIABLE 用于描述局部变量
        • ElementType.METHOD 作用于方法
        • ElementType.PACKAGE 用于描述包
        • ElementType.PARAMETER 用于描述参数
        • ElementType.TYPE 用于描述类、接口(包括注解类型) 或enum声明,最常用
    3. 自定义注解

      自定义注解允许开发者根据具体需求定义自己的注解,为代码添加额外的元数据。这些元数据可以在编译时或运行时被读取和使用,以实现特定的功能或检查。

自定义注解

  1. 作用

    • 代码可读性:通过为方法、类、字段等添加注解,可以提供关于它们如何被使用或它们的预期行为的额外信息,方便于以后的查看和理解。
    • 简化配置:在框架和库中,自定义注解经常用于简化配置。例如,Spring框架中的@Component@Service@Repository@Controller 等注解用于定义bean,从而避免了在XML配置文件中手动定义。
    • 运行时行为控制:通过反射API,可以在运行时读取和处理注解,从而改变程序的行为。例如,某些框架可能会查找带有特定注解的方法,并在特定条件下调用它们。
    • 框架开发:当开发框架或库时,自定义注解是提供可扩展性和可配置性的关键工具。框架开发者可以定义一组注解,然后允许用户在自己的代码中使用这些注解来定制框架的行为。
    • AOP(面向切面编程):自定义注解经常与AOP一起使用,以定义切点(pointcuts)。例如,Spring AOP允许开发者定义带有注解的方法作为切点,并在这些方法执行前后插入额外的逻辑。
    • 减少模板代码:通过自定义注解和相应的处理逻辑,可以减少或消除重复的模板代码。例如,Lombok中 @Data 可以使用注解自动生成getter和setter方法,或者自动生成数据库访问代码等。
    • 测试:在测试场景中,自定义注解可以用于标记需要被测试的方法、类或字段,或者用于定义测试数据等。
  2. 实例一(登录验证)

    全局拦截器+自定义登录验证注解

    以用户登陆状态效验业务为示例,定义注解 @AccessLimit 进行统一拦截状态效验

    1. 定义注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      /***
      * 自定义登录效验注解
      *
      * @author deng
      * @date 2024/6/5
      */
      @Retention(RUNTIME)
      @Target(METHOD)
      public @interface AccessLimit {

      /**
      * 是否需要登录
      *
      * @return 是否需要登录
      */
      boolean needLogin() default true;
      }

      定义了注解 @AccessLimit 作用于方法上,注解的生命周期为最高级别 (可通过反射机制读取)

      needLogin 为注解元素,默认值为 true 代表需要登录,如传入值为 false 则无需登录验证

    2. 前置拦截验证

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      /***
      * 登录注解拦截验证类
      * @author deng
      * @date 2024/6/5
      */
      @Component
      public class AccessInterceptor implements HandlerInterceptor {

      /***
      * 前置拦截
      * @param request
      * @param response
      * @param handler
      * @return
      */
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
      // 判断当前处理器类型为 HandlerMethod
      if (handler instanceof HandlerMethod) {
      // 获取处理器方法信息
      HandlerMethod hm = (HandlerMethod) handler;
      // 判断是否需要登录
      AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
      // 无注解则放行
      if (accessLimit == null) {
      return true;
      }
      // 获取AccessLimit注解元素信息
      boolean needLogin = accessLimit.needLogin();
      // 是否需要登录
      if (needLogin) {
      /*
      * 效验登录状态(登录状态在线则放行,不在线抛错返回)
      */
      System.out.println("=======进行登录状态效验=======");
      }
      }
      return true;
      }
      }
    3. 统一请求拦截器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      /***
      * 统一入口拦截器
      * @author deng
      * @date 2024/6/5
      */
      @Configuration
      @RequiredArgsConstructor
      public class WebConfig implements WebMvcConfigurer {

      private final AccessInterceptor accessInterceptor;

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(accessInterceptor);
      }
      }
    4. 控制器方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      /***
      * @author deng
      * @date 2024/6/5
      */
      @RestController
      @RequestMapping("/annotation")
      public class AnnotationController {

      /***
      * 经过登录拦截方法
      */
      @AccessLimit
      @RequestMapping("/test")
      public void test() {
      System.out.println("进入方法test");
      }

      /***
      * 未经过登录拦截方法
      */
      @AccessLimit(needLogin = false)
      @RequestMapping("/test2")
      public void test2() {
      System.out.println("进入方法test2");
      }
      }

      image-20240605164949914

  3. 实例二(日志记录)

    AOP切面+自定义日志记录注解

    以记录用户行为操作日志业务为示例,定义注解 @Log 进行统一行为操作记录

    1. 定义注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      /***
      * 自定义操作日志记录注解
      * @author deng
      * @date 2024/6/18
      */
      @Target({ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Log {

      /**
      * 模块
      */
      String title() default "";

      /**
      * 功能
      */
      String businessType() default "其它";
      }

      定义注解 @Log 作用于方法上,注解的生命周期为最高级别 (可通过反射机制读取) ,注解元素为titlebusinessType

    2. 切面处理

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      /***
      * 操作日志记录切面处理
      * @author deng
      * @date 2024/6/18
      */
      @Aspect
      @Component
      public class LogAspect {

      /**
      * 处理完请求后执行
      *
      * @param joinPoint 切点
      */
      @AfterReturning(pointcut = "@annotation(controllerLog)")
      public void doAfterReturning(JoinPoint joinPoint, Log controllerLog) {
      try {
      // *========操作日志=========*//
      // 获取方法名称
      String className = joinPoint.getTarget().getClass().getName();
      String methodName = joinPoint.getSignature().getName();
      // 获取注解信息
      String title = controllerLog.title();
      String businessType = controllerLog.businessType();
      System.out.println("切面处理 => 当前请求控制器:" + className + ",当前请求方法:" + methodName + ",当前模块:" + title + ",当前功能:" + businessType);
      /*
      * 进行用户操作日志记录,保存数据库
      */
      System.out.println("进行用户操作日志记录");
      } catch (Exception exp) {
      // 记录本地异常日志
      exp.printStackTrace();
      }
      }
      }
    3. 控制器方法

      1
      2
      3
      4
      5
      6
      7
      8
      /***
      * 经过操作日志记录方法
      */
      @Log(title = "测试模块", businessType = "查询操作")
      @RequestMapping("/test3")
      public void test3() {
      System.out.println("进入方法test3");
      }

      image-20240618135908616