AOP+反射 批量参数校验

  • 本案例包括4个核心类:
    AopUtils:抽取出来的公共方法
    @ValidateGroup
    @ValidateField
    ValidateAspectJHandler
  • 功能包括:长度、值范围、正则匹配、非空校验
  • 以下是设计思路、最终代码、测试结果
  • 后续扩展只需要修改@ValidateField 和 ValidateAspectJHandler

1.演示: 最终使用方法

以注册功能为例

@RestController
@RequestMapping("validater")
public class ValidateController {

  @ValidateGroup(fields = {
      @ValidateField(index = 0,notNull = true,maxLen = 10,code = "param1-error",message = "param1校验错误"),
      @ValidateField(index = 1,notNull = true,fieldName = "passWord",minLen = 6,code = "passWord-erro",message = "密码校验错误"),
      @ValidateField(index = 1,notNull = true,fieldName = "age",minVal = 0,code = "age-error",message = "年龄不能小于0"),
      @ValidateField(index = 1,notNull = true,fieldName = "tall",minVal = 0,maxVal = 250.9,code ="tall-error",message = "身高范围出错"),
      @ValidateField(index = 1,notNull = true,fieldName = "phone",regStr = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",code = "phone-error",message = "手机号错误")
  })
  @PostMapping("post")
  public String postValidater(@RequestParam String param1,  RegisterDto dto){

    System.out.println("成功通过校验");
    System.out.println("第一个参数是:" + param1);

    System.out.println("第二个参数是"+dto.toString());

    return "succeed";
  }	
}

其中:请求Dto包含name、passWord、phone等字段;
利用AspectJ对接口方法直接进行代理校验

2.流程分析

  • AspectJ代理注解@ValidateGroup标注的方法,而这个Group注解中的属性就是@ValidateField []
  • 获取到@ValidateField数组后,遍历。通过比对注解的参数 与 dto或者param中对应名字的参数来进行校验
  • 如果校验成功就放行,校验失败就抛异常终止

3.公共方法抽取AopUtils

经过流程分析可知:至少需要以下几个方法(并可以抽取为公共组件)

  • public Method getMethod(ProceedingJoinPoint pjp):获取被AOP拦截的方法Method对象
  • public Annotation getAnnotationByMethod(Method method, Class annoClass):获取目标方法对象的指定注解对象
  • public Object getFieldFromDtoByFieldName(Object dto , String fieldName) 从dto中,获取指定属性名的属性值

如下工具类也可以做成全静态方法

public class AopUtils {

private volatile static AopUtils aopUtils;


private AopUtils() {
}

public static AopUtils getInstance() {
    if (aopUtils == null) {
        synchronized (AopUtils.class) {
            if (aopUtils == null) {
                aopUtils = new AopUtils();
            }
        }
    }
    return aopUtils;
}

/**
 * 获取目标类的指定方法
 */
public Method getMethodByClassAndName(Class c, String methodName) {
    Method[] methods = c.getDeclaredMethods();
    for (Method method : methods) {
        if (method.getName().equals(methodName)) {
            return method;
        }
    }
    return null;
}

/**
 * 获取目标方法的指定注解
 * 相当于 method.getAnnotation(xxxx.class);
 */
public Annotation getAnnotationByMethod(Method method, Class annoClass) {
    Annotation all[] = method.getAnnotations();
    for (Annotation annotation : all) {
        if (annotation.annotationType() == annoClass) {
            return annotation;
        }
    }
    return null;
}

/**
 * 获取被拦截方法的对象
 * 配合使用,最终用于在Aspectj中获取被拦截方法上的注解
 * 例如:AopUtils.getMethod(pjp).getDeclaredAnnotation(被aop拦截的注解.class)
 */
public Method getMethod(ProceedingJoinPoint pjp) {
    //获取参数的签名
    MethodSignature msig = (MethodSignature) pjp.getSignature();
    // MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象 如果在实现类的方法上,应该使用反射获取当前对象的方法对象
    Object target = pjp.getTarget();//获取连接点所在的目标对象(被代理的对象)而不是父类or接口
    //方法名 + 方法形参 ————》获取指定的方法对象(重载)
    String methodName = msig.getName();
    Class[] parameterTypes = msig.getParameterTypes();
    Method method = null;
    try {
        method = target.getClass().getMethod(methodName, parameterTypes);
    } catch (NoSuchMethodException e) {
        //log.error(...);
    }
    return method;
}

/**
 * 从dto中,获取指定属性名的属性值;
 */
public Object getFieldFromDtoByFieldName(Object dto , String fieldName) throws NoSuchFieldException, IllegalAccessException {
    Class<?> dtoClazz = dto.getClass();
    Field field = dtoClazz.getDeclaredField(fieldName);
    field.setAccessible(true);
    return field.get(dto);
}

//  这个其实还有另一种写法
//  private Method getMethod(ProceedingJoinPoint joinPoint) {
//    try {
//      Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
//      return joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(), parameterTypes);
//    } catch (NoSuchMethodException e) {
//      e.printStackTrace();
//    }
//    return null;
//  }
}

4.注解及设计原理

4.1ValidateGroup

这个注解用于被AspectJ拦截,其属性是一个数组,用于参数校验

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface ValidateGroup {

  ValidateFiled[] fields();

}

4.2ValidateField

