@PostMapping(value = "/testJavaBeanFrom", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public SysRes<Void> testJavaBeanFrom(@Valid PhoenixSaveDTO dttt){
log.info("进来了,{}", dttt);
return SysRes.success();
所以我们需要使用BindingResult
把异常信息绑定,然后自定义输出。
三、全局统一异常处理(入参校验)
看过了上面内容,我们基本上对JSR303
注解校验有了一定的理解,但是还是有一个问题,在实际开发中,我们基本每个接口都有入参校验,所以如果每个方法都使用前言(JSR303
)这种校验方式,虽然相对于if
要简洁不少,但是这还是不够的,我们还有更加简洁的方法。
从上文我们了解到,@Valid
或者 @Validated
进行参数校验的时候,如果不加BindingResult
那么会抛出异常,如果加上了,那么异常会封装进BindingResult
对象中,所以大家想我们是不是可以通过不加BindingResult
,然后对异常进行统一捕获处理从而达到简化的效果呢??
3.1 @ControllerAdvice 与 @RestControllerAdvice
在spring 3.2中,新增了@ControllerAdvice
注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理,可以用于定义@ExceptionHandler
、@InitBinder
、@ModelAttribute
,并应用到所有@RequestMapping
中。这里对这些不做详解了,可以参考spring的@ControllerAdvice注解 - yanggb - 博客园 (cnblogs.com)。
简单地说,@RestControllerAdvice
与@ControllerAdvice
的区别就和@RestController
与@Controller
的区别类似,@RestControllerAdvice
注解包含了@ControllerAdvice
注解和@ResponseBody
注解。
如果全部异常处理返回json,那么可以使用@RestControllerAdvice
代替 @ControllerAdvice
,这样在方法上就可以不需要添加 @ResponseBody
。
综上,我们可以通过SpringBoot提供的@RestControllerAdvice
和@ControllerAdvice
结合@ExceptionHandler
使用完成异常统一处理,需要捕获什么异常通过@ExceptionHandler
来指定对应异常类就可以了这里原则是按照从小到大异常进行依次执行。
通俗来讲就是当小的异常没有指定捕获时,大的异常包含了此异常就会被执行比如Exception
异常包含了所有异常类,是所有异常超级父类,当出现没有指定异常时此时对应捕获了Exception异常的方法会执行。
3.2 统一入参校验处理(JavaBean)
这里就不多说了,相信看了上面的内容,大家都能理解,所以这里就直接贴代码了。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
* JavaBean参数校验
* @param e BindException
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/8/31 19:42
@ExceptionHandler(BindException.class)
public SysRes<Void> handlerBindException(BindException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
List<String> messageList = e.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
String message = String.valueOf(messageList);
// 参数校验不需要打印过多信息 比如e,只需要知道是哪些有问题即可
log.error("[统一异常处理]请求地址:{}, 参数校验异常:{}", requestUri, message);
return SysRes.fail(SysCode.PARAMS_EXCEPTION, message);
这个代码和网上很多的不太一样,下面我来解释一下。
对于JavaBean的校验网上有的用MethodArgumentNotValidException
有的用 BindException
, 有的两个都捕获了,相信大家都很迷惑为什么同样的东西使用的方法不一样。
POST
请求参数有form data、JSON等。所以相对于JSON格式的传参,form-data抛出的异常肯定是不一样的,同样的入参校验,如果是JSON格式的POST请求,那么会抛出MethodArgumentNotValidException
,如果是form-data,则会抛出BindException
(2.4有图,大家可以仿着试一下),所以得出初步结论MethodArgumentNotValidException
校验 @RequestBody
的json对象数据,BindException
校验formData数据。
但是这里需要注意,在springboot2.3.0以上版本中,MethodArgumentNotValidException
extends BindException
,BindException extends Exception implements BindingResult
,在2.3.0以下的版本中MethodArgumentNotValidException extends Exception
,BindException extends Exception implements BindingResult
,所以会有form-data入参校验与Json入参校验分开捕获,form-data入参校验与Json入参统一使用bindingresult
捕获两种写法。其实这两种都对,大家根据自己的版本来选择合适的写法即可。
3.3 统一入参校验处理(单参数)
如果参数不满足要求,那么会抛出ConstraintViolationException异常,这个异常只有在单一参数校验的时候抛出,如果你的参数是JavaBean,那么就不是这个异常了
* requestParam单参数校验(需要在类上面加校验注解 方法上面加不管用的)
* ConstraintViolationException extends ValidationException
* @param exception ValidationException
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/8/31 18:02
@ExceptionHandler(ConstraintViolationException.class)
public SysRes<Void> handlerValidationException(ConstraintViolationException exception, HttpServletRequest request) {
String requestUri = request.getRequestURI();
Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
String validateMsg = String.valueOf(constraintViolations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList()));
log.error("[统一异常处理]请求地址:{}, 参数校验异常:{}", requestUri, validateMsg);
return SysRes.fail(SysCode.PARAMS_EXCEPTION, validateMsg);
以下是controller校验代码
* @author 伍六柒
* @since 2021/8/31 19:54
@Slf4j
@Validated //单参数校验(需要在类上面加校验注解 方法上面加不管用的)
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping(value = "/testJavaBeanJson", consumes = MediaType.APPLICATION_JSON_VALUE)
public SysRes<Void> testJavaBeanJson(@Valid @RequestBody PhoenixSaveDTO dttt){
log.info("进来了,{}", dttt);
return SysRes.success();
@PostMapping(value = "/testJavaBeanFrom", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public SysRes<Void> testJavaBeanFrom(@Valid PhoenixSaveDTO dttt){
log.info("进来了,{}", dttt);
return SysRes.success();
@GetMapping("/testParams")
public SysRes<Void> testParams(@RequestParam(value = "id") @NotBlank(message = "id为空") String id){
log.info("进来了,{}",id);
return SysRes.success();
四、全局统一异常处理(其余异常)
有了上面的经验,那么我们处理其他自定义异常就游刃有余了。下面我只举几个我自己常用的,大家如果有特殊需求可以自己封装。
* 全局异常处理
* 1.@RestControllerAdvice( @ResponseBody + @ControllerAdvice ):
* 如果使用@ControllerAdvice 则需要给每个方法添加@ResponseBody
* 它是一个Controller增强器,可对controller中被@RequestMapping注解的方法加一些逻辑处理。最常用的就是异常处理
* 需要配合@ExceptionHandler使用。当将异常抛到controller时,可以对异常进行统一处理,规定返回的json格式或是跳转到一个错误页面
* 2.@ExceptionHandler:
* (1)用来统一处理防范抛出的异常
* (2)被@ExceptionHandler注解的方法就会处理被@RequestMapping注解抛出的异常。
* (3)可添加参数:某个异常类的class,代表该方法专门处理该异常类
* (4)就近原则:
* 比如:NumberFormatException,这个异常有父类RuntimeException,
* RuntimeException还有父类Exception,如果我们分别定义异常处理方法,
* @ ExceptionHandler分别使用这三个异常作为参数,会依次去匹配对应的异常处理类
* (5)返回值类型和处理@RequestMapping的方法是统一的,我们也可以添加@ResponseBody注解,直接返回字符串,
* 否则默认返回Spring的ModelAndView对象,这时的String是ModelAndView的路径,而不是字符串本身。
* (6)使用@ExceptionHandler时尽量不要使用相同的注解参数,即同样的异常不要用两个专门的方法去处理。
* 编译可以通过,但是当抛出该异常的时候,spring会报错:
* java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class java.lang.NumberFormatException]:
* {public java.lang.String TestController.handlerException(java.lang.Exception),
* public java.lang.String TestController.handlerException2(java.lang.Exception)}
* 3.异常体系:
* (1)Object
* (2)Throwable
* 2.1: Error
* 2.2:Exception
* @author 伍六柒
* @since 2021/8/31 15:06
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
* 请求路径异常,这里有坑,需要修改springMVC静态资源的默认路径
* @param e NoHandlerFoundException
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/9/1 14:17
@ExceptionHandler(NoHandlerFoundException.class)
public SysRes<Void> noHandlerFoundException(NoHandlerFoundException e) {
log.error("[统一异常捕获处理][请求资源不存在]请求地址:{}", e.getRequestURL());
return SysRes.fail(SysCode.NOT_FOUND_EXCEPTION, e.getRequestURL());
* 请求方法不支持
* @param e HttpRequestMethodNotSupportedException
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/8/31 17:43
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public SysRes<Void> handlerHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("[统一异常捕获处理][不支持该请求方法]请求地址:{},请求方式:{},接口支持方式:{}",
requestUri, e.getMethod(), e.getSupportedMethods());
return SysRes.fail(SysCode.METHOD_NOT_ALLOWED_EXCEPTION, e.getMethod());
* 请求类型错误Content-Type/Accept
* @param e HttpMessageNotReadableException
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/8/31 17:43
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public SysRes<Void> handlerHttpMessageNotReadable(HttpMediaTypeNotSupportedException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
String requestContentType = String.valueOf(e.getContentType());
log.error("[统一异常处理]请求地址:{}, 请求类型:{}, 接口支持的类型:{}",
requestUri, requestContentType, e.getSupportedMediaTypes());
return SysRes.fail(SysCode.BAD_REQUEST_EXCEPTION, requestContentType);
* 请求参数不可读异常
* (HttpMessageNotReadableException 和 TypeMismatchException 都继承了 NestedRuntimeException)
* @param e TypeMismatchException
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/8/31 17:45
@ExceptionHandler(HttpMessageNotReadableException.class)
public SysRes<Void> handlerTypeMismatch(HttpMessageNotReadableException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("[统一异常处理]请求地址:{}, 属性名称:{}", requestUri, e.getSuppressed());
return SysRes.fail(SysCode.TYPE_MISMATCH_EXCEPTION);
* 系统业务异常
* @param e BusinessException
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/9/4 17:17
@ExceptionHandler(BusinessException.class)
public SysRes<Void> businessException(BusinessException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("[统一异常处理]请求地址:{}", requestUri, e);
return SysRes.fail(SysCode.BUSINESS_ERROR);
* 系统内部错误异常
* @param e Exception
* @param request HttpServletRequest
* @return SysRes<Void>
* @author 伍六柒
* @since 2021/9/4 17:17
@ExceptionHandler(Exception.class)
public SysRes<Void> unNoException(Exception e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("[统一异常处理]请求地址:{}", requestUri, e);
return SysRes.fail(SysCode.INTERNAL_SERVER_ERROR);
上面需要注意一下路径异常-404会不生效,需要在yml中配置
spring: mvc: static-path-pattern: /statics throw-exception-if-no-handler-found: true
最后小小的总结一下有些在上面没有提到的:
1.首先不建议大家在处理过程中,使用e.printStackTrace()
打印日志,因为这是控制台打印日志,只能在控制台输出,但是如果在开发过程中,我们的项目部署在服务器上,同时我们对日志进行了分片(时间或日志文件大小),那么只有在总日志上才可以看到异常的详细信息,在分片日志是看不到详细信息的,我们只能知道出了个啥异常,不知道具体是怎么出现的,而且这个可能会导致锁死。所以我个人一般都是直接在log中打印e
,这个e
打印不需要占位符。
2.其次打印异常信息的时候,不要所有的异常都打印全部信息,只需要打印自己需要的东西即可,比如我们入参校验、请求方式异常等我们不需要打印异常的详细信息。
3.不要过度封装,比如我上面的请求路径异常、请求方法异常、请求类型错误这些异常都是有明确的HTTP规范的(路径异常-404,方法异常-405,其他的可自行查阅HTTP code码),人家规范好了你不去遵守非得多此一举写一个,试想这种情况下,如果和你合作的前端人家把http的异常按规范已经处理好了,但是你把人家规范改了,这就很容易。。。(特殊业务需求或者自己玩一下另说)。我写那几个的目的主要是前端那边没有明确封装各个http的code,同时为了方便调试才写的。