Spring 中的异常处理

2020/11/13 24

Spring

Spring 提供了多种方式将异常转换为响应:

将异常映射为 HTTP 状态码

默认情况下,Spring 会将自身的一些异常自动转换为合适的状态码。

Spring 异常 HTTP 状态码
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowd
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request

尽管这些内置的映射是很有用的,但是对于应用所抛出的其它异常就无能为力了。幸好,Spring 提供了一种机制,能够通过 @ResponseStatus 注解将异常映射为 HTTP 状态码。

@ResponseStatus

@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String spittle (
    @PathVariable long spittleId,
    Model model
) {
    Spittle spittle = spittleRepository.findOne(spittleId);
    if (spittle == null)
        throw new SpittleNotFoundException();
    model.addAttribute(spittle);
    return "spittle";
}

如果 findOne() 方法返回的是 null,那么就会抛出 SpittleNotFoundException 异常,这是一种资源没有找到的场景,所以 404 状态码是最为精准的响应码,因此可以使用 @ResponseStatus 注解来实现:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException { }

在很多场景想,将异常映射为状态码是很简单的方案,但是如果想响应一个视图给用户,那该怎么办呢?

当然最简单的做法是:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
    try {
        spittleRepository.save(new Spittle());
        return "redirect:/spittles";
    } catch (DuplicateSpittleException e) {
        return "error/duplicate";
    }
}

它的确能解决问题,但是它太复杂了。该方法可能有两个路径,如果能让方法只关注正确的路径,而让其它方法处理异常的话,就更加优雅了。

@ExceptionHandler

我们可以将 saveSpittle() 方法中的异常处理剥离掉:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
    spittleRepository.save(new Spittle());
    return "redirect:/spittles";
}

可以看到,方法变得更简单了,因为只关注成功的情况,所以更容易理解和测试了。

现在,我们可以添加一个新的方法,它会处理抛出的 DuplicateSpittleException 的情况:

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
    return "error/duplicate";
}

使用 @ExceptionHandler 标注的方法,能处理同一控制器中所有对应的异常。那有意思的就来了,你可能会问有没有一种方法能够处理所有控制器中方法抛出的异常呢?从 Spring 3.2 开始,这肯定能够实现,我们只需要将其定义到控制器通知类中即可。

@ControllerAdvice

任意带有 @ControllerAdvice 注解的类都是控制器通知,这个类会包含一个或多个如下类型的方法:

在带有 @ControllerAdvice 注解的类中,上述这些方法会运用到整个应用程序所有控制器中带有 @RequestMapping 注解的方法上。

@ControllerAdvice 本身已经使用了 @Component,因此 @ControllerAdvice 注解所标注的类会自动被组件扫描到,就像带有 @Component 注解的类一样。

@ControllerAdvice 最为实用的一个场景就是将所有的 @ExceptionHandler 方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。