SpringBoot系列之表单参数校验整理

Posted smileNicky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot系列之表单参数校验整理相关的知识,希望对你有一定的参考价值。

1、前言

表单的校验在一些对接的接口,要求比较多,使用较多的是Hibernate的表单校验进行JSR-303验证,在springboot项目中,有封装的spring-boot-starter-validation这个starter,也是基于Hibernate的表单校验,下面通过一个RestFul风格的接口来看看

2、环境搭建

开发环境

  • JDK 1.8
  • SpringBoot2.2.1
  • MybatisPlus3.4.3.4
  • Maven 3.2+
  • mysql5.7.36
  • 开发工具
    • IntelliJ IDEA
    • smartGit

使用阿里云提供的脚手架快速创建项目:
https://start.aliyun.com/bootstrap.html

也可以在idea里,将这个链接复制到Spring Initializr这里,然后创建项目

jdk版本选择jdk8


加上必要的pom配置

表单校验选择Validation

也可以创建一个springboot项目之后,自己在pom配置文件加上配置:

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

3、RestFul例子

下面以一个用户注册的例子,写一些RestFul的api接口

package com.example.validated.model.dto;


import com.example.validated.common.validated.EnumValueValidator;
import com.example.validated.model.enumCls.SexTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserDto implements Serializable 

    private Long id;

    @NotBlank( message = "用户名name必须传!")
    private String name;

    private Integer age;

    @Email( message = "email invalid!")
    @NotBlank(message = "邮箱email必须传!")
    private String email;

    @NotBlank( message = "手机号contactNumber必须传")
    @Pattern(regexp = "1[3|4|5|7|8][0-9]\\\\d8" ,message = "手机号格式不对")
    private String contactNumber;

    @NotBlank(message = "密码password必须传!")
    @Size(min = 6 ,message = "密码password必须6位以上")
    private String password;

    @EnumValueValidator(enumClass = SexTypeEnum.class , enumMethod = "isValueValid" ,message = "性别类型校验有误!")
    private Integer sex;



启动校验的一般有两种方法:

  1. @Valid加在请求正文之前,可以加在@RequestBody注解之前
  2. @Validated 是JSR-303 的@Valid的变体,可以支持组校验,什么是组校验?
@PostMapping(value = "/user")
public ResultBean<User> save(@Valid @RequestBody UserDto userDto) 
    User user = BeanUtil.copyProperties(userDto , User.class);
    boolean flag = userService.save(user);
    if (flag) return ResultBean.ok(user);
    return ResultBean.badRequest("新增失败");

在postman调用测试接口:

默认返回的异常信息:


    "timestamp": "2022-01-12T06:37:45.076+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        
            "codes": [
                "NotBlank.userDto.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                
                    "codes": [
                        "userDto.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                
            ],
            "defaultMessage": "姓名必须传!",
            "objectName": "userDto",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        
    ],
    "message": "Validation failed for object='userDto'. Error count: 1",
    "path": "/api/user"

所以可以自己定义一个全局的异常类,捕获MethodArgumentNotValidException异常信息:

package com.example.validated.common.exception;
import com.example.validated.common.rest.ResultBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler 


    @ResponseBody
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ResultBean<?> exception(Exception e) 
        log.error("服务器错误," , e);
        return ResultBean.serverError(e.getMessage());
    

    @ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultBean<?> exception(MethodArgumentNotValidException e) 
        log.error("参数校验异常," , e.getMessage());
        List<String> res = e.getBindingResult().getAllErrors().stream()
                .map(err -> String.format("%s->%s", ((FieldError) err).getField()  , err.getDefaultMessage()))
                .collect(Collectors.toList());
        return ResultBean.badRequest("参数校验异常" , res);
    


自定义的ResultBean类:

package com.example.validated.common.rest;

import lombok.Data;
import org.springframework.http.HttpStatus;

