2021-06-01 补充:捕捉文件上传大小限制的异常和参数 key 都不传的情况。
前言
工作的项目中,先前的同事其实已经实现了参数校验异常统一处理。部分实现代码如下。
//全局异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = BaseException.class)
public S2VJSONResult baseHandler(BaseException e) {
return S2VJSONResult.msg(false, 400, e.getMessage());
}
//接口
@PostMapping("/transfer/company")
S2VJSONResult transferCompany(@Validated @RequestBody AdminTransferCompanyVO vo,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return S2VJSONResult.msg(false, 400, bindingResult.getFieldError().getDefaultMessage());
}
//参数实体类
public class AdminTransferCompanyVO {
@NotNull(message = "客户 ID 不能为空")
private List<Integer> companyIds;
@NotNull(message = "目标客户经理 ID 不能为空")
private Integer targetCustomerId;
@NotNull(message = "管理员 ID 不能为空")
private Integer operationCustomerId;
//统一返回响应
public class S2VJSONResult<T> {
private Integer code;//状态码
private Boolean isSuccess;//状态
private String message;//消息
private T data;//数据对象
public static <T> S2VJSONResult<T> all(Boolean success, Integer code,String message,T data) {
return new S2VJSONResult<>(success, code, message, data);
}
public static <T> S2VJSONResult<T> msg(Boolean success, Integer code,String message) {
return new S2VJSONResult<>(success, code, message);
}
整理了一下其思路,大概如下:
- 利用
@RestControllerAdvice
全局处理异常 - Spring MVC 会将验证结果放入
BindResult
中 - 封装报错信息返回统一错误响应
我个人很懒,感觉有些地方还是重复代码了,不够优雅,比如 BindingResult
每个接口都要加,既然是异常应该就能够捕捉到,为什么不全局处理?验证的 message
每个字段都要写,可否在不写的情况下,返回特定模板的报错信息?
全局处理
要解决我的问题,思路也很清晰,只要知道校验失败具体抛出的是什么异常,获取异常中有用的的报错信息,然后利用 @RestControllerAdvice
全局处理一下就好了。
这里遇到个坑,同事写的全局处理类继承了 ResponseEntityExceptionHandler
,而我要特定捕捉的异常都在这里面,所以我一开始的时候,老是报重复处理异常的错误。。。
1. 处理 RequestParam 和 PathVariable 方式
Controller 层代码示例如下,将需要的约束添加到 @RequestParam
注解的前面,此时需要在类上添加注解 @Validated
才能实现验证。
@Controller
@RequestMapping("/report1")
@ResponseBody
@Validated
public class TestController {
@PostMapping("/test1")
S2VJSONResult<String> test1(@Max(3) @RequestParam("id") Integer id){
return S2VJSONResult.all(true, 200, "success", JSON.toJSONString(id));
}
@PostMapping("/test2/{id}")
S2VJSONResult<String> test2(@Max(value = 3, message = "别瞎填!") @PathVariable("id") Integer id){
return S2VJSONResult.all(true, 200, "success", JSON.toJSONString(id));
}
这种情况会抛出 ConstraintViolationException
异常。Spring 会处理这种异常并抛出报错信息,
{
"timestamp": "2021-02-02 11:34:45",
"status": 500,
"error": "Internal Server Error",
"message": "test1.id: 最大不能超过3",
// "message": "test2.id: 别瞎填!",
"path": "/pcg/report1/test1"
}
可以看到 message
字段中有较为完整的报错信息,可以直接拿来用。统一异常处理代码如下,
import javax.validation.ConstraintViolationException;
/**
* 处理 @RequestParam @PathVariable 参数验证失败
*
* @param e
* @return S2VJSONResult<T>
* @Author zrr
* @Date 2021/2/2 10:48
* @throws
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public <T> S2VJSONResult<T> handlerConstraintViolationException(ConstraintViolationException e) {
return S2VJSONResult.msg(false, 400, "参数验证未通过(" + e.getMessage() + ")");
}
另外,用户也存在参数 key 都不传的情况,这个时候的异常也需要捕捉。
import org.springframework.web.bind.MissingServletRequestParameterException;
/**
* 处理 @RequestParam 参数验证失败(整个不传)
* @param e
* @param <T>
* @return
*/
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public <T> S2VJSONResult<T> handlerMissingServletRequestParameterException(MissingServletRequestParameterException e) {
return S2VJSONResult.msg(false, 400, "参数验证未通过(" + e.getMessage() + ")");
}
封装报错信息,统一返回响应的结果如下,
{
"code": 400,
"isSuccess": false,
// "message": "参数验证未通过(test1.id: 最大不能超过3)",
"message": "参数验证未通过(test2.id: 别瞎填!)",
"data": null
}
基本实现了我想要的结果,不需要通过 BindingResult
,只需要添加注解就可以校验参数,并且不写 message
也可以返回较为清楚的报错信息。
但是这只是传参的一种情况,其他的情况抛出的异常还不一样,需要再次处理。
2. 处理实体类接收
接口传入很多参数的时候,通常代码会用实体类接收。Controller 层代码示例如下,这种情况无需在类上添加注解 @Validated
。
@PostMapping("/test3")
S2VJSONResult<String> test3(@Validated ReportVO vo){
return S2VJSONResult.all(true, 200, "success", JSON.toJSONString(vo));
}
实体类添加相应的约束,
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
public class ReportVO implements Serializable {
@NotNull
private Integer cgeId;
@NotEmpty(message = "baseinfoSqrxm 不能为空")
private String baseinfoSqrxm;
}
这种情况会抛出 BindException
异常。Spring 仍然会处理这种异常并抛出报错信息,
{
"timestamp": "2021-02-02 13:58:18",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.reportVO.cgeId",
"NotNull.cgeId",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"reportVO.cgeId",
"cgeId"
],
"arguments": null,
"defaultMessage": "cgeId",
"code": "cgeId"
}
],
"defaultMessage": "不能为null",
"objectName": "reportVO",
"field": "cgeId",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='reportVO'. Error count: 1",
"path": "/pcg/report1/test3"
}
可以看到 defaultMessage
中有相应的报错信息,但是不够完整,可以将字段名 field
一起拼接后作为报错信息,统一异常处理代码如下,
import org.springframework.validation.BindException;
/**
* 处理实体校验失败(multipart/form-data、x-www-form-urlencoded)
*
* @param e
* @return S2VJSONResult<T>
* @Author zrr
* @Date 2021/2/2 10:50
* @throws
*/
@ExceptionHandler(value = BindException.class)
public <T> S2VJSONResult<T> handlerBindException(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
assert fieldError != null;
String message = MessageFormat.format("参数 {0} 有误,{1}", fieldError.getField(), fieldError.getDefaultMessage());
return S2VJSONResult.msg(false, 400, message);
}
其中,e.getBindingResult()
中会有所有的参数报错信息,对我来说感觉一个就够了,所以我只取了一个,统一错误响应后的结果为,
{
"code": 400,
"isSuccess": false,
"message": "参数 baseinfoSqrxm 有误,baseinfoSqrxm 不能为空",
// "message": "参数 cgeId 有误,不能为null",
"data": null
}
3. 处理 RequestBody 情况
RequestBody 情况和上面很类似,但抛出的异常不一样,仍然需要单独处理。Controller 层代码示例如下,
@PostMapping("/test4")
S2VJSONResult<String> test4(@Validated @RequestBody ReportVO vo){
return S2VJSONResult.ok(JSON.toJSONString(vo));
}
这种情况会抛出 MethodArgumentNotValidException
异常。默认报错消息同 BindException
,处理方式基本一致。
import org.springframework.web.bind.MethodArgumentNotValidException;
/**
* 处理 @RequestBody 参数验证失败(application/json)
*
* @param e
* @return S2VJSONResult<T>
* @Author zrr
* @Date 2021/2/2 10:49
* @throws
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public <T> S2VJSONResult<T> handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
assert fieldError != null;
String message = MessageFormat.format("参数 {0} 有误,{1}", fieldError.getField(), fieldError.getDefaultMessage());
return S2VJSONResult.msg(false, 400, message);
}
这三种情况基本涵盖了传参的所有方式,后面若是有遗漏的情况,也是同样的原理,再加上就好了。
4. 处理集合
参数是集合如 list 的时候,@Validated 失效,但 @Valid 可以支持。
import javax.validation.Valid;
public S2VJSONResult<Object> batchUpdate(@RequestBody List<@Valid ProductModuleVO> list){
productModuleService.batchUpdate(list);
return S2VJSONResult.ok();
}
此时,抛出的异常为 ConstraintViolationException,和第 1 种情况相同。
5. 处理文件上传大小异常
可能由于没有继承 ResponseEntityExceptionHandler
,若是项目配置了文件上传大小限制,当超出时,系统没有捕捉异常,需要手动捕捉。
spring:
servlet:
multipart:
max-file-size: 10MB
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.beans.factory.annotation.Value;
@Value("${spring.servlet.multipart.max-file-size}")
private String maxFileSize;
/**
* 处理文件上传大小限制
*
* @param e
* @return com...entity.S2VJSONResult<T>
* @Author zrr
* @Date 2021/5/6 11:48
* @throws
*/
@ExceptionHandler(value = MaxUploadSizeExceededException.class)
public <T> S2VJSONResult<T> handlerMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
logger.error("catch MaxUploadSizeExceededException {}", e.getMessage());
return S2VJSONResult.msg(false, 400, "文件大小超出限制(" + maxFileSize + ")");
}
校验失败立即返回
上述三种默认情况都是校验全部字段后再返回报错信息,可以通过配置实现当一个参数校验有误时立即返回,提高效率。
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* valid
*
* @author: zrr
* @date: 2021/2/1 16:30
*/
@Configuration
public class ValidConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class)
.configure()
.failFast(true) //立即返回
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
Validated 验证组
接口通常有新增和更新,它们字段一致但是约束可能不同,若想要利用同一个实体类,可以通过组的方式实现共用。注意 @Valid
不支持组只有 @Validated
可以。
首先新增两个接口组,
import javax.validation.groups.Default;
public interface ValidGroupCreate extends Default {
}
import javax.validation.groups.Default;
public interface ValidGroupUpdate extends Default {
}
这里我继承了 Default.class
默认分组,可以实现参数没有标明组的情况时,也可以使用组。可以减少一些代码量。
实体类添加约束如下:
public class ReportVO implements Serializable {
@NotNull
private Integer cgeId;
@NotEmpty(groups = ValidGroupCreate.class)
private String baseinfoSqrxm;
@NotEmpty(groups = ValidGroupUpdate.class)
private String baseinfoSfzhm;
@NotEmpty(groups = {ValidGroupCreate.class, ValidGroupUpdate.class})
private String baseinfoHyzk;
再然后 Controller 层指定组即可,不填的话则是默认分组,
@PostMapping("/test3")
S2VJSONResult<String> test3(@Validated(ValidGroupUpdate.class) ReportVO vo){
return S2VJSONResult.all(true, 200, "success", JSON.toJSONString(vo));
}
注解用法
在此列举一些常用的校验注解,方便查看。
@NotNull
验证带注释的属性值不为 null。@AssertTrue
验证带注释的属性值为 true。@Size
验证带注释的属性值的大小在属性 min 和 max 之间;可以应用于 String, Collection, Map和数组属性。@Min
验证带注释的属性的值不小于 value 属性。@Max
验证带注释的属性的值不大于 value 属性。@Email
验证带注释的属性是有效的电子邮件地址。@NotEmpty
验证该属性不为 null 或为空;可以应用于 String, Collection, Map或Array值。@NotBlank
只能应用于文本值,并验证该属性不是null还是空白。@Positive
和@PositiveOrZero
适用于数值,并验证它们严格为正,或包括 0 在内为正。@Negative
和@NegativeOrZero
适用于数值,并验证它们严格为负数,或为负数(包括0)。@Past
和@PastOrPresent
验证日期值是过去还是现在(包括现在);可以应用于日期类型,包括 Java 8中添加的日期类型。@Future
和@FutureOrPresent
验证日期值是将来的日期还是将来的日期(包括现在)。@Pattern
字段或属性的值必须与 regexp 元素中定义的正则表达式匹配。
后续
可以创建自定义约束,目前项目大多用到就是非空判断,暂时先不弄
参考
Last modified on 2021-02-02