springboot写自定义参数验证方式

简介: 本次发表文章距上次发表已近有两月有余,原因是两月前离开了上家公司(离开原因可能会在年终终结叙述,本篇暂且忽略),来到了现在所在的京东集团,需要花时间熟悉环境和沉淀一下新的东西,因此写文章也暂时没那么勤奋了,不得不说这次是机遇也是对自己职业生涯的一次重要决定。

本次发表文章距上次发表已近有两月有余,原因是两月前离开了上家公司(离开原因可能会在年终终结叙述,本篇暂且忽略),来到了现在所在的京东集团,需要花时间熟悉环境和沉淀一下新的东西,因此写文章也暂时没那么勤奋了,不得不说这次是机遇也是对自己职业生涯的一次重要决定。

话说本篇内容主要分享的是自定义方法参数的验证,参数的基本校验在对外接口或者公用方法时经常所见,用过hibernate的验证方式的朋友一定不会陌生,读完本篇内容能够很好的帮助各位朋友对自定义参数验证方式有一定了解:

  • 自定义参数验证的思路
  • 实战参数验证的公用方法
  • aop结合方法参数验证实例

自定义参数验证的思路

对于自定义参数验证来说,需要注意的步骤有以下几步:

  1. 怎么区分需要验证的参数,或者说参数实体类中需要验证的属性(答案:可用注解标记)
  2. 对于参数要验证哪几种数据格式(如:非空、邮箱、电话以及是否满足正则等格式)
  3. 怎么获取要验证的参数数据(如:怎么获取方法参数实体传递进来的数据)
  4. 验证失败时提示的错误信息描述(如:统一默认校验错误信息,或者获取根据标记验证注解传递的错误提示文字暴露出去)
  5. 在哪一步做校验(如:进入方法内部时校验,或是可以用aop方式统一校验位置)

实战参数验证的公用方法

根据上面思路描述,我们首先需要有注解来标记哪些实体属性需要做不同的校验,因此这里创建两种校验注解(为了本章简短性):IsNotBlank(校验不能为空)和RegExp(正则匹配校验),如下代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IsNotBlank {
    String des() default "";
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RegExp {
    String pattern();

    String des() default "";
}

然后为了统一这里创建公用的验证方法,此方法需要传递待验证参数的具体实例,其主要做的工作有:

  1. 通过传递进来的参数获取该参数实体的属性
  2. 设置field.setAccessible(true)允许获取对应属性传进来的数据
  3. 根据对应标记属性注解来验证获取的数据格式,格式验证失败直接提示des描述

这里有如下公用的验证方法:

public class ValidateUtils {

    public static void validate(Object object) throws IllegalAccessException {
        if (object == null) {
            throw new NullPointerException("数据格式校验对象不能为空");
        }
        //获取属性列
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            //过滤无验证注解的属性
            if (field.getAnnotations() == null || field.getAnnotations().length <= 0) {
                continue;
            }
            //允许private属性被访问
            field.setAccessible(true);
            Object val = field.get(object);
            String strVal = String.valueOf(val);

            //具体验证
            validField(field, strVal);
        }
    }

    /**
     * 具体验证
     *
     * @param field  属性列
     * @param strVal 属性值
     */
    private static void validField(Field field, String strVal) {
        if (field.isAnnotationPresent(IsNotBlank.class)) {
            validIsNotBlank(field, strVal);
        }
        if (field.isAnnotationPresent(RegExp.class)) {
            validRegExp(field, strVal);
        }
        /** add... **/
    }

    /**
     * 匹配正则
     *
     * @param field
     * @param strVal
     */
    private static void validRegExp(Field field, String strVal) {
        RegExp regExp = field.getAnnotation(RegExp.class);
        if (Strings.isNotBlank(regExp.pattern())) {
            if (Pattern.matches(regExp.pattern(), strVal)) {
                return;
            }
            String des = regExp.des();
            if (Strings.isBlank(des)) {
                des = field.getName() + "格式不正确";
            }
            throw new IllegalArgumentException(des);
        }
    }

    /**
     * 非空判断
     *
     * @param field
     * @param val
     */
    private static void validIsNotBlank(Field field, String val) {
        IsNotBlank isNotBlank = field.getAnnotation(IsNotBlank.class);
        if (val == null || Strings.isBlank(val)) {
            String des = isNotBlank.des();
            if (Strings.isBlank(des)) {
                des = field.getName() + "不能为空";
            }
            throw new IllegalArgumentException(des);
        }
    }
}

有了具体验证方法,我们需要个测试实例,如下测试接口和实体:

public class TestRq extends BaseRq implements Serializable {

    @IsNotBlank(des = "昵称不能为空")
    private String nickName;
    @RegExp(pattern = "\\d{10,20}", des = "编号必须是数字")
    private String number;
    private String des;
    private String remark;
}
    @PostMapping("/send")
    public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
        ValidateUtils.validate(rq);
        return testService.sendTestMsg(rq);
    }

image

aop结合方法参数验证实例

上面是围绕公用验证方法来写的,通常实际场景中都把它和aop结合来做统一验证;来定制两个注解,MethodValid方法注解(是否验证所有参数)和ParamValid参数注解(标记方法上的某个参数):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface MethodValid {
    /**
     * 验证所有参数
     *
     * @return true
     */
    boolean isValidParams() default true;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PARAMETER})
public @interface ParamValid {
}

