概述
- spring boot参数校验框架validator的使用以及优雅的异常处理方式。
- 在spring boot项目中,为了让rest接口更加的稳定,健壮,避免非法参数造成的系统未知异常,因此对传入的参数进行校验是非常有必要的。接下来分享一下比较友好的校验方式。
校验框架[hibernate-validator]
介绍
- 来自官网的介绍 Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.
- 大概翻译过来就是,使用基于注解的标准化的式表达式验证规则,并受益于与各种框架的透明集成。
- 自带的注解如下:
spring boot集成
- 1.maven添加依赖,如果spring boot在2.3.0以下,忽略此步骤,因为2.3.0以下的版本已经集成有validator模块
<!--spring boot 2.3.0以上需要手动引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 2.添加代码
//传输对象dto
@Data
public class DemoDTO implements Serializable {
private static final long serialVersionUID = 1019466745376831818L;
@NotNull(message = "字段a不允许为空")
private Integer a;
@NotBlank(message = "字段b不允许为空")
private String b;
}
//统一结果返回
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
private static final long serialVersionUID = 4921114729569667431L;
//状态码,200为成功,其它为失败
private Integer code;
//消息提示
private String message;
//数据对象
private T data;
//成功状态码
public static final int SUCCESS = 200;
//失败状态码
public static final int ERROR = 1000;
public static <R> Response<R> success(R data) {
return new Response<>(SUCCESS, "success", data);
}
public static <R> Response<R> error(String msg) {
return new Response<>(ERROR, msg, null);
}
}
//控制器
@RestController
public class DemoController {
@PostMapping(value = "/check")
public Response<Boolean> check(@Valid @RequestBody DemoDTO dto) {
return Response.success(true);
}
}
- 3.启动服务,访问 http://localhost:8080/check
//请求参数:
{
"a": null,
"b": null
}
//返回结果:
{
"timestamp": "2021-03-05T09:05:35.780+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/check"
}
说明能正常拦截,但是这个提示不友好,该如何优化呢?往下会具体说明。
自定义校验器
- 1.默认的校验器,并不能满足所有的需求,比如我要限制传入的字符串长度在1到20之间,但是要求中文一个汉字按两个字符计算,即最多只能输入10个汉字,默认的@Size无法解决这问题;又比如,传入的参数只能是固定的枚举值,又该怎么办呢?所以,就需要到自定义注解出马了。
- 2.添加代码
//字符长度校验注解(中文占两个字符)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = LengthValidator.class)
public @interface Length {
boolean required() default true;
String message() default "字符串长度不在范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default Integer.MAX_VALUE;
}
//字符长度校验器(中文占两个字符)
public class LengthValidator implements ConstraintValidator<Length, String> {
private Length length;
@Override
public void initialize(Length constraintAnnotation) {
this.length = constraintAnnotation;
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
int len = this.calLen(s);
return len >= length.min() && len <= length.max();
}
private int calLen(String s) {
if (null == s) {
return 0;
}
//中文占两个字符长度
return s.trim()
.replaceAll("[^\\x00-\\xff]", "**")
.length();
}
}
//范围枚举值
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = RangeEnumValidator.class)
public @interface RangeEnum {
boolean required() default true;
String message() default "枚举值错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] allowableValues() default {};
}
//范围枚举值校验器
public class RangeEnumValidator implements ConstraintValidator<RangeEnum, Object> {
private RangeEnum range;
@Override
public void initialize(RangeEnum constraintAnnotation) {
this.range = constraintAnnotation;
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
String[] allowableValues = range.allowableValues();
if (null == o || allowableValues.length == 0) {
return false;
}
String s = o.toString();
for (String allowableValue : allowableValues) {
if (s.equals(allowableValue)) {
return true;
}
}
return false;
}
}
//传输对象dto
@Data
public class Demo2DTO implements Serializable {
private static final long serialVersionUID = 1019466745376831818L;
@Length(min = 1, max = 5, message = "字段c长度在1到5个字符之间")
private String c;
@RangeEnum(allowableValues = {"x", "y"}, message = "字段d允许取值为:x,y")
private String d;
}
//控制器
@RestController
public class DemoController {
@PostMapping(value = "/check2")
public Response<Boolean> check2(@Valid @RequestBody Demo2DTO dto) {
return Response.success(true);
}
}
- 3.启动服务,访问 http://localhost:8080/check2
//请求参数:
{
"c": null,
"d": null
}
//返回结果:
{
"timestamp": "2021-03-05T09:05:35.780+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/check2"
}
优雅的异常处理
- 1.前面我们已经看到,参数非法,提示十分不友好,请求方根本不知道哪里出了问题,为了解决这个问题,我们需要使用到
org.springframework.web.bind.annotation.RestControllerAdvice
,此注解允许我们捕获全局通知,配合org.springframework.web.bind.annotation.ExceptionHandler
拦截异常,并对异常做自定义处理,然后响应请求。 - 2.添加代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response validExceptionHandler(MethodArgumentNotValidException e) {
print(e);
StringBuilder buf = new StringBuilder();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
buf.append(",").append(error.getDefaultMessage());
}
return Response.error(buf.length() > 0 ? buf.substring(1) : "参数校验失败");
}
/**
* 处理Exceptionn异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Response handleException(Exception e) {
print(e);
String message = e.getMessage();
if (null == message || "".equals(message)) {
message = e.getClass().getSimpleName();
}
return Response.error(message);
}
private void print(Exception e) {
log.error(" Exception : {}", e.getClass().getSimpleName(), e);
}
}
参数校验不通过,会抛出MethodArgumentNotValidException
异常,我们通过添加@ExceptionHandler(MethodArgumentNotValidException.class)
注解,即可以对参数校验异常进行处理
- 3.启动服务,访问 http://localhost:8080/check2
//请求参数:
{
"a": null,
"b": null
}
//返回结果:
{
"code": 1000,
"message": "字段d允许取值为:x,y,字段c长度在1到5个字符之间",
"data": null
}
- 码云 https://gitee.com/hweiyu/spring-boot-validator.git