查看: 90|回复: 1

Java项目校验参数基于-Spring Validation

[复制链接]

3

主题

9

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-7-16 20:29:06 | 显示全部楼层 |阅读模式

  • 校验参数的必要性
​ 校验参数在项目中是很常见的,在执行业务逻辑层操作之前,都要验证参数的合法性。比如:入参否为空,数据格式、数据长度是否正确等等,一般的写法就是在执行业务逻辑之前写一大推的if-else去进行判断,既不美观也不优雅,更不便于后期维护,影响代码的可读性。
public void userRegister(UserRequestDTO userDTO){
if(Objects.isNull(userDTO)){
     throw new ServiceException("参数为空");
  }
  if(userDTO.getNickName()==null){
     throw new ServiceException("昵称不能为空");
  }
  if(userDTO.getAge()==null){
     throw new ServiceException("年龄不能为空");
   }
   if(userDTO.getEmail()==null){
     throw new ServiceException("邮箱不能为空");
   }
   //校验通过,执行业务逻辑
}这个时候JCP组织站出来了,并且制定了一个标准来规范校验的操作,这个标准就是Java Validation API(JSR 303)

2. 实现方式
Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length、@NotNull、@Max、@Min、@Size等。
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来我们主要介绍Spring Validation的使用。
3. 引入依赖
 如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>或
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency> 4.使用场景
4.1、需求
用户注册接口,名称,年龄,邮箱、不能为空,参数个数较多,使用requestBody传递参数,DTO(Data Transfer Object)数据传输对象接收。
用户修改接口,名称,年龄,邮箱,主键id,不能为空,参数个数较多,使用requestBody传递参数,,DTO(Data Transfer Object)数据传输对象接收。
用户信息接口,入参为单个参数,使用requestParam/PathVariable的方式传递参数。
4.2、编码实现
因为用户注册和用户修改都是使用同一个DTO对象来传输数据,主键ID校验规则不同,因此这里需要使用到spring-validation的分组校验功能
定义新增分组校验接口
/**
* 新增时的验证规则
*
* @author ven
* @since 2023-02-28
*/
public interface ValidAddRules {
}定义修改分组校验接口
/**
* 修改时的验证规则
*
* @author ven
* @since 2023-02-28
*/
public interface ValidUpdateRules {
}定义入参对象UserRequestDTO
/**
* 入参对象UserRequestDTO
*
* @author ven
* @since 2023-02-28
**/
@Data
@Builder
public class UserRequestDTO implements java.io.Serializable {

    private static final long serialVersionUID = -2655536314774756670L;
    /**
     * 主键ID
     */
    @NotNull(message = "id不能为空",groups = {ValidUpdateRules.class})
    @Min(1)
    private Long userId;
    /**
     * 年龄
     */
    @NotNull(message = "年龄不能为空",groups = {ValidUpdateRules.class,ValidAddRules.class})
    @Min(1)
    private Integer age;
   
   
    /**
     * 邮箱
     */
    @NotBlank(message = "邮箱不能为空",groups = {ValidUpdateRules.class,ValidAddRules.class})
    @Email(message = "邮箱格式不正确")
    private String email;

    /**
     * 昵称
     */
    @NotBlank(message = "邮箱不能为空",groups = {ValidUpdateRules.class,ValidAddRules.class})
    private String nickName;

}控制器controller
/**
* 用户控制器
*
* @author ven
* @since 2023-02-28
**/
@RestController
@Validated
public class UserController {

    /**
     * 用户注册
     * @param userRequest 用户注册信息
     * @return Result<String>
     */
    @PostMapping("/user/register")
    public Result<String> registerUser(@Validated(ValidAddRules.class) @RequestBody UserRequestDTO userRequest){
        return Result.data("新增成功");
    }

    /**
     * 根据用户ID获取用户信息
     * @param userId 用户ID
     * @return  Result<UserRequestDTO>
     */
    @PostMapping("/user/get")
    public Result<UserRequestDTO> getUser(@NotNull(message = "用户id不能为空") @Min(1) Long userId){
        return Result.data(UserRequestDTO.builder().build());
    }

    /**
     * 用户修改
     * @param userRequest 用户信息
     * @return Result<String>
     */
    @PostMapping("/user/update")
    public Result<String> updateUser(@Validated(ValidUpdateRules.class) @RequestBody UserRequest userRequest){
        return Result.data("修改成功");
    }
}

05.校验参数快速失败配置
Spring Validation默认会校验完所有字段,然后才抛出异常。我们可以开启Fali Fast模式,一旦校验失败就立即返回。代码如下:
/**
  *参数校验快速失败配置
* @author ven
* @since 2023/02/28
**/
@Configuration
public class ValidatedConfiguration {

    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}6.校验参数统一异常处理