  • 当且仅当只有一个参数的时候可以不用指定index
  • index默认为0,例如public void register(@RequestParam String param1 , @RequestBody Dto dto){}中应该设置index = 1 ,这是由于joinPoint.getArgs()获取的形参是一个数组,需要用index指定位置
  • 所有参数都有默认值(不进行校验)

如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface ValidateField {

  /**
   * 参数索引位置:接口中有多个参数时用index指定需要校验的参数
   * 默认:0号索引,当且仅当接口方法只有一个参数
   */
  int index() default 0;

  /**
   * 默认:如果参数是基本数据类型或String,就不用指定该参数
   * 如果参数是对象,要验证对象里面某个属性,就用该参数指定属性名
   */
  String fieldName() default "";

  /**
   * 错误码,用于日志记录
   */
  String code() default "";

  /**
   * 错误提示语,用于日志记录
   */
  String message() default "";

  /**
   * 正则验证
   */
  String regStr() default "";

  /**
   * 非空校验,为true表示不能为空,false表示能够为空
   */
  boolean notNull() default false;

  /**
   * 字符串最大长度
   */
  int maxLen() default 0x7fffffff;

  /**
   * 字符串最小长度
   */
  int minLen() default 0;

  /**
   * 最大值,用于验证数值类型数据
   */
  double maxVal() default 0x1.fffffffffffffP+1023;

  /**
   * 最小值,用于验证数值类型数据
   */
  double minVal() default 0x0.0000000000001P-1022;

}

5.ValidateAspectJHandler

5.1大体结构

@Component
@Aspect
public class ValidateAspectHandler {

  private AopUtils aopUtils = AopUtils.getInstance();
  /**
   * 使用AOP对使用了ValidateGroup的方法进行代理校验
   */
  @Around("@annotation(ValidateGroup)")
  public Object validateAround(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取当前方法对象method
//2.根据method对象获取对应的ValidateGroup对象
//3.调用封装方法,将validateGroup.fields() 与 joinPoint.getArgs()形参数组传入,进行校验
//4.如果为true,则执行下一步
    return joinPoint.proceed();
  }


