Spring AOP与注解搭配 使用自定义注解Annotation实现@Valid@NotEmpty数据校验的功能

本文介绍如何利用AOP和自定义注解实现参数校验功能,包括创建自定义注解、异常处理及AOP切面的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

本文涉及AOP以及自定义注解两个功能点。切面部分,涉及的一些基本的切面知识点,同时也包含一些反射的相关内容。自定义注解没什么好说的,注意元注解的使用方法以及参数使用就好。

早在几个月前就写好了AOP专题和注解的相关知识点题目草稿,不过由于前几个月加班严重,所以一直没时间。蹭着试运行这段的空闲期做个总结。



实现

大体思路

可以先参考@Valid的校验流程:

  1. 参数如果被@Valid注解,那么就对该参数进行校验。这是校验前提
  @PostMapping("/example")
  public void example(@RequestBody @Valid MeetingDTO meetingDTO ) {
  }
  1. 参数对象中的属性如果被@NotEmpty @NotNull @Max @Min等注解注解,标明该属性需要做非空校验、非null校验、最大最小值校验等等。这是具体的校验规则
@Data
public class MeetingDTO implements Serializable {
  
  @NotEmpty(message = "预约id不能为空")
  private String orderId;

  @NotEmpty(message = "预约状态不能为空")
  private String orderStatus;

}
  1. 当校验不通过时,即上述校验结果为空或null时抛出异常[org.springframework.web.bind.MethodArgumentNotValidException];否则正常运行。这是最终校验的结果

我们可以根据如上三个要点入手,用相同的方法实现我们自定义注解的校验功能。



要点

根据@Valid的思路与具体实现,要考虑到如下几点:

  1. 为了多种校验规则能够实现,可以添加一个父注解,方便后续管理及实现。

  2. 切面可以拦截使用注解的方法,但是没法拦截使用注解的参数。因此无法向@Valid那样直接作用到参数上,所以我们可以退一步,直接加到方法级别上(这里可以使用Java的拦截器拦截指定接口请求,然后校验参数中是否有注解,但是我暂时还没使用过拦截器与注解的搭配,就不献丑了)。

  // 无法拦截
  @PostMapping("/valid")
  public String valid(@RequestBody @CValid ExampleDTO exampleDTO) {
  }

  // 可以
  @CValid
  @PostMapping("/valid")
  public String valid(@RequestBody ExampleDTO exampleDTO) {
  }

OK,可以正式开始敲代码了。



自定义注解
  1. 首先实现检验参数的注解。该注解作用于方法。其功能是标记需要做数据校验的方法参数。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface CValid {
}

2. 所有具体校验规则注解的父类注解。类似于**接口**的作用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, ANNOTATION_TYPE})
@Inherited
public @interface ValidMark {
}


3. 实现一个**非空校验**的注解,该注解用于字段或参数。message参数用于报错消息展示。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, PARAMETER })
@ValidMark // 注解标记
public @interface CNotEmpty {

  /**
   * 报错消息
   * @return
   */
  public String message() default "字段不能为null或空";

}

##### 异常类 自定义异常类。
public class CValidException extends RuntimeException {

  public CValidException(String message) {
    super("com.training.spring.exception.CValidException: field valid failed: "
            + message);
  }

  public CValidException() {
    super();
  }
}
AOP实现注解功能

切面功能的代码实现思路如下:

  • 设置目标切点,获取参数数据、注解数据。

  • 根据上述数据判断类型:是对象校验还是对象属性校验 、是非空校验还是其他校验规则。

  • 最后根据校验规则,判断是否抛出异常。

代码如下:

@Aspect
@Component
public class ValidAspect {

  @Pointcut("@annotation(com.training.spring.aop.annotation.valid.CValid)")
  public void check() {
  }

  @Before("check()")
  public void beforeCheck(JoinPoint joinpoint) {
    // 获取参数注解
    MethodSignature signature = (MethodSignature) joinpoint.getSignature();
    Method method = signature.getMethod();
    Annotation[][] annotations = method.getParameterAnnotations();
    // 获取入参
    Object[] args = joinpoint.getArgs();
    for (int i = 0; i < args.length; i++) {
      Object object = args[i];
      Class<?> aClass = object.getClass();
      Annotation[] paraAnnotation = annotations[i];
      Annotation annotation = checkAnnotationExist(paraAnnotation);
      if (null != annotation) {
        // 验证当前对象
        this.validObject(object, aClass, annotation);
      } else {
        // 验证对象的属性
        this.validObjectField(object, aClass);
      }
    }
  }

  // 判断是否是校验注解
  public Annotation checkAnnotationExist(Annotation[] annotations) {
    for (Annotation annotation : annotations) {
      if (annotation.annotationType().isAnnotationPresent(ValidMark.class))
        return annotation;
    }
    return null;
  }

  // 校验对象
  public void validObject(Object object, Class<?> aClass, Annotation annotation) {
    // 校验是否为空
    if (CNotEmpty.class == annotation.annotationType()) {
      if (this.checkNullOrBlank(object, aClass)) {
        throw new CValidException(((CNotEmpty) annotation).message());
      }
    }
    // 校验其他…………
  }

  // 校验属性
  public void validField(Field field) {
    field.setAccessible(true);
    // 校验是否为空
    if (field.isAnnotationPresent(CNotEmpty.class)) {
      if (this.checkNullOrBlank(field, field.getClass())) {
        CNotEmpty cNotEmpty = field.getAnnotation(CNotEmpty.class);
        String message = cNotEmpty.message();
        throw new CValidException(message);
      }
    }
    // 校验其他…………
  }


  // 校验对象的属性
  public void validObjectField(Object object, Class<?> aClass) {
    Field[] fields = aClass.getDeclaredFields();
    if (fields.length < 1)
      return;
    for (Field field : fields) {
      // 校验属性
      this.validField(field);
    }
  }

  // 验证参数是否为空
  public boolean checkNullOrBlank(Object object, Class<?> aClass) {
    if (null == object)
      return true;
    /**
     * Boolean Character Byte Short Integer Long Float Double Void
     */
    if (aClass.isPrimitive())
      return false;
    if (object instanceof String)
      return StringUtils.isEmpty(object);
    if (object instanceof Collection)
      return CollectionUtils.isEmpty((Collection<?>) object);
    return false;
  }

}



调用

controller层调用

  // 作用于属性
  @CValid
  @PostMapping("/object")
  public String  object(@RequestBody ExampleDTO exampleDTO) {
    System.out.println(exampleDTO.toString());
    return exampleDTO.getName();
  }

  // 作用于参数
  @CValid
  @PostMapping("/field")
  public String field(@RequestBody @CNotEmpty List<String> list) {
    System.out.println(list.toString());
    return null;
  }

对象类展示

public class ExampleDTO implements Serializable {

  @CNotEmpty(message = "姓名不能为空")
  private String name;
}



总结

至此整体功能实现,框架也有了雏形。不过缺陷还是存在的,校验注解的实现,依赖于if else编程,容易造成代码冗余,难以维护;除此之外还有很多功能缺失。需要继续学习相关知识才行。此外多看英文文档有好处,或许能让你对概念的理解更深刻。

参考

Lesson: Annotations.
Aspect Oriented Programming with Spring.
Overview of Java Built-in Annotations.
Creating a Custom Annotation in Java.
Spring AOP Tutorial.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值