参数校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常,在实际的项目开发中,需要获取参数校验失败的具体原因,这个时候就需要统一异常处理来返回异常信息。代码如下:
/**
* 全局统一异常处理,处理可预见的异常,Order 排序优先级高
*
* @author ven
* @since 2023-02-28
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice{

        /**
         * 业务异常
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(ServiceException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(ServiceException e) {
                log.error("业务异常",e);
                return Result.fail(e.getResultCode(), e.getMessage());
        }

        /**
         * 服务器异常
         * @param e 500异常
         * @return Result
         */
        @ExceptionHandler(Throwable.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public Result<Object> handleError(Throwable e) {
                log.error("服务器异常", e);
                return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, (Objects.isNull(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage()));
        }

        /**
         * 缺少请求参数异常
         * @param e 少参异常
         * @return Result
         */
        @ExceptionHandler(MissingServletRequestParameterException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(MissingServletRequestParameterException e) {
                String message = String.format("缺少必要的请求参数: %s", e.getParameterName());
                log.error("缺少请求参数:{}", e.getParameterName());
                return Result.fail(ResultCode.PARAM_MISS, message);
        }

        /**
         * 请求参数格式错误异常
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(MethodArgumentTypeMismatchException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(MethodArgumentTypeMismatchException e) {
                String message = String.format("请求参数格式错误: %s", e.getName());
                log.error("请求参数格式错误{}", e.getMessage());
                return Result.fail(ResultCode.PARAM_TYPE_ERROR, message);
        }

        /**
         * 参数验证失败异常
         * @param e 非法参数异常
         * @return Result
         */
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(MethodArgumentNotValidException e) {
                return handleError(e.getBindingResult());
        }

        /**
         * 参数绑定失败异常
         * @param e 绑定错误异常
         * @return Result
         */
        @ExceptionHandler(BindException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(BindException e) {
                return handleError(e.getBindingResult());
        }

        private Result<Object> handleError(BindingResult result) {
                FieldError error = result.getFieldError();
                String errorMessage = null != error ? error.getDefaultMessage() : "参数异常";
                log.error("参数验证或绑定异常,ex = {}",errorMessage);
                return Result.fail(ResultCode.PARAM_BIND_ERROR, errorMessage);
        }

        /**
         * 参数验证失败异常
         * @param e 400异常
         * @return Result
         */
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(ConstraintViolationException e) {
                Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
                ConstraintViolation<?> violation = violations.iterator().next();
                String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
                String message = String.format("%s:%s", path, violation.getMessage());
                log.error("参数验证失败{}", message);
                return Result.fail(ResultCode.PARAM_VALID_ERROR, message);
        }

        /**
         * 404没找到请求
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(NoHandlerFoundException.class)
        @ResponseStatus(HttpStatus.NOT_FOUND)
        public Result<Object> handleError(NoHandlerFoundException e) {
                log.error("404没找到请求:{}", e.getMessage());
                return Result.fail(ResultCode.NOT_FOUND, e.getMessage());
        }

        /**
         * 消息不能读取
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(HttpMessageNotReadableException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result<Object> handleError(HttpMessageNotReadableException e) {
                log.error("消息不能读取:{}", e.getMessage());
                return Result.fail(ResultCode.MSG_NOT_READABLE, e.getMessage());
        }

        /**
         * 不支持当前请求方法
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
        @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
        public Result<Object> handleError(HttpRequestMethodNotSupportedException e) {
                log.error("不支持当前请求方法:{}", e.getMessage());
                return Result.fail(ResultCode.METHOD_NOT_SUPPORTED, e.getMessage());
        }

        /**
         * 不支持当前媒体类型
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
        @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
        public Result<Object> handleError(HttpMediaTypeNotSupportedException e) {
                log.error("不支持当前媒体类型:{}", e.getMessage());
                return Result.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage());
        }

        /**
         * 不接受的媒体类型
         * @param e 异常信息
         * @return Result
         */
        @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
        @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
        public Result<Object> handleError(HttpMediaTypeNotAcceptableException e) {
                log.error("不接受的媒体类型:{}", e.getMessage());
                return Result.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED,e.getMessage());
        }

        /**
         * 空指针异常
         * @param ex 异常信息
         * @return Result
         */
        @ExceptionHandler(NullPointerException.class)
        @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
        public Result<Object> handleTypeMismatchException(NullPointerException ex) {
                log.error("空指针异常,{}", (Object) ex.getStackTrace());
                return Result.fail(ResultCode.INTERNAL_SERVER_ERROR,"空指针异常");
        }
}
7.扩展使用
嵌套校验
前面的示例中,UserRequestDTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况可以使用嵌套校验。比如,UserRequestDTO类中带有一个工作类型Job字段对象。需要注意的是,此时DTO类的对应字段必须标记@Valid注解,代码如下:
/**
* 入参对象UserRequestDTO
*
* @author ven
* @since 2023-02-28
**/
@Data
@Builder
public class UserRequestDTO implements java.io.Serializable {

    private static final long serialVersionUID = -2655536314774756670L;
    /**
     * 主键ID
     */
    @NotNull(message = "id不能为空",groups = {ValidUpdateRules.class})
    @Min(1)
    private Long userId;
    /**
     * 年龄
     */
    @NotNull(message = "年龄不能为空",groups = {ValidUpdateRules.class,ValidAddRules.class})
    @Min(1)
    private Integer age;


    /**
     * 邮箱
     */
    @NotBlank(message = "邮箱不能为空",groups = {ValidUpdateRules.class,ValidAddRules.class})
    @Email(message = "邮箱格式不正确")
    private String email;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 工作对象
     */
    @NotNull(groups = {ValidAddRules.class, ValidUpdateRules.class})
    @Valid
    private Job job;

    /**
     * 工作对象
     */
    @Data
    public static class Job {

        /**
         * 主键ID
         */
        @NotNull(message = "id不能为空",groups = {ValidUpdateRules.class})
        private Long jobId;

        /**
         * 工作名称
         */
        @NotNull(message = "工作名称不能为空!",groups = {ValidAddRules.class, ValidUpdateRules.class})
        @Length(min = 2, max = 10, groups = {ValidAddRules.class, ValidUpdateRules.class})
        private String jobName;

        /**
         *职位
         */
        @NotNull(message = "职位不能为空!",groups = {ValidAddRules.class, ValidUpdateRules.class})
        @Length(min = 2, max = 10, groups = {ValidAddRules.class, ValidUpdateRules.class})
        private String position;
    }

}集合校验
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:包装List类型,并声明@Valid注解
public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}controller写法:
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(ValidAddRules.class) ValidationList<UserRequestDTO> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}编码式校验
@Autowired
private javax.validation.Validator globalValidator;

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

    } else {
        for (ConstraintViolation<UserRequestDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}自定义校验
现实中项目的业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。
  自定义spring validation非常简单,假设我们自定义电话号码是否合法的校验注解,主要分为两步:
第一步:自定义注解
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

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

    String message() default "手机格式不合法";

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

    Class<? extends Payload>[] payload() default {};
}第二步:实现ConstraintValidator接口编写约束校验器
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

