1. 前言
平时写接口的时候难免有要对外开放接口调用,如果接口请求体的结构比较简单还好,但是不可避免会遇到一些包含嵌套对象的请求体。
这个时候最烦的就是做参数校验了,一堆的 Objects.isNull
或者 Strings.isNullOrEmpty
。冗长的一堆很不雅观而且基本不太可能复用。
最近突然发现 JSR303 (Bean Validation)
的存在,结合 @ControllerAdvice
定制全局异常可以大大减轻参数校验的负担呢。
简单来说,从参数校验的内容来看可以分为两种:
-
业务无关的参数校验:NotNull、NotEmpty 等等。
-
业务相关的校验:ID 值的有效性等。
对于业务无关的参数校验,如果校验不通过则会抛出相应的异常。对于与业务有关的参数我们可以在校验不通过后抛出运行时异常。
这些异常可以在 @ControllerAdvice
注解的类中统一处理。
2. 代码实例
2.1 依赖引入
pom.xml
文件中首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.2 简单使用
我们首先定义一个接口:
"jobs")
(public Response<String> get( GetRequest request) {
return Responses.successResponse(request.getKeyword());
}
然后是请求体格式:
package com.avaloninc.springvalidationexample.request;
import com.avaloninc.webapi.common.request.base.BaseRequest;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
public class GetRequest extends BaseRequest {
private String keyword;
}
简单来说就是一个只包含一个参数的 GET 请求。请求体的 keyword
字段上面通过 @NotBlank
注解修饰了参数。
而 Controller
上使用 @Valid
参数触发对参数的校验。
2.3 嵌套结构
通常的使用场景不会有这么简单,请求体往往是嵌套的结构。即请求体的实例成员变量往往是一个集合对象或者另一个类的实例:
package com.avaloninc.springvalidationexample.request;
import com.avaloninc.webapi.common.request.base.BaseRequest;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class CreateRequest extends BaseRequest {
private String name;
private List<String> alarmReceivers;
private Resource resource;
private List<File> files;
public static class Resource {
private String account;
private String queue;
}
public static class File {
private String fileName;
private String filePath;
}
}
这种情况下记得在这些成员变量之上同样加上 @Valid
注解即可!
2.3 全局异常处理
对于参数违反的约束都会以异常的形式抛出来,我们可以在一个类中进行全局的异常处理:
package com.avaloninc.springvalidationexample.advice;
import com.avaloninc.webapi.common.response.Response;
import com.avaloninc.webapi.common.response.Responses;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
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.stream.Collectors;
public class GlobalControllerAdvice {
BindException.class)
(
HttpStatus.BAD_REQUEST)
( public Response handle(final BindException ex) {
String errorMsg = ex.getFieldErrorCount() > 0
? ex.getFieldErrors().stream()
.map(this::getFieldErrorMessage).collect(Collectors.joining(" "))
: ex.getMessage();
return Responses.errorResponse(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
MethodArgumentNotValidException.class)
(
HttpStatus.BAD_REQUEST)
( public Response handle(final MethodArgumentNotValidException ex) {
String errorMsg = ex.getBindingResult().getFieldErrorCount() > 0
? ex.getBindingResult().getFieldErrors().stream()
.map(this::getFieldErrorMessage).collect(Collectors.joining(" "))
: ex.getMessage();
return Responses.errorResponse(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
private String getFieldErrorMessage(FieldError err) {
return err.getField() + " " + err.getDefaultMessage() + "!";
}
IllegalArgumentException.class)
(
HttpStatus.BAD_REQUEST)
( public Response handle(final IllegalArgumentException ex) {
return Responses.errorResponse(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
}
Exception.class)
(
HttpStatus.INTERNAL_SERVER_ERROR)
( public Response handle(Exception ex) {
Response response = Responses.errorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
log.error("Internal Server Error with trace id {}.", response.getMeta().getTraceId(), ex);
return response;
}
}
如上面的代码所示,参数校验不通过抛出的异常基本可以总结为:BindException
和 MethodArgumentNotValidException
两种。我们可以从 FieldError
中取出违反约束的参数名以及对应的错误提示!
对于业务相关的参数我们可以用 IllegalArgumentException
来进行异常的抛出和捕获。
最后使用了一个 Exception
来捕获所有没有处理的异常,进行统一的错误日志记录和错误信息的返回。
3. 总结
其实这里只是做了一个最简单的罗列,具体的注解的功能以及使用的类型大家可以从参考资料进一步学习。
借助于 Bean Validation
毕竟参数校验可能包含了一定的业务逻辑,全部放在注解中是否合适还有待商榷。