前言
validation主要是校验用户提交的数据的合法性,比如是否为空,密码是否符合规则,邮箱格式是否正确等等,校验框架比较多,用的比较多的是hibernate-validator, 也支持国际化,也可以自定义校验类型的注解,这里只是简单地演示校验框架在SpringBoot中的简单集成,要想了解更多可以参考 hibernate-validator。
1. pom.xml
org.springframework.boot
spring-boot-starter-validation
2. dto
public class UserInfoIDto {
private Long id;
@NotBlank
@Length(min=3, max=10)
private String username;
@NotBlank
@Email
private String email;
@NotBlank
@Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,3-9]))\\d{8}$", message="手机号格式不正确")
private String phone;
@Min(value=18)
@Max(value = 200)
private int age;
@NotBlank
@Length(min=6, max=12, message="昵称长度为6到12位")
private String nickname;
// Getter & Setter
}
3. controller
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto, BindingResult result){
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
String field = fieldError.getField();
String msg = fieldError.getDefaultMessage();
return field + ":" + msg;
}
System.out.println("开始注册用户...");
return "success";
}
}
4. 去掉BindingResult参数
每个接口都需要BindingResult参数,而且每个接口都需要处理错误信息,这样增加一个参数也不优雅,处理错误信息代码量也很重复。如果去掉BindingResult参数,系统就会报错MethodArgumentNotValidException,我们只需要使用全局异常来捕获该错误,处理一下就可以省略传BindingResult参数了。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("开始注册用户...");
return "success";
}
}
@RestControllerAdvice 用于拦截所有的@RestController
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return objectError.getDefaultMessage();
}
}
[图片上传失败...(image-d7fc73-1628992210760)]
5. 统一返回格式
错误码枚举
@Getter
public enum ErrorCodeEnum {
SUCCESS(1000, "成功"),
FAILED(1001, "响应失败"),
VALIDATE_FAILED(1002, "参数校验失败"),
ERROR(5000, "未知错误");
private Integer code;
private String msg;
ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
自定义异常。
@Getter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException(ErrorCodeEnum errorCodeEnum) {
super(errorCodeEnum.getMsg());
this.code = errorCodeEnum.getCode();
this.msg = errorCodeEnum.getMsg();
}
}
定义返回格式。
@Getter
public class Response {
/**
* 状态码,比如1000代表响应成功
*/
private int code;
/**
* 响应信息,用来说明响应情况
*/
private String msg;
/**
* 响应的具体数据
*/
private T data;
public Response(T data) {
this.code = ErrorCodeEnum.SUCCESS.getCode();
this.msg = ErrorCodeEnum.SUCCESS.getMsg();
this.data = data;
}
public Response(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
全局异常处理器增加对APIException的拦截,并修改异常时返回的数据格式。
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
}
@ExceptionHandler(APIException.class)
public Response APIExceptionHandler(APIException e) {
return new Response<>(e.getCode(), e.getMsg());
}
}
SimpleController 增加一个抛出异常的方法。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("开始注册用户...");
return "success";
}
@GetMapping("/users")
public Response list() {
UserInfoIDto userInfoIDto = new UserInfoIDto();
userInfoIDto.setUsername("monday");
userInfoIDto.setAge(30);
userInfoIDto.setPhone("123456789");
if (true) {
throw new APIException(ErrorCodeEnum.ERROR);
}
// 为了保持数据格式统一,必须使用Response包装一下
return new Response<>(userInfoIDto);
}
}
[图片上传失败...(image-7d13e9-1628992210760)]
报错返回的格式。
[图片上传失败...(image-cff7a0-1628992210760)]
image.png
不报错,返回的格式。
[图片上传失败...(image-2d2b81-1628992210760)]
image.png
6. 去掉接口中的Response包装
@RestControllerAdvice既可以全局拦截异常也可拦截指定包下正常的返回值,可以对返回值进行修改。
@RestControllerAdvice(basePackages = {"com.example.validator.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice
@RestController
public class SimpleController {
@GetMapping("/users")
public UserInfoIDto list() {
UserInfoIDto userInfoIDto = new UserInfoIDto();
userInfoIDto.setUsername("monday");
userInfoIDto.setAge(30);
userInfoIDto.setPhone("123456789");
// 直接返回值,不需要再使用Response包装
return userInfoIDto;
}
}
[图片上传失败...(image-d9a87a-1628992210760)]
7. 每个校验错误都对应不同的错误码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ValidateErrorCode {
/** 校验错误码 code */
int value() default 100000;
}
@Data
public class UserInfoIDto {
@NotBlank
@Email
@ValidateErrorCode(value = 20000)
private String email;
@NotBlank
@Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,3-9]))\\d{8}$", message="手机号格式不正确")
@ValidateErrorCode(value = 30000)
private String phone;
}
校验异常获取注解中的错误码。
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response methodArgumentNotValidException(MethodArgumentNotValidException e) throws NoSuchFieldException {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 参数的Class对象,等下好通过字段名称获取Field对象
Class> parameterType = e.getParameter().getParameterType();
// 拿到错误的字段名称
String fieldName = e.getBindingResult().getFieldError().getField();
Field field = parameterType.getDeclaredField(fieldName);
// 获取Field对象上的自定义注解
ValidateErrorCode annotation = field.getAnnotation(ValidateErrorCode.class);
if (annotation != null) {
return new Response<>(annotation.value(),objectError.getDefaultMessage());
}
// 然后提取错误提示信息进行返回
return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
}
[图片上传失败...(image-c9a27a-1628992210760)]
image.png
[图片上传失败...(image-3f418-1628992210760)]
8. 个别接口不统一包装响应
有时候第三方接口回调我们的接口,我们的接口必须按照第三方定义的返回格式来,此时第三方不一定和我们自己的返回格式一样,所以要提供一种可以绕过统一包装的方式。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseWrap {
}
@RestController
public class SimpleController {
@NotResponseWrap
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("开始注册用户...");
return "success";
}
}
ResponseControllerAdvice 增加一个不包装的条件,配置了@NotResponseWrap注解就跳过包装。
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> aClass) {
return !(returnType.getParameterType().equals(Response.class) || returnType.hasMethodAnnotation(NotResponseWrap.class));
}
[图片上传失败...(image-9776b2-1628992210760)]