spring validate参数校验

spring validate 实现优雅的参数校验

常用注解

@Valid:用于方法参数,表示该参数需要进行数据校验。
@NotNull:用于字段,表示该字段不能为null。
@NotEmpty:用于集合或字符串字段,表示该字段不能为null并且长度/大小必须大于0。
@NotBlank:用于字符串字段,表示该字段不能为null并且去除首尾空格后的长度必须大于0。
@Size(min=, max=):用于字符串或集合字段,表示该字段的长度/大小必须在指定的范围内。
@Min(value=) 和 @Max(value=):用于数值字段,表示该字段的值必须在指定的范围内。
@Pattern(regexp=):用于字符串字段,表示该字段的值必须匹配指定的正则表达式。
@Email:用于字符串字段,表示该字段的值必须是一个有效的电子邮件地址。
@Past 和 @Future:用于日期字段,表示该字段的值必须是一个过去的日期或一个未来的日期。
@Positive 和 @Negative:用于数值字段,表示该字段的值必须是正数或负数。

引入依赖

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.1.Final</version>
</dependency>

对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  1. POST、PUT请求,使用requestBody传递参数;
  2. GET请求,使用requestParam/PathVariable传递参数。

requestBody参数校验

POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。

DTO表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在spring-web项目中可以表示用于接收请求参数的Bean对象。

@Data
public class LoginFormDTO {

    @NotNull(message = "手机号不能为空")
    @Length(min = 11, max = 11, message = "手机号格式不正确")
    private String phone;

    @NotNull(message = "密码不能为空")
    @Length(min = 6, max = 20, message = "密码格式不正确")
    private String password;

    @NotNull(message = "验证码不能为空")
    @Length(min = 4, max = 4, message = "验证码格式不正确")
    private String code;
}

在方法参数上声明注解:

@PostMapping("easyLogin")
@ApiOperation("简单登录")
@AuthAccess
public Result easyLogin(@RequestBody @Validated LoginFormDTO loginFormDTO) throws SerialException {
    if (StrUtil.isBlank(loginFormDTO.getPhone()) || StrUtil.isBlank(loginFormDTO.getPassword())) {
        return Result.Failed("输入数据不合法");
    }
    return this.userInfoService.easyLogin(loginFormDTO);
}

这种情况下使用@validated和@valid都可以

requestParam/PathVariable参数校验

GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。代码示例如下:

@PostMapping("/sendCode")
    @ApiOperation("发送验证码")
    @AuthAccess
    public Result sendCode(@ApiParam("手机号")
                           @RequestParam
                           @Length(min = 11, max = 11, message = "手机号格式不正确")
                           @NotNull String phone) {
        if (log.isInfoEnabled()) {
            log.info("userController.sendCode.sendCode.phone:{}", phone);
        }
        return userInfoService.sendCode(phone);
    }

统一异常处理

如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。

package com.cqie.datacommon.exception;

import com.cqie.datacommon.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

/**
 * @Author QingYuQiao
 * @date 2024/5/6  11:20
 * 参数校验异常处理类
 */
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {

    /**
     * 方法参数校验异常处理
     * @param exception
     * @return
     */
    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public Result handlerMethodArgumentNotValidException(MethodArgumentNotValidException exception){
        BindingResult bindingResult = exception.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for(FieldError fieldError : bindingResult.getFieldErrors()){
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(",");
        }
        String msg = sb.toString();
        log.error(msg);
        return Result.Failed(msg);
    }

    /**
     * 约束校验异常处理(外键)
     * @param exception
     * @return
     */
    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public Result handlerConstraintViolationException(ConstraintViolationException exception){
        return Result.Failed("参数校验失败:" + exception.getMessage());
    }

}

进阶使用

分组校验

package com.cqie.datauser.entity.dto;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;

/**
 * @Author QingYuQiao
 * @date 2024/3/18  10:11
 */
@Data
public class UserInfoDTO {

    /**
     * 自增主键
     */
    @TableId(type = IdType.AUTO)
    @ApiModelProperty("id")
    @NotNull(groups = {Update.class})
    private Integer id;
    /**
     * 用户名
     */

    @ApiModelProperty("用户名")
    @NotNull(message = "密码不能为空", groups = {Save.class})
    private String userName;
    /**
     * 密码
     */

    @ApiModelProperty("密码")
    @NotNull(message = "密码不能为空", groups = {Save.class, ResetPassword.class})
    @Length(
            groups = {Save.class, ResetPassword.class, Update.class},
            min = 6,
            max = 16,
            message = "密码长度必须在6-16之间"
    )
    private String password;
    /**
     * 账号
     */

    @ApiModelProperty("账号")
    @NotNull(message = "账号不能为空", groups = {Save.class, Update.class})
    private String account;
    /**
     * 性别(0 - 男 1 - 女 2 - 待确认)
     */

    @ApiModelProperty("性别(0 - 男 1 - 女 2 - 待确认)")
    private Integer sex;
    /**
     * 住址
     */

    @ApiModelProperty("住址")
    private String address;
    /**
     * 电话
     */
    @ApiModelProperty("电话")
    @Length(max = 11, message = "电话长度不能超过11位")
    @NotNull(message = "电话不能为空", groups = {Save.class, Update.class})
    private String phone;

    @ApiModelProperty("头像")
    private String image;

    @ApiModelProperty("token")
    private String token;

    @ApiModelProperty("新密码")
    @NotNull(groups = {ResetPassword.class}, message = "新密码不能为空")
    @Length(groups = {ResetPassword.class}, min = 6, max = 16, message = "新密码长度必须在6-16之间")
    private String newPassword;

    @ApiModelProperty("确认密码")
    @NotNull(groups = {ResetPassword.class}, message = "新密码不能为空")
    @Length(groups = {ResetPassword.class}, min = 6, max = 16, message = "新密码长度必须在6-16之间")
    private String confirmPassword;

    @ApiModelProperty("邮箱")
    private String email;


    /**
     * 分组校验
     */
    public interface Save {
    }

    public interface Update {
    }

    public interface ResetPassword {
    }

    ;
}

在@Validate注解上指定校验分组

@PutMapping("/resetPassword")
@ApiOperation("重置密码")
public Result resetPassword(@RequestBody @Validated(UserInfoDTO.ResetPassword.class) UserInfoDTO userInfoDTO) {
    if (log.isInfoEnabled()) {
        log.info("userController.resetPassword.userInfoDTO:{}", JSON.toJSONString(userInfoDTO));
    }
    return this.userInfoService.resetPassword(userInfoDTO);
}

嵌套校验

DTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。需要注意的是,此时DTO类的对应字段必须标记@Valid注解
嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验。

集合校验

如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:

package com.cqie.datacommon.utils;

import lombok.experimental.Delegate;

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author QingYuQiao
 * @date 2024/5/7  16:11
 */
public class ValidationList<E> implements List<E> {

    @Delegate
    @Valid
    public List<E> list = new ArrayList<>();

    @Override
    public String toString() {
        return list.toString();
    }
}

比如,我们需要一次性保存多个User对象,Controller层的方法可以这么写:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

自定义校验

自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";

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

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

实现ConstraintValidator接口编写约束校验器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

这样我们就可以使用@EncryptId进行参数校验了!

编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入javax.validation.Validator对象,然后再调用其api。

@Autowired
private javax.validation.Validator globalValidator;

// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // 如果校验通过,validate为空;否则,validate包含未校验通过项
    if (validate.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

快速失败

Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

/**
     * spring validate 快速失败配置
     * @return
     */
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //快速失败
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

@Valid和@Validated区别

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Object-v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值