有了两个标记注解再来创建aop,我这里是基于springboot框架的实例,所有引入如下mvn:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

然后aop需要做如下逻辑:

  1. 获取方法上传递参数(param1,param2...)
  2. 遍历每个参数实体,如有验证注解就做校验
  3. 遍历标记有ParamValid注解的参数,如有验证注解就做校验

这里特殊的地方是,想要获取方法参数对应的注解,需要method.getParameterAnnotations()获取所有所有参数注解后,再用索引来取参数对应的注解;如下aop代码:

package com.shenniu003.common.validates;

import com.shenniu003.common.validates.annotation.MethodValid;
import com.shenniu003.common.validates.annotation.ParamValid;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * des:
 *
 * @author: shenniu003
 * @date: 2019/12/01 11:04
 */
@Aspect
@Component
public class ParamAspect {

    @Around(value = "@annotation(methodValid)", argNames = "joinPoint,methodValid")
    public Object validMethod(ProceedingJoinPoint joinPoint, MethodValid methodValid) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println("method:" + method.getName());
        String strArgs = Arrays.toString(joinPoint.getArgs());
        System.out.println("params:" + strArgs);

        //获取方法所有参数的注解
        Annotation[][] parametersAnnotations = method.getParameterAnnotations();

        for (int i = 0; i < joinPoint.getArgs().length; i++) {
            Object arg = joinPoint.getArgs()[i];
            if (arg == null) {
                continue; //
            }

            if (methodValid.isValidParams()) {
                //验证所有参数
                System.out.println(arg.getClass().getName() + ":" + arg.toString());
                ValidateUtils.validate(arg);
            } else {
                //只验证参数前带有ParamValid注解的参数
                //获取当前参数所有注解
                Annotation[] parameterAnnotations = parametersAnnotations[i];
                //是否匹配参数校验注解
                if (matchParamAnnotation(parameterAnnotations)) {
                    System.out.println(Arrays.toString(parameterAnnotations) + " " + arg.getClass().getName() + ":" + arg.toString());
                    ValidateUtils.validate(arg);
                }
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 是否匹配参数的注解
     *
     * @param parameterAnnotations 参数对应的所有注解
     * @return 是否包含目标注解
     */
    private boolean matchParamAnnotation(Annotation[] parameterAnnotations) {
        boolean isMatch = false;
        for (Annotation parameterAnnotation : parameterAnnotations) {
            if (ParamValid.class == parameterAnnotation.annotationType()) {
                isMatch = true;
                break;
            }
        }
        return isMatch;
    }
}

这里编写3中方式的测试用例,验证方法所有参数、无参数不验证、验证方法参数带有@ParamValid的参数,以此达到不同需求参数的校验方式:

    //验证方法所有参数
    @MethodValid
    public void x(TestRq param1, String param2) {
    }
    //无参数不验证
    @MethodValid
    public void xx() {
    }
    //验证方法参数带有@ParamValid的参数
    @MethodValid(isValidParams = false)
    public void xxx(TestRq param1, @ParamValid String param2) {
    }

同样用send接口作为测试入口,调用上面3种方法:

    @PostMapping("/send")
    @MethodValid
    public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
//        ValidateUtils.validate(rq);
        testController.x(rq, "验证方法所有参数");
        testController.xx();
        testController.xxx(rq, "验证方法参数带有@ParamValid的参数");
        return testService.sendTestMsg(rq);
    }

image

相关文章
|
15天前
|
Java 数据库连接 Spring
Spring Boot命令行启动添加参数
Spring Boot命令行启动添加参数
|
1月前
|
XML Java 数据库连接
spring boot 参数的过滤注解与实战
在Spring Boot应用中,对于入参的过滤,通常会涉及到对Web层的数据验证和处理。Spring Boot借助Spring框架提供了强大的验证框架支持,主要基于JSR-303/JSR-380(Bean Validation API)规范,以及Spring自身的@Valid或@Validated注解来实现请求参数的验证。以下是一些常见的使用案例来展示如何对参数进行过滤和验证。
29 1
|
1月前
|
缓存 前端开发 Java
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
32 1
|
1月前
|
Java 数据库 数据安全/隐私保护
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
102 1
|
2天前
|
前端开发 Java
SpringBoot之实体参数的详细解析
SpringBoot之实体参数的详细解析
10 0
|
4天前
|
JSON Java 数据格式
Spring Boot实现各种参数校验
这些是Spring Boot中实现参数校验的一些常见方法,你可以根据项目需求选择适合的方式来进行参数校验。
11 0
|
1月前
|
前端开发 NoSQL Java
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
|
1月前
|
存储 NoSQL 前端开发
【SpringBoot】Redis集中管理Session和自定义用户参数解决登录状态及校验问题
【SpringBoot】Redis集中管理Session和自定义用户参数解决登录状态及校验问题
|
1月前
|
Java 容器 Spring
SpringBoot:Bean生命周期自定义初始化和销毁
SpringBoot:Bean生命周期自定义初始化和销毁
|
JSON 数据安全/隐私保护 数据格式
SpringBoot中的异常处理与参数校验_2
  兄弟们好,这次来跟老铁交流两个问题,异常和参数校验,在说参数校验之前我们先来说异常处理吧,因为后面参数的校验会牵扯到异常处理这块的内容。   说到异常处理,我不知道大家有没有写过或者遇到过如下的写法。
206 0

相关实验场景

更多