3.3 启动程序,进行测试
启动程序,然后再浏览器里我们就可以进行输入: swagger访问地址: http://localhost:8080/doc.html#/home
打开swagger文档 就可以进行测试了:
首先我们访问 http://localhost:8080/users/get?id=-1
进行测试,查看返回结果,果然对我们的 id 进行校验。
接下来我们访问 http://localhost:8080/users/add
进行新增用户的校验:请求体我们写成:
{ "password": "233", "username": "33" }
然后返回结果的如下:
{ "timestamp": "2023-04-09T13:33:58.864+0000", "status": 400, "error": "Bad Request", "errors": [ { "codes": [ "Length.userAddDTO.password", "Length.password", "Length.java.lang.String", "Length" ], "arguments": [ { "codes": [ "userAddDTO.password", "password" ], "arguments": null, "defaultMessage": "password", "code": "password" }, 16, 4 ], "defaultMessage": "密码长度为 4-16 位", "objectName": "userAddDTO", "field": "password", "rejectedValue": "233", "bindingFailure": false, "code": "Length" }, { "codes": [ "Length.userAddDTO.username", "Length.username", "Length.java.lang.String", "Length" ], "arguments": [ { "codes": [ "userAddDTO.username", "username" ], "arguments": null, "defaultMessage": "username", "code": "username" }, 16, 5 ], "defaultMessage": "账号长度为 5-16 位", "objectName": "userAddDTO", "field": "username", "rejectedValue": "33", "bindingFailure": false, "code": "Length" } ], "message": "Validation failed for object='userAddDTO'. Error count: 2", "path": "/users/add" }
返回结果的json串中的 errors 字段,参数错误明细数组。每一个数组元素,对应一个参数错误明细。这里,username 违背了 账号长度为 5-16 位规定。 password违反了 密码长度为 4-16 位的规定。
返回结果示意图:
3.3 一些疑问
在这里 细心的小伙伴可能会有几个疑问:
3.3.1 疑问一
#get(id)
方法上,我们并没有给 id 添加 @Valid 注解,而 #add(addDTO)
方法上,我们给 addDTO 添加 @Valid 注解。这个差异,是为什么呢?
因为 UserController
使用了 @Validated 注解,那么 Spring Validation 就会使用 AOP 进行切面,进行参数校验。而该切面的拦截器,使用的是 MethodValidationInterceptor
。
- 对于
#get(id)
方法,需要校验的参数 id ,是平铺开的,所以无需添加 @Valid 注解。 - 对于
#add(addDTO)
方法,需要校验的参数 addDTO ,实际相当于嵌套校验,要校验的参数的都在 addDTO 里面,所以需要添加 @Valid (其实实测加@Validated也行,暂时不知道为啥 为了好区分就先用 @Valid 吧 )注解。
3.3.2 疑问二
#get(id)
方法的返回的结果是 status = 500
,而 #add(addDTO)
方法的返回的结果是 status = 400
。
- 对于
#get(id)
方法,在MethodValidationInterceptor
拦截器中,校验到参数不正确,会抛出ConstraintViolationException
异常。 - 对于
#add(addDTO)
方法,因为 addDTO 是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用DataBinder#validate(Object... validationHints)
方法,进行校验。在校验不通过时,会抛出BindException
。
在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver
处理异常。
- 对于
BindException
异常,处理成 400 的状态码。 - 对于
ConstraintViolationException
异常,没有特殊处理,所以处理成 500 的状态码。
这里,我们在抛个问题,如果 #add(addDTO)
方法,如果参数正确,在走完 DataBinder 中的参数校验后,会不会在走一遍 MethodValidationInterceptor
的拦截器呢?思考 100 毫秒…
答案是会。这样,就会导致浪费。所以 Controller 类里,如果 只有 类似的 #add(addDTO)
方法的 嵌套校验,那么我可以不在 Controller 类上添加 @Validated 注解。从而实现,仅使用 DataBinder 中来做参数校验。
3.3.3 返回提示很不友好,太长了
第三点,无论是 #get(id)
方法,还是 #add(addDTO)
方法,它们的返回提示都非常不友好,那么该怎么办呢?我们将在 第四章节通过 处理校验异常 进行处理。
四、处理校验异常
4.1 校验不通过的枚举类
package com.ratel.validation.enums; /** * 业务异常枚举 */ public enum ServiceExceptionEnum { // ========== 系统级别 ========== SUCCESS(0, "成功"), SYS_ERROR(2001001000, "服务端发生异常"), MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"), INVALID_REQUEST_PARAM_ERROR(2001001002, "请求参数不合法"), // ========== 用户模块 ========== USER_NOT_FOUND(1001002000, "用户不存在"), // ========== 订单模块 ========== // ========== 商品模块 ========== ; /** * 错误码 */ private final int code; /** * 错误提示 */ private final String message; ServiceExceptionEnum(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }
4.2 统一返回结果实体类
package com.ratel.validation.common; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.util.Assert; import java.io.Serializable; /** * 通用返回结果 * * @param <T> 结果泛型 */ public class CommonResult<T> implements Serializable { public static Integer CODE_SUCCESS = 0; /** * 错误码 */ private Integer code; /** * 错误提示 */ private String message; /** * 返回数据 */ private T data; /** * 将传入的 result 对象,转换成另外一个泛型结果的对象 * * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 * * @param result 传入的 result 对象 * @param <T> 返回的泛型 * @return 新的 CommonResult 对象 */ public static <T> CommonResult<T> error(CommonResult<?> result) { return error(result.getCode(), result.getMessage()); } public static <T> CommonResult<T> error(Integer code, String message) { Assert.isTrue(!CODE_SUCCESS.equals(code), "code 必须是错误的!"); CommonResult<T> result = new CommonResult<>(); result.code = code; result.message = message; return result; } public static <T> CommonResult<T> success(T data) { CommonResult<T> result = new CommonResult<>(); result.code = CODE_SUCCESS; result.data = data; result.message = ""; return result; } public Integer getCode() { return code; } public void setCode(Integer 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; } @JsonIgnore public boolean isSuccess() { return CODE_SUCCESS.equals(code); } @JsonIgnore public boolean isError() { return !isSuccess(); } @Override public String toString() { return "CommonResult{" + "code=" + code + ", message='" + message + '\'' + ", data=" + data + '}'; } }
4.3 增加全局异常处理类 GlobalExceptionHandler
package com.ratel.validation.exception; import com.ratel.validation.common.CommonResult; import com.ratel.validation.enums.ServiceExceptionEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; @ControllerAdvice(basePackages = "com.ratel.validation.cotroller") public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(getClass()); /** * 处理 MissingServletRequestParameterException 异常 * * SpringMVC 参数不正确 */ @ResponseBody @ExceptionHandler(value = MissingServletRequestParameterException.class) public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) { logger.error("[missingServletRequestParameterExceptionHandler]", ex); // 包装 CommonResult 结果 return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(), ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage()); } @ResponseBody @ExceptionHandler(value = ConstraintViolationException.class) public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) { logger.error("[constraintViolationExceptionHandler]", ex); // 拼接错误 StringBuilder detailMessage = new StringBuilder(); for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) { // 使用 ; 分隔多个错误 if (detailMessage.length() > 0) { detailMessage.append(";"); } // 拼接内容到其中 detailMessage.append(constraintViolation.getMessage()); } // 包装 CommonResult 结果 return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(), ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString()); } @ResponseBody @ExceptionHandler(value = BindException.class) public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) { logger.info("========进入了 bindException======"); logger.error("[bindExceptionHandler]", ex); // 拼接错误 StringBuilder detailMessage = new StringBuilder(); for (ObjectError objectError : ex.getAllErrors()) { // 使用 ; 分隔多个错误 if (detailMessage.length() > 0) { detailMessage.append(";"); } // 拼接内容到其中 detailMessage.append(objectError.getDefaultMessage()); } // 包装 CommonResult 结果 return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(), ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString()); } @ResponseBody @ExceptionHandler(value = MethodArgumentNotValidException.class) public CommonResult MethodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) { logger.info("-----------------进入了 MethodArgumentNotValidException-----------------"); logger.error("[MethodArgumentNotValidException]", ex); // 拼接错误 StringBuilder detailMessage = new StringBuilder(); for (ObjectError objectError : ex.getBindingResult().getAllErrors()) { // 使用 ; 分隔多个错误 if (detailMessage.length() > 0) { detailMessage.append(";"); } // 拼接内容到其中 detailMessage.append(objectError.getDefaultMessage()); } // 包装 CommonResult 结果 return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(), ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString()); } /** * 处理其它 Exception 异常 * @param req * @param e * @return */ @ResponseBody @ExceptionHandler(value = Exception.class) public CommonResult exceptionHandler(HttpServletRequest req, Exception e) { // 记录异常日志 logger.error("[exceptionHandler]", e); // 返回 ERROR CommonResult return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(), ServiceExceptionEnum.SYS_ERROR.getMessage()); } }
4.4 测试
访问:http://localhost:8080/users/add
可以看到异常结果已经被拼接成一个字符串,相比之前清新 易懂了不少。