@Data
public class ResultBean<T> 

	/**
	 * 状态
	 * */
	private int status;
	/**
	 * 描述
	 * */
	private String desc;
	/**
	 * 数据返回
	 * */
	private T data;

	public ResultBean(int status, String desc, T data) 
		this.status = status;
		this.desc = desc;
		this.data = data;
	

	public ResultBean(T data) 
		this.status = HttpStatus.OK.value();
		this.desc = "处理成功";
		this.data = data;
	

	public static <T> ResultBean<T> ok(T data) 
		return new ResultBean(data);
	

	public static <T> ResultBean<T> ok() 
		return new ResultBean(null);
	

	public static <T> ResultBean<T> badRequest(String desc,T data) 
		return new ResultBean(HttpStatus.BAD_REQUEST.value(), desc, data);
	

	public static <T> ResultBean<T> badRequest(String desc) 
		return new ResultBean(HttpStatus.BAD_REQUEST.value(), desc, null);
	

	public static <T> ResultBean serverError(String desc, T data)
		return new ResultBean(HttpStatus.INTERNAL_SERVER_ERROR.value(),"服务器内部异常:"+desc,data);
	

	public static <T> ResultBean serverError(String desc)
		return new ResultBean(HttpStatus.INTERNAL_SERVER_ERROR.value(),"服务器内部异常:"+desc,null);
	


不自定义ResultBean类,可以加到ResponseEntity类:

@ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers, HttpStatus status, WebRequest request) 

        Map<String, Object> body = new HashMap<>(16);
        body.put("timestamp", new Date());
        body.put("status", status.value());

        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(err -> String.format("%s->%s", ((FieldError) err).getField()  , err.getDefaultMessage()))
                .collect(Collectors.toList());

        body.put("errors", errors);
        return new ResponseEntity<>(body, headers, status);
    

4、@Valid@Validated区别

在 Spring 中,我们喜欢使用 JSR-303 的@Valid注解进行校验,同时还有@Validated这个注解,两个注解看起来差不多,然后具体有什么差别?现在对比一下

  • 注解地方
    • @Validated:可以用在类型、方法和方法参数上,但是不能用在成员属性上
    • @Valid:可以用在方法、构造函数、方法参数和成员属性

综上其实最明显的区别还是能不能用在成员属性上,@Validated是不支持的,在IDEA里加上去试试,马上会变红提示

  • 分组校验
    组校验,@Valid是不支持的,但是可以使用@Validated实现,举个场景,假如一个接口要进行分阶段同步数据,第一步同步基本的信息,第二步再同步附加信息,但是@Valid是不支持这种分组校验的,下面例子参考https://www.baeldung.com/spring-valid-vs-validated

新增一个基础信息的接口

package com.example.validated.interfaces
// copy from https://github.com/eugenp/tutorials/blob/master/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/springvalidation/interfaces/BasicInfo.java
public interface BasicInfo 
    // validation group marker interface


所有用户信息接口

package com.example.validated.interfaces

public interface UserInfo 
    // validation group marker interface


原来的DTO类,加上groups 进行分组

package com.example.validated.model.dto;
import com.example.validated.common.validated.EnumValueValidator;
import com.example.validated.interfaces.BasicInfo;
import com.example.validated.interfaces.UserInfo;
import com.example.validated.model.enumCls.SexTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserGroupDto implements Serializable 

    private Long id;

    @NotBlank( message = "用户名name必须传!" ,groups = BasicInfo.class , UserInfo.class)
    private String name;

    @NotBlank(message = "密码password必须传!")
    @Size(min = 6 ,message = "密码password必须6位以上" ,groups = BasicInfo.class , UserInfo.class)
    private String password;

    private Integer age;

    @Email( message = "email invalid!",groups = UserInfo.class)
    @NotBlank(message = "邮箱email必须传!" ,groups = UserInfo.class)
    private String email;

    @NotBlank( message = "手机号contactNumber必须传" ,groups = UserInfo.class)
    @Pattern(regexp = "1[3|4|5|7|8][0-9]\\\\d8" ,message = "手机号格式不对",groups = UserInfo.class)
    private String contactNumber;

    @EnumValueValidator(enumClass = SexTypeEnum.class , enumMethod = "isValueValid" ,message = "性别类型校验有误!",groups = UserInfo.class)
    private Integer sex;



@Validated只指定BasicInfo这个组的校验,也只校验用户名和密码,其它的不校验,@Validated(BasicInfo.class)

@PostMapping(value = "/user/basic")
    public ResultBean<User> saveBasic(@Validated(BasicInfo.class) @RequestBody UserGroupDto userDto) 
        User user = BeanUtil.copyProperties(userDto , User<

以上是关于SpringBoot系列之表单参数校验整理的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot系列之自定义枚举类的数据校验注解

SpringBoot系列之自定义枚举类的数据校验注解

springboot之validator校验

SpringBoot系列之对Excel报表的校验提示

SpringBoot系列之对Excel报表的校验提示

应用二:Vue之ElementUI Form表单校验