  /**
   * 封装方法:验证参数是否合法
   */
  private boolean validateField(ValidateFiled[] validateFields, Object[] args) {
    for (ValidateFiled validateFiled : validateFields) {
		//1.每次循环都是一个校验逻辑
		//2.index仍然是指定对第几号元素进行校验
		//3.fieldName如果不指定,那就是对基本数据类型即@RequestParam进行校验;如果指定,则对Dto即@RequestBody进行校验

}

5.2具体实现

@Component
@Aspect
public class ValidateAspctJHandler {

//    private static org.slf4j.Logger logger = LoggerFactory.getLogger("ValidateAspctJHandler");
    private AopUtils aopUtils = AopUtils.getInstance();

  /**
   * 使用AOP对使用了ValidateGroup的方法进行代理校验
   */
  //相当于用@Around + return joinPoint.proceed(); 
  //使用@Before的时候不能用ProceedingJoinPoint 只能用JoinPoint
    @Before("@annotation(ValidateGroup)")
    public void validateAround(JoinPoint joinPoint) throws Throwable {
        //获取被代理的方法对象
        Method method = aopUtils.getMethod(joinPoint);
        //获取被代理的方法对象对应的@ValidateGroup对象
        ValidateGroup validateGroup = (ValidateGroup)aopUtils.getAnnotationByMethod(method, ValidateGroup.class);
        //获取被代理方法的参数数组(这是参数值,而不是 method.getParameterTypes()返回的是Class[]  )
        Object[] args = joinPoint.getArgs();
        /*
         * args和validateGroup中包含了全部需要校验的信息,因此可以封装为一个方法
         * 在这个方法中,如果校验失败则用throws抛异常的方式终止
         */
        validateAllFields(validateGroup.fields(), args);
    }

    /**
     * 验证参数是否合法
     * ValidateField[]中每一条都是一个校验规则,每一条都对应一个属性
     * Object[] args中是所有的请求参数值,需要从args[validateFiled.index()中确定是对谁进行校验
     */
  private void validateAllFields(ValidateField[] validateFields, Object[] args) throws NoSuchFieldException, IllegalAccessException {
      //遍历:对每个@ValidateField进行校验
    for (ValidateField validateFiled : validateFields) {
      Object arg;
      //1.当fieldName为默认值""的时候,此时是对@RequestParam即基本数据类型orString进行校验
      if ("".equals(validateFiled.fieldName())) {
          //arg是基本数据类型orString
        arg = args[validateFiled.index()];
        //2.如果fieldName设置了,那就是对dto中的某个属性进行校验
      } else {
          //获取第index号参数dto指定的属性值
        arg = aopUtils.getFieldFromDtoByFieldName(args[validateFiled.index()], validateFiled.fieldName());
      }

      //3.以下是校验流程,需要同时考虑是对dto属性or基本数据类型orString
      //3.1判断参数是否为空
      if (validateFiled.notNull()) {
        if (arg == null || arg.equals("")) {
//            logger.error(validateFiled.code() + ":" + validateFiled.message());
          throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
        //如果该参数能够为空,并且当参数为空时,就不用判断后面的了 ,直接返回
        } }else {
        if (arg == null || arg.equals("")) {
          return;
        }
      }

      //3.2判断字符串最大长度  如果设置为一个负数则不校验  默认为最大int值
      if (validateFiled.maxLen() >= 0) {
        if (arg.toString().length() > validateFiled.maxLen()) {
//          logger.error(validateFiled.code() + ":" + validateFiled.message());
          throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
        }
      }
      //3.3判断字符串最小长度  如果设置为一个负数则不校验  默认为0
      if (validateFiled.minLen() >= 0) {
        if (arg.toString().length() < validateFiled.minLen()) {
//          logger.error(validateFiled.code() + ":" + validateFiled.message());
          throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
        }
      }
      //3.4判断数值最大值  当不是默认值0x1.fffffffffffffP+1023的时候进行判断
      if (validateFiled.maxVal() != 0x1.fffffffffffffP+1023) {
        if (Double.parseDouble(arg.toString()) > validateFiled.maxVal()) {
//          logger.error(validateFiled.code() + ":" + validateFiled.message());
          throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
        }
      }
      //3.5判断数值最小值   当不是默认值0x0.0000000000001P-1022的时候进行判断
      if (validateFiled.minVal() != 0x0.0000000000001P-1022) {
        if (Double.parseDouble(arg.toString()) < validateFiled.minVal()) {
//          logger.error(validateFiled.code() + ":" + validateFiled.message());
          throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
        }
      }

      //3.6判断正则 若未设置正则校验则跳过
      if (!"".equals(validateFiled.regStr())) {
        if (arg instanceof String || arg instanceof Integer || arg instanceof BigDecimal || arg instanceof Double) {
          if (!(arg.toString()).matches(validateFiled.regStr())) {
//            logger.error(validateFiled.code() + ":" + validateFiled.message());
            throw new RuntimeException(validateFiled.code()  + ":" + validateFiled.message());
          }
        }
      }
    }
    return;
  }
}

5.3接口测试

@RestController
@RequestMapping("validater")
public class ValidateController {

  @ValidateGroup(fields = {
  	//如果是index=0也可以省略不写
      @ValidateField(index = 0,notNull = true,maxLen = 10,code = "param1-error",message = "param1校验错误"),
      @ValidateField(index = 1,notNull = true,fieldName = "passWord",minLen = 6,code = "passWord-erro",message = "密码校验错误"),
      @ValidateField(index = 1,notNull = true,fieldName = "age",minVal = 0,code = "age-error",message = "年龄不能小于0"),
      @ValidateField(index = 1,notNull = true,fieldName = "tall",minVal = 0,maxVal = 250.9,code ="tall-error",message = "身高范围出错"),
      @ValidateField(index = 1,notNull = true,fieldName = "phone",regStr = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",code = "phone-error",message = "手机号错误")
  })
  @PostMapping("post")
  public String postValidater(@RequestParam String param1,  RegisterDto dto){

    System.out.println("成功通过校验");
    System.out.println("第一个参数是:" + param1);

    System.out.println("第二个参数是"+dto.toString());

    return "succeed";
  }
}


如果参数错误,则会直接抛异常终止请求

如果参数都正确就可以通过校验,如下:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值