前言
我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写
public String add(UserVO userVO) {
if(userVO.getAge() == null){
return "年龄不能为空";
}
if(userVO.getAge() > 120){
return "年龄不能超过120";
}
if(userVO.getName().isEmpty()){
return "用户名不能为空";
}
// 省略一堆参数校验...
return "OK";
}
业务代码还没开始写呢,光参数校验就写了一堆判断。这样写虽然没什么错,但是给人的感觉就是:不优雅,不专业,代码可读性也很差,一看就是新手写的代码
作为久经战争的老司机怎么能这样呢,大神是不允许这样代码出现的,其实SpringBoot提供整合了参数校验解决方案spring-boot-starter-validation
整合使用
在SpringBootv2.3之前的版本只需要引入 web 依赖就可以了他包含了validation校验包在此之后SpringBoot版本就独立出来了需要单独引入依赖
org.springframework.boot
spring-boot-starter-validation
内置的校验注解有很多,罗列如下:
注解 | 校验功能 |
---|---|
@AssertFalse | 必须是false |
@AssertTrue | 必须是true |
@DecimalMax | 小于等于给定的值 |
@DecimalMin | 大于等于给定的值 |
@Digits | 可设定最大整数位数和最大小数位数 |
校验是否符合Email格式 | |
@Future | 必须是将来的时间 |
@FutureOrPresent | 当前或将来时间 |
@Max | 最大值 |
@Min | 最小值 |
@Negative | 负数(不包括0) |
@NegativeOrZero | 负数或0 |
@NotBlank | 不为null并且包含至少一个非空白字符 |
@NotEmpty | 不为null并且不为空 |
@NotNull | 不为null |
@Null | 为null |
@Past | 必须是过去的时间 |
@PastOrPresent | 必须是过去的时间,包含现在 |
@Pattern | 必须满足正则表达式 |
@PositiveOrZero | 正数或0 |
@Size | 校验容器的元素个数 |
单个参数校验
使用很简单只需要在需要校验controller上加上@Validated
注解在需校验参数上加上@NotNull,@NotEmpty
之类参数校验注解就行了,
@Validated
@GetMapping("/home")
public class ProductController {
public Result index(@NotBlank String name, @Email @NotBlank String email) {
return ResultResponse.success();
}
}
对象参数校验
在上面的基础上只需要在对象参数前面加上@Validated
注解,然后在需要校验的对象参数的属性上面加上
@NotNull,@NotEmpty
之类参数校验注解就行了,
@PostMapping("/user")
public Result index1(@Validated @RequestBody UserParams userParams) {
log.info("info test######");
log.error("error test #####");
return ResultResponse.success(userParams);
}
@Data
public class UserParams {
@NotBlank
private String username;
private int age;
@NotBlank
private String addr;
@Email
private String email;
}
参数校验异常信息处理
上面我们进行了参数校验,默认当参数校验没通过后会通过异常方式来抛出错误信息MethodArgumentNotValidException
是在校验时抛出的还包括很多其他异常信息,这时我们可以通过全局异常捕获信息来处理这些参数校验异常
全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可
package cn.soboys.core;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.soboys.core.authentication.AuthenticationException;
import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultCode;
import cn.soboys.core.ret.ResultResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author kenx
* @version 1.0
* @date 2021/6/17 20:19
* 全局异常统一处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 json 请求体调用接口对象参数校验失败抛出的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result jsonParamsException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
List errorList = CollectionUtil.newArrayList();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
String msg = String.format("%s%s;", fieldError.getField(), fieldError.getDefaultMessage());
errorList.add(msg);
}
return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, errorList);
}
/**
* 处理单个参数校验失败抛出的异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result ParamsException(ConstraintViolationException e) {
List errorList = CollectionUtil.newArrayList();
Set> violations = e.getConstraintViolations();
for (ConstraintViolation> violation : violations) {
StringBuilder message = new StringBuilder();
Path path = violation.getPropertyPath();
String[] pathArr = StrUtil.splitToArray(path.toString(), ".");
String msg = message.append(pathArr[1]).append(violation.getMessage()).toString();
errorList.add(msg);
}
return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, errorList);
}
/**
* @param e
* @return 处理 form data方式调用接口对象参数校验失败抛出的异常
*/
@ExceptionHandler(BindException.class)
public Result formDaraParamsException(BindException e) {
List fieldErrors = e.getBindingResult().getFieldErrors();
List collect = fieldErrors.stream()
.map(o -> o.getField() + o.getDefaultMessage())
.collect(Collectors.toList());
return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, collect);
}
/**
* 请求方法不被允许异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
return ResultResponse.failure(ResultCode.METHOD_NOT_ALLOWED);
}
/**
* @param e
* @return Content-Type/Accept 异常
* application/json
* application/x-www-form-urlencoded
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public Result httpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
return ResultResponse.failure(ResultCode.BAD_REQUEST);
}
/**
* handlerMapping 接口不存在跑出异常
*
* @param e
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result noHandlerFoundException(NoHandlerFoundException e) {
return ResultResponse.failure(ResultCode.NOT_FOUND, e.getMessage());
}
/**
* 认证异常
* @param e
* @return
*/
@ExceptionHandler(AuthenticationException.class)
public Result UnNoException(AuthenticationException e) {
return ResultResponse.failure(ResultCode.UNAUTHORIZED,e.getMessage());
}
/**
*
* @param e 未知异常捕获
* @return
*/
@ExceptionHandler(Exception.class)
public Result UnNoException(Exception e) {
return ResultResponse.failure(ResultCode.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
这里关于全局异常处理请参考我这篇SpringBoot优雅的全局异常处理
这里我返回的是自定义响应体api 请参考我这篇Spring Boot 无侵入式 实现RESTful API接口统一JSON格式返回
当然校验异常的处理还有其它方式,当参数在没有通过校验情况下会帮我们把错误信息注入到BindingResult对象中,会注入到对应controller方法,这个仅限于参数有@RequestBody
或者@RequestParam
修饰的参数才行
public String add1(@Validated UserVO userVO, BindingResult result) {
List fieldErrors = result.getFieldErrors();
if(!fieldErrors.isEmpty()){
return fieldErrors.get(0).getDefaultMessage();
}
return "OK";
}
当然我推荐第一种可以通过全局异常处理的方式统一处理校验异常
如果每个Controller方法中都写一遍对BindingResult信息的处理,使用起来还是很繁琐。代码很冗余
当我们写了@validated注解,不写BindingResult的时候,SpringBoot 就会抛出异常。由此,可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。