跳至主要內容

SpringMVC-参数校验

holic-x...大约 19 分钟JAVA框架

SpringMVC-参数校验

学习核心

  • 参数校验场景:理解为什么要引入校验器进行参数校验?具备什么优缺点

  • 两种校验方式的优缺点和业务场景适配度

    • 传统参数校验方式:串行校验,具备业务灵活性
    • 引入Validator:校验器,可自定义校验规则,使用上有一定的门槛
  • 案例源码:springboot-validator-demoopen in new window

学习资料

参数校验

1.场景介绍

传统串行校验

传统串行校验方式

传统的校验方式采用的是串行校验,根据传入的参数依次进行校验判断,这种模式在于相对灵活,可自定义校验规则,一些业务的嵌入校验也可以很好地进行处理。但对于一些基础的判断可以考虑通过注解校验的方式进行简化

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void save(User user) {
        // 串行校验(逐个手动校验,可以设定不同的处理方式)
        String username = user.getUsername();
        String password = user.getPassword();
        String email = user.getEmail();
        String phone = user.getPhone();

        // 校验用户名
        if(StringUtils.isEmpty(username)){
            // 处理1:抛出自定义异常
            throw new BusinessException(Constant.PARAM_FAIL_ERROR,"用户名不能为空");
        }

        // 校验密码
        if(StringUtils.isEmpty(password)){
            // 处理2:定义一个Map存放异常信息,并将Map返回
            Map<String,Object> map = new HashMap<>();
            map.put("code",Constant.PARAM_FAIL_ERROR);
            map.put("msg","密码不能为空");
        }

        // 校验邮箱
        if(!StringUtils.isEmpty(email)){
            // 如果传入邮箱,则校验邮箱有效性
            if(!email.contains("@")){
                throw new BusinessException(Constant.PARAM_FAIL_ERROR,"【邮箱】格式校验错误");
            }
        }

        // 校验手机号
        if(!StringUtils.isEmpty(phone)){
            // 如果传入手机号,则校验手机号有效性
            if(!Pattern.matches("^[1][3,4,5,6,7,8,9][0-9]{9}$", phone)){
                throw new BusinessException(Constant.PARAM_FAIL_ERROR,"【联系方式】格式校验错误");
            }
        }

        // 校验通过,模拟添加用户操作
        System.out.println("数据校验通过,模拟用户添加操作");
    }
}

自定义异常处理器:(@RestControllerAdvice = @ControllerAdvice+@ResponseBody)

// 自定义异常
@RestControllerAdvice
public class GlobalException {

    /**
     * @ExceptionHandler:限定对何种异常进行处理
     * @ResponseBody:处理返回的格式(SpringMVC会响应一个json格式信息)
     */
    @ExceptionHandler(value = BusinessException.class)
    public String handler(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "- 具体异常信息:" + ex.getMessage());
        // 返回结果
        return "{\n" +
                "    \"code\":\"-1\",\n" +
                "    \"msg\":\"服务器出现异常,请联系管理员处理...\",\n" +
                "    \"data\":null\n" +
                "}";
    }
}

Controller接口

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 1.传统串行校验方式
    @GetMapping("/add")
    public String add(@RequestBody User user){
        userService.save(user);
        return "success";
    }

}

测试

image-20240611145429223

思考:传统串行校验方式有什么优缺点?

​ 从传统校验方式的处理来看,这种校验方式思路简单,实现方便,且不需要依赖其他第三方框架,可自定义校验逻辑,实现具备灵活性。在针对一些业务场景的参数校验,可以使用传统串行校验方式来灵活定义

​ 但是针对一些简单的参数校验(例如非空验证、特殊字段的正则验证登),一旦业务扩展、实体属性会越来越多,需要手动校验的参数也会越来越多,就会使得串行进行参数校验的方法越来越庞大,导致后期维护可能存在不便。

Validator引入

javax.validation的引入

​ JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,可以直接将这些注解加在我们JavaBean的属性上面(面向注解编程的时代),就可以在需要校验的时候进行校验了,在SpringBoot中已经包含在starter-web中。在其他项目中可以引用下述依赖,并自行调整版本

<!--jsr 303-->
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>5.2.0.Final</version>
</dependency>

​ 如果是Springboot版本,需注意Springboot版本和validator的版本兼容性(一些高版本的Springboot已经取消了对validator的依赖,需要自定引入。如果版本不匹配,则使用的时候会报错),如果引入的是高版本的Springboot则自行加入下述配置,显式引入valiator相关依赖(也可自行指定版本,但尽量和Springboot版本一致,避免版本兼容导致不可预测的问题)

<!-- 引入参数校验相关 -->
<!--jsr 303-->
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
</dependency>
<!-- hibernate validator-->
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependency>

基于注解校验的方式:定义UserAddDTO接收添加参数

/**
 * 用户添加实体类定义
 */
public class UserAddDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    // 用户名
    @NotNull(message = "用户名不能为空")
    @Length(min = 6,max = 20,message = "用户名设定在6-20个字符")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
    private String username;

    // 用户密码
    @NotNull(message = "用户密码不能为空")
    private String password;

    // 邮箱
    @NotNull(message = "邮箱不能为空")
    @Email(message = "邮箱格式错误")
    private String email;

    // 联系方式
    @NotNull(message = "联系方式不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String phone;

    @NotNull(message = "创建时间不能为空")
    @Future(message = "时间必须是将来时间")
    private Date createTime;

}

全局异常处理:为了优化交互显式,此处自定义RspDTO返回类型和全局异常处理,进而自定义一套交互规范

