java web项目如何优雅的进行入参的校验
前言
之前看过有一个同事写的代码,他为了进行细粒度的返回值提示,针对每一种参数不合法的情况,都规定了一个code值,然后在service层去进行各种校验,捕捉各种异常,然后返回给controller不同的code码。controller再根据这些code码,返回不同的错误提示。
他这样做可以改进的地方有两处:
- service层先根据错误类型返回不同的code码,controller再根据不同的code码返回不同的文案。进行了两步处理,代码量显得特别大。
- 在service层捕捉了大量异常,造成事务失效,为后人埋下了很多坑。
javax.validation
介绍
javax.validation 是基于JSR-303标准开发出来的,使用注解方式实现,及其方便,但是这只是一个接口,没有具体实现。
Hibernate-Validator是一个hibernate独立的包,可以直接引用,他实现了javax.validation同时有做了扩展,比较强大。
SpringBoot在spring-boot-starter-web中引入了hibernate-validator,所以我们在springboot的web项目中可以直接使用。
具体使用
我就直接堆代码了, 由于代码比较多,所以用了一种折叠的方式,不过貌似只有chrome等少数的几个浏览器才支持。
返回值的包装类:
展开源码
public class ResponseData implements Serializable {
private String code;
private String message;
private T data;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static class ResponseDataBuilder {
private ResponseData responseData;
public ResponseDataBuilder() {
responseData = new ResponseData<>();
}
public ResponseDataBuilder code(String code) {
responseData.setCode(code);
return this;
}
public ResponseDataBuilder message(String message) {
responseData.setMessage(message);
return this;
}
public ResponseDataBuilder data(T data) {
responseData.setData(data);
return this;
}
public ResponseDataBuilder success() {
responseData.setCode("0000");
responseData.setMessage("请求成功");
return this;
}
public ResponseData build() {
return responseData;
}
}
}
请求实体类:
展开源码
public class Phone {
@NotEmpty(message = "手机名称不能为空")
private String name;
private String brand;
@Min(value = 1, message = "电话尺寸最小为1")
@Max(value = 10, message = "电话尺寸最大为10")
private Integer size;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}
可以看到上面的代码中对字段"name"进行了非空校验,对字段"size"进行的大小的限制。
controller:
展开源码
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/a")
public ResponseData test(@RequestBody @Validated Phone phone) {
return new ResponseData.ResponseDataBuilder<>().success().build();
}
}
统一的异常处理类,主要用来捕捉所有的validate异常,统一返回值的结构:
展开源码
@ControllerAdvice
public class MyExceptionHandler {
private Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
@ExceptionHandler(ValidationException.class)
@ResponseBody
public ResponseData handleException(ValidationException exception) {
String errorMessage = exception.getMessage();
log.error(errorMessage);
return new ResponseData.ResponseDataBuilder<>().code("9999").message(errorMessage).build();
}
@ExceptionHandler(BindException.class)
@ResponseBody
public ResponseData handleException(BindException exception) {
String errorMessageStr = getErrorMessageStr(exception);
log.error(errorMessageStr);
return new ResponseData.ResponseDataBuilder<>().code("9999").message(errorMessageStr).build();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResponseData handleException(MethodArgumentNotValidException exception) {
String errorMessageStr = getErrorMessageStr(exception);
log.error(errorMessageStr);
return new ResponseData.ResponseDataBuilder<>().code("9999").message(errorMessageStr).build();
}
private String getErrorMessageStr(BindException exception) {
List errorMessages = exception.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
String errorMessageStr = errorMessages.stream().filter(message -> !StringUtils.isEmpty(message)).collect(Collectors.joining("--"));
return errorMessageStr;
}
private String getErrorMessageStr(MethodArgumentNotValidException exception) {
List errorMessages = exception.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
String errorMessageStr = errorMessages.stream().filter(message -> !StringUtils.isEmpty(message)).collect(Collectors.joining("--"));
return errorMessageStr;
}
}
我们怎么简单的运用
上面介绍的方法,可以对入参进行非业务的校验。但是在实际的项目中,我们还会遇到业务型的校验,比如:注册的时候判断用户名是否重复,这个时候我们就可以借鉴上面的思想进行一些优雅的校验。
自定义一个校验异常
展开源码
public class MyValidateException extends RuntimeException {
public MyValidateException() {
super();
}
public MyValidateException(String message) {
super(message);
}
public MyValidateException(String message, Throwable cause) {
super(message, cause);
}
public MyValidateException(Throwable cause) {
super(cause);
}
protected MyValidateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
在service层去进行业务校验
展开源码
@Service
public class TestService {
List list = new ArrayList<>(Arrays.asList("Huawei"));
public void test(Phone phone) {
if (list.contains(phone.getName())) {
throw new MyValidateException("手机的名字已存在");
}
}
}
统一的异常处理
展开源码
@ControllerAdvice
public class MyExceptionHandler {
private Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
@ExceptionHandler(MyValidateException.class)
@ResponseBody
public ResponseData handleException(MyValidateException exception) {
String errorMessage = exception.getMessage();
log.error(errorMessage);
return new ResponseData.ResponseDataBuilder<>().code("9999").message(errorMessage).build();
}
}