/**
* 手机号码校验器
*/
public class PhoneValidator implements ConstraintValidator<CheckPhone,String> {

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
        if (phone.length() != 11) {
            return false;
        }
        // 移动号段正则表达式
        Pattern pattern = Pattern.compile("^((13[4-9])|(147)|(15[0-2,7-9])|(178)|(18[2-4,7-8]))\\d{8}|(1705)\\d{7}$");
        if (pattern.matcher(phone).matches()) {
            return true;
        }
        // 联通号段正则表达式
        pattern = Pattern.compile("^((13[0-2])|(145)|(15[5-6])|(176)|(18[5,6]))\\d{8}|(1709)\\d{7}$");
        if (pattern.matcher(phone).matches()) {
            return true;
        }
        // 电信号段正则表达式
        pattern = Pattern.compile("^((133)|(153)|(177)|(18[0,1,9])|(149))\\d{8}$");
        if (pattern.matcher(phone).matches()) {
            return true;
        }
        //虚拟运营商正则表达式
        pattern = Pattern.compile("^((170))\\d{8}|(1718)|(1719)\\d{7}$");
        return pattern.matcher(phone).matches();
    }
}直接@CheckPhone  就可以来使用了,代码如下:
@CheckPhone(message = "手机不合法")
public String mobile;常用注解
@NotNull被注释的元素不能为null
@Null 被注释的元素必须为null
@NotNull 被注释的元素不能为null,可以为空字符串
@AssertTrue 被注释的元素必须为true
@AssertFalse 被注释的元素必须为false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min) 被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@Length 被注释的字符串的大小必须在指定的范围内
@Range 被注释的元素必须在合适的范围内
@NotEmpty:用在集合类上,不能为null,并且长度必须大于0
@NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
回复

使用道具 举报

3

主题

8

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2023-7-16 20:29:35 | 显示全部楼层
很喜欢你的归纳,让我以后写代码更加的好[思考]
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表