// 返回类型定义
@Data
public class RspDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    private int code;
    private String msg;
    private Object data;
    public RspDTO(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public RspDTO(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
// 自定义异常
@RestControllerAdvice
public class GlobalException {

    /**
     * @ExceptionHandler:限定对何种异常进行处理
     * @ResponseBody:处理返回的格式(SpringMVC会响应一个json格式信息)
     */
    @ExceptionHandler(value = BusinessException.class)
    public String handler(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "- 具体异常信息:" + ex.getMessage());
        // 返回结果
        return "{\n" +
                "    \"code\":\"-1\",\n" +
                "    \"msg\":\"服务器出现异常,请联系管理员处理...\",\n" +
                "    \"data\":null\n" +
                "}";
    }

    // 异常处理器:针对参数校验设定相应的handler
    /**
     * 方法参数校验
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public RspDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        return new RspDTO(Constant.PARAM_FAIL_ERROR, e.getBindingResult().getFieldError().getDefaultMessage());
    }

    /**
     * ValidationException
     */
    @ExceptionHandler(ValidationException.class)
    public RspDTO handleValidationException(ValidationException e) {
        return new RspDTO(Constant.PARAM_FAIL_ERROR, e.getCause().getMessage());
    }

    /**
     * ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public RspDTO handleConstraintViolationException(ConstraintViolationException e) {
        return new RspDTO(Constant.PARAM_FAIL_ERROR, e.getMessage());
    }

    // ---------- 其他异常处理 ---------
    @ExceptionHandler(NoHandlerFoundException.class)
    public RspDTO handlerNoFoundException(Exception e) {
        return new RspDTO(404, "路径不存在,请检查路径是否正确");
    }

    @ExceptionHandler(Exception.class)
    public RspDTO handleException(Exception e) {
        return new RspDTO(500, "系统繁忙,请稍后再试");
    }
}

Controller定义(在需要校验的方法参数实体定义前添加@Validated注解)

// 2.validator校验方式
@GetMapping("/addByValidator")
public String addByValidator(@RequestBody @Validated UserAddDTO userAddDTO){
  userService.saveByValidator(userAddDTO);
  return "success";
}

对ResDTO响应类型的处理问题:上述异常处理返回的是一个RspDTO实体,需要设定相关配置来适配全局响应返回的数据,否则console提示下述异常

  • org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation异常处理
2024-06-11 14:21:22.952  WARN 20000 --- [nio-8080-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Failure in @ExceptionHandler com.noob.framework.exception.GlobalException#handleMethodArgumentNotValidException(MethodArgumentNotValidException)

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:315) ~[spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:183) ~[spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) ~[spring-web-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:135) ~[spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(ExceptionHandlerExceptionResolver.java:428) ~[spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver.doResolveException(AbstractHandlerMethodExceptionResolver.java:75) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:142) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1331) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1142) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.24.jar:5.3.24]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.3.24.jar:5.3.24]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:670) [tomcat-embed-core-9.0.69.jar:4.0.FR]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.3.24.jar:5.3.24]
	

image-20240611190006122

​ 通过排查定位到问题,发现是RspDTO没有配置@Data(没有提供getter构造器)导致程序无法正常将其转化为JSON数据,进而导致触发异常被归类到不匹配的媒体类型

​ 基于==@Data==的问题在这个项目测试中还有一个隐藏的坑点,即在调用addByValidator接口时,由于是直接通过对请求方法参数进行校验,无法通过设定断点获取到传入的UserAddDTO对象,但是这里如果在UserAddDTO中没有指定getter构造器或者配置@Data注解,则无法获取到传入的UserAddDTO对象指定的参数值,则在校验的时候直接触发了非空验证异常,进而抛出错误

@GetMapping("/addByValidator")
public String addByValidator(@RequestBody @Validated UserAddDTO userAddDTO){
  userService.saveByValidator(userAddDTO);
  return "success";
}

​ 当UserAddDTO没有提供getter、setter构造器,访问接口,每次校验的顺序不同(每次字段校验的顺序不同,但都是反馈字段非空),因此要排查传入的字段参数和方法定义的对象是否一致,确保字段无误然后去定位其他问题。此处发现传入字段一致,则说明传入对象属性没有被正常设置。

image-20240611190823390

​ 给UserAddDTO配置@Data注解,调整之后,请求数据,正常响应(全局异常正常拦截处理),校验也符合预期。依次调整接口请求参数,验证参数校验

{
    "username":"哈哈哈哈哈哈哈",
    "password":"小白",
    "email":"13800@163.com",
    "phone":"13800138000",
    "createTime":"2024-06-16"
}

思考:Valiator的引入解决了什么问题?

​ 根据上述案例可以看到,针对同样场景的参数校验,通过引入Validator框架,可以通过注解的方式来灵活配置参数的校验规则,替代了传统手工编写校验代码的工作量,代码具备良好的可读性和可维护性。

​ 在一些场景下可以使用Validator框架提供的注解灵活地对参数进行校验,此外框架还提供了其他的一些注解配置和扩展入口来适配业务的灵活性,这点在后面的案例中进行介绍

  • 单个参数校验
  • 对象属性校验
  • 自定义校验器(自定义注解校验规则)
  • 分组校验

2.Valiator注解说明

@NotNull:不能为null,但可以为empty(""," "," ")

@NotEmpty:不能为null,而且长度必须大于0 (" "," ")

@NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0("test") 即:必须有实际字符

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,boolean验证注解的元素值是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型验证注解的元素值大于等于@Min指定的value值
@Max(value=值)和@Min要求一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)和@Min要求一样验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值)和@Min要求一样验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@Min要求一样验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

此处只列出Hibernate Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。

3.自定义参数注解

自定义参数注解构建思路

  • 自定义校验注解
  • 自定义Valiator(实现验证的逻辑代码)
  • 使用自定义注解校验(在实体参数上配置自定义注解进行参数校验)

构建步骤参考

【1】自定义校验注解IdentityCardNumber

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {

    String message() default "身份证号码不合法";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

【2】自定义Valiator(实现验证的逻辑代码)

​ 其中引用到的IdCardValidatorUtils是一个自定义的身份证校验工具类(如果仅仅是为了测试自定义校验的正确性,此处仅需要关注isValid返回的结果即可)

​ 校验逻辑:isValid方法返回true则校验通过校验;isValid方法返回false则校验失败,此时会抛出MethodArgumentNotValidException(该异常会在自定义全局异常处理器中被拦截处理,进而处理成自定义的数据响应格式)

public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {

    @Override
    public void initialize(IdentityCardNumber identityCardNumber) {
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        // 空指针异常处理
        if(o==null){
            // 如果传入o为空,则不做任何校验,默认放行
            return true;
        }
        // 如果传入o不为空,则进行业务校验
        return IdCardValidatorUtils.isValidate18Idcard(o.toString());
    }
}

【3】在实体属性中使用自定义注解进行校验(此处以UserAddDTO为参考)

// 在UserAddDTO中补充字段并引入自定义校验规则
@NotNull(message = "idCard不能为空")
@IdentityCardNumber(message = "idCard校验失败")
private String idCard;

​ 接口请求测试,分别测试传入idCard、不传idCard、传入错误格式的idCard的情况,在校验代码中打断点,进而验证自定义校验器是否生效

image-20240611202421407

扩展说明:Validator框架提供的校验对参数的校验并没有按照限定的顺序,例如此处定义了@NotNull、@IdentityCardNumber校验规则,正常逻辑下可能会理所当然地希望它先进行非空校验然后再进行自定义的idCard校验规则,但实际运行并不是如此。反而是先进入了@IdentityCardNumber(如果在自定义校验器中不进行空指针处理、非空校验,而是直接引用Object对象,则必然会抛出空指针异常。因此在自定义校验器中是先对Object为空的情况默认放行,只有非空才进行校验),具体校验顺序还需结合实际的源码分析,看其是按照单个属性注解的定义顺序、还是以注解进行分组进行校验

​ 此处每种场景的的校验规则说明如下:

  • 当不传入idCard,先进入自定义校验的逻辑进行判断(Object为空,放行返回true),随后进入框架自带的@NotNull校验(发现字段为空,则校验不通过)
  • 当传入idCard,先进入自定义的校验逻辑判断(Object不为空,校验格式是否匹配,匹配返回true,不匹配返回false)
    • 如果返回true,则自定义校验逻辑通过,进入其他校验规则判断
    • 如果返回false,则自定义校验逻辑不通过,直接抛出异常

​ 结合上述分析,此处可能会存在一个疑问,在自定义规则下一些原生的注解校验是不是就失去了意义(看起来好像一些场景下部分注解校验不生效了?),那是不是将所有的操作直接在自定义校验器完成即可?但这种情况本质上是校验规则的先后判断的顺序问题,要做的是理解其校验流程和原理,然后调整自定义校验逻辑

4.分组校验

​ 从上述案例中可以看到,当对一个请求的实体类进行参数校验的时候,校验的规则是定义在实体上的。在实际的场景中很可能是要对同一个对象进行“复用”,所谓“复用”可以从最简单的新增和修改操作进行理解,例如针对同一个对象,新增的时候不需要校验userId、修改的时候需要校验userId,那么这里则需要进行区分判断,此处有多种思路达到相应的目的:

​ 思路1:对象复用(新增和修改使用同一个对象实体),借助Valiator的分组校验规则来进行分析区分

​ 思路2:尽量避免对象复用(新增和修改分别使用各自的对象实体,更具备灵活性和可维护性),例如新增使用XXXAddDTO、XXXUpdateDTO

​ 其中思路2和一般的校验规则定义无异,正常区分不同的请求对象实体的校验规则即可,而思路1则是需要引入”校验分组“的概念来区分不同场景下对同一个对象的复用

分组校验构建思路

  • 定义分组规则(例如Add、Update)
  • 对复用对象的属性校验进行分组(每个属性属于哪个分组)
  • 对要校验的对象进行分组(对象属于哪个分组,则相应执行对应分组的属性校验)

案例参考

​ 分组定义:分别定义两个接口Create、Update,区分不同分组

import javax.validation.groups.Default;
public interface Create extends Default {}
public interface Update extends Default {}

​ 为了区分前面的案例,此处新引入Person类(作为复用对象)进行测试

/**
 * 用户添加实体类定义
 */
@Data
public class Person implements Serializable {

    private static final long serialVersionUID = 1L;

    // 用户ID
    @NotNull(message = "用户ID不能为空", groups = {Update.class})
    private Integer id;

    // 用户名
    @NotNull(message = "用户名不能为空")
    @Length(min = 6,max = 20,message = "用户名设定在6-20个字符", groups = {Create.class, Update.class})
    private String username;

    @NotNull(message = "创建时间不能为空")// 走默认分组
    @Future(message = "时间必须是将来时间", groups = {Create.class})
    private Date createTime;

}

​ 新增接口进行区分测试

@RestController
@RequestMapping("/person")
public class PersonController {
    // 添加
    @GetMapping("/add")
    public String add(@RequestBody @Validated(Create.class) Person person){
        System.out.println("模拟数据操作完成..." + person.toString());
        return "success";
    }

    // 修改
    @GetMapping("/update")
    public String update(@RequestBody @Validated(Update.class) Person person){
        System.out.println("模拟数据操作完成..." + person.toString());
        return "success";
    }
  
    // 默认分组
    @GetMapping("/defaultGroup")
    public String defaultGroup(@RequestBody @Validated Person person){
        System.out.println("模拟数据操作完成..." + person.toString());
        return "success";
    }
}

​ 接口请求:分别组合不同的请求方式验证参数校验

{
    "id":"001",
    "username":"小白",
    "createTime": "2024-06-12"
}

​ 基于上述配置可以看到,针对当对请求对象指定@Validated校验中明确指定了所属分组,则会按照对应分组规则依次校验对应分组的属性校验规则。且因为Create、Update都继承了Default(javax.validation.groups.Default),则针对一些没有显式指定分组的校验规则(例如此处案例中的@NotNull(message = "创建时间不能为空"),其默认分组是Default.class)也会被归到对应的分组(因为分组继承了Default,则归属于默认分组的校验规则也是属于对应分组的)。

​ 换句话说,如果分组接口没有继承Default,则一些没有限制指定归属某个分组的校验规则,其默认归属于Default,即与其他分组无关,自然也就不会加入相应的分组校验。(理解这个点,也就更好地排查一些场景中为什么会出现校验规则失效的问题)

​ 例如上述defaultGroup接口,@Validated没有明确指定哪个分组,则其归属于默认分组,也就是说只校验默认分组的校验规则。此时再看Person的校验规则定义,属于默认分组的校验规则只有username、createTime的@NotNull注解校验,其他校验规则都明确指定了分组,即访问defaultGroup接口只会校验username、createTime的非空性

5.Restful风格

在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated

@GetMapping("/get")
public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) {
  User user = userService.selectById(userId);
  if (user == null) {
    return new RspDTO<User>().nonAbsent("用户不存在");
  }
  return new RspDTO<User>().success(user);
}
@RestController
@RequestMapping("user/")
@Validated
public class UserController extends AbstractController {
	.....
}

校验方式的选择

​ 综上,对比传统校验方式和引入Validator框架这两种校验规则,应结合实际业务场景选择合适的参数校验方式。两种方式都具备其相应的优缺点:

  • 传统串行方式校验
    • 灵活、实现思路简单,不需要依赖第三方框架
    • 可支持复杂的业务交互的校验
    • 缺点:随着业务扩展,如果需要校验的字段属性越来越多就会导致校验代码越来越臃肿
  • 引入Validator框架校验
    • 针对简单字段属性的校验可快速、方便地实现校验
    • 提供单个参数校验、自定义注解校验器、分组校验方式等扩展入口支撑不同业务场景的校验
    • 缺点:需要依赖第三方框架,需要对框架的使用和错误排查有一定的了解,使用上有一定的门槛(例如在上述的案例中排查校验问题的时候一些场景无法直接通过断点定位代码校验片段,给程序运维带来一定的难度)

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3