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

前言

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

什么是自定义注解?

官方定义segmentfault

  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.app

Google翻译一下jvm

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

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

创建一个自定义注解

  咱们先回顾一下需求的场景,是要针对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 {

}

继续测试spa

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

测试普通接口

  没有用@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. 解析注解 --> 在编译期或运行时检测到标记,并进行特殊操做。

上期回顾

结尾

  若是以为对你有帮助,能够多多评论,多多点赞哦,也能够到个人主页看看,说不定有你喜欢的文章,也能够随手点个关注哦,谢谢。

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