你的开发利器Spring自定义注解

前言

  自定义注解在开发中是一把利器,经常会被使用到。在上一篇文章中有提到了自定义校验注解的用法。 然而最近接到这样一个需求,主要是针对某些接口的返回数据需要进行一个加密操作。于是很自然的就想到了自定义注解+AOP去实现这样一个功能。但是对于自定义注解,只是停留在表面的使用,没有做到知其然,而知其所以然。所以这篇文章就是来了解自定义注解这把开发利器的。

什么是自定义注解?

官方定义

  An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

Google翻译一下

  注解是元数据的一种形式,可以添加到Java源代码中。 类,方法,变量,参数和包都可以被注释。 注解对其注释的代码的操作没有直接影响。

看完这个定义是不是有点摸不到头脑,不要慌实践出真知。

建立一个自定义注解

  我们先回顾一下需求的场景,是要针对xx接口的返回数据需要做一个加密操作。之前说到使用自定义注解+AOP来实现这个功能。所以我们先定义一个注解叫Encryption,被Encryption注解修饰后接口,返回的数据要被加密。

public @interface Encryption {
}

  你会发现创建自定义注解,就和建立普通的接口一样简单。只是所使用的关键字有所不同。在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口。

编写相应的接口

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
 return ResultVoUtil.success("不一样的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
 return ResultVoUtil.success("不一样的科技宅");
}

编写切面

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 获取注解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被标识了,则进行加密
  if(annotation != null){
    // 进行加密
    String encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
    resultVo.setData(encrypt);
  }

  return resultVo;
}

测试结果

  这个时候,你会发现返回的数据并没有被加密。 那么这个是为啥呢?俗话说遇到问题不要慌,先掏出手机发个朋友圈(稍微有点跑题了)。出现这个原因是,缺少了@Retention@Encryption的修饰,让我们把它加上。

@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

}

继续测试

  这个时候返回的数据就被加密了,说明自定义注解生效了。

测试普通接口

  没有用@Encryption的接口,返回的数据没有被加密。到此需求就已经实现了,接下来就该了解原理了。

@Retention

@Retention作用是什么

  Retention的翻译过来就是"保留"的意思。也就意味着它的作用是,用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicyRetentionPolicy有三种策略,分别是:

  • SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
  • CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
  • RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

选择合适的生命周期

  首先要明确生命周期 RUNTIME > CLASS > SOURCE 。一般如果需要在运行时去动态获取注解信息,只能使用RUNTIME。如果要在编译时进行一些预处理操作,比如生成一些辅助代码就用CLASS。如果只是做一些检查性的操作,比如 @Override和@SuppressWarnings,则可选用 SOURCE。

我们实际开发中的自定义注解几乎都是使用的RUNTIME

  最开始@Encryption没有使用@Retention对其生命周期进行定义。所以导致AOP在获取的时候一直为空,如果为空就不会对数据进行加密。

  是不是感觉这个注解太简陋。那再给他加点东西,加上个@Target

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

}

@Target

  @Target注解是限定自定义注解可以使用在哪些地方。这就和参数校验一样,约定好规则,防止乱用而导致问题的出现。针对上述的需求可以限定它只能用方法上。根据不同的场景,还可以使用在更多的地方。比如说属性、包、构造器上等等。

  • TYPE - 类,接口(包括注解类型)或枚举
  • FIELD - 字段(包括枚举常量)
  • METHOD - 方法
  • PARAMETER - 参数
  • CONSTRUCTOR - 构造函数
  • LOCAL_VARIABLE - 局部变量
  • ANNOTATION_TYPE -注解类型
  • PACKAGE - 包
  • TYPE_PARAMETER - 类型参数
  • TYPE_USE - 使用类型

  上面两个是比较常用的元注解,Java一共提供了4个元注解。你可能会问元注解是什么?元注解的作用就是负责注解其他注解。

@Documented

  @Documented的作用是对自定义注解进行标注,如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。没什么实际作用,了解一下就好了。

@Inherited

  被@Inherited修饰的注解,被用在父类上时其子类也拥有该注解。 简单的说就是,当在父类使用了被@Inherited修饰的注解@InheritedTest时,继承它的子类也拥有@InheritedTest注解。

这个可以单独讲下

注解元素类型

  参照我们在定义接口的经验,在接口中能定义方法和常量。但是在自定义注解中,只能定义一个东西:注解类型元素Annotation type element

其实可以简单的理解为只能定义方法,但是和接口中的方法有区别。

定义注解类型元素时需要注意如下几点:

  • 访问修饰符必须为public,不写默认为public。
  • 元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型。
  • type()括号中不能定义方法参数,仅仅只是一个特殊的语法。但是可以通过default关键字设置"默认值"。
  • 如果没有默认值,则使用注解时必须给该类型元素赋值。

继续改造

  需求这个东西经常都在变动。原本需要加密的接口只使用AES进行加密,后面又告知有些接口要使用DES加密。针对这样的情况,我们可以在注解内,添加一下配置项,来选择使用何种方式加密。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

    /**
     * 加密类型
     */

    String value() default "AES";
  
}

调整接口

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
 return ResultVoUtil.success("不一样的科技宅");
}

@Encryption(value = "DES")
@GetMapping("/encryptDes")
public ResultVo encryptDes(){
 return ResultVoUtil.success("不一样的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
 return ResultVoUtil.success("不一样的科技宅");
}

调整AOP

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 获取注解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被标识了,则进行加密
  if(annotation != null){
    // 进行加密
    String encrypt = null;
    switch (annotation.value()){
      case "AES":
        encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
        break;
      case "DES":
        encrypt = EncryptUtil.encryptByDes(JSON.toJSONString(resultVo.getData()));
        break;
      default:
        break;
    }
    resultVo.setData(encrypt);
  }

  return resultVo;
}

  至此就改造完了。可以发现注解元素类型,在使用的时候,操作元素类型像在操作属性。解析的时候,操作元素类型像在操作方法。

小技巧

  • 当注解没有注解类型元素,使用时候可直接写为@Encryption@Encryption()等效。
  • 当注解只有一个注解类型元素,并且命名是value。在使用时@Encryption("DES")@Encryption(value = "DES")等效。

注意的点

  • 需要根据实际情况指定注解的生命周期@Retention
  • 使用@Target来限制注解的使用范围,防止注解被乱用。
  • 如果注解是配置在方法上的,那么我们要从Method对象上获取。如果是配置在属性上,就需要从该属性对应的Field对象上去获取。总之用在哪里,就去哪里获取。

总结

  注解可以理解为就是一个标识。可以在程序代码中的关键节点上打上这些标识,它不会改变原有代码的执行逻辑。然后程序在编译时或运行时可以检测到这些标记,在做出相应的操作。结合上面的小场景,可以得出自定义注解使用的基本流程:

  1. 定义注解 --> 根据业务进行创建。
  2. 使用注解 --> 在相应的代码中进行使用。
  3. 解析注解 --> 在编译期或运行时检测到标记,并进行特殊操作。

上期回顾

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×