Spring MVC异常处理

Posted 天码营

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC异常处理相关的知识,希望对你有一定的参考价值。

Spring MVC异常处理 由David发表在天码营 


Spring MVC框架提供了多种机制用来处理异常,初次接触可能会对他们用法以及适用的场景感到困惑。现在以一个简单例子来解释这些异常处理的机制。

假设现在我们开发了一个博客应用,其中最重要的资源就是文章(Post),应用中的URL设计如下:

  • 获取文章列表:GET /posts/
  • 添加一篇文章:POST /posts/
  • 获取一篇文章:GET /posts/id
  • 更新一篇文章:PUT /posts/id
  • 删除一篇文章:DELETE /posts/id

这是非常标准的复合RESTful风格的URL设计,在Spring MVC实现的应用过程中,相应也会有5个对应的用@RequestMapping注解的方法来处理相应的URL请求。在处理某一篇文章的请求中(获取、更新、删除),无疑需要做这样一个判断——请求URL中的文章id是否在于系统中,如果不存在需要返回404 Not Found

使用HTTP状态码

在默认情况下,Spring MVC处理Web请求时如果发现存在没有应用代码捕获的异常,那么会返回HTTP 500(Internal Server Error)错误。但是如果该异常是我们自己定义的并且使用@ResponseStatus注解进行修饰,那么Spring MVC则会返回指定的HTTP状态码:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No Such Post")//404 Not Found
public class PostNotFoundException extends RuntimeException 

Controller中可以这样使用它:

@RequestMapping(value = "/posts/id", method = RequestMethod.GET)
public String showPost(@PathVariable("id") long id, Model model) 
    Post post = postService.get(id);
    if (post == null) throw new PostNotFoundException("post not found");
    model.addAttribute("post", post);
    return "postDetail";

这样如果我们访问了一个不存在的文章,那么Spring MVC会根据抛出的PostNotFoundException上的注解值返回一个HTTP 404 Not Found给浏览器。

最佳实践

上述场景中,除了获取一篇文章的请求,还有更新和删除一篇文章的方法中都需要判断文章id是否存在。在每一个方法中都加上if (post == null) throw new PostNotFoundException("post not found");是一种解决方案,但如果有10个、20个包含/posts/id的方法,虽然只有一行代码但让他们重复10次、20次也是非常不优雅的。

为了解决这个问题,可以将这个逻辑放在Service中实现:

@Service
public class PostService 

    @Autowired
    private PostRepository postRepository;

    public Post get(long id) 
        return postRepository.findById(id)
                .orElseThrow(() -> new PostNotFoundException("post not found"));
    


这里`PostRepository`继承了`JpaRepository`,可以定义`findById`方法返回一个`Optional<Post>`——如果不存在则Optional为空,抛出异常。

这样在所有的Controller方法中,只需要正常活取文章即可,所有的异常处理都交给了Spring MVC。

Controller中处理异常

Controller中的方法除了可以用于处理Web请求,还能够用于处理异常处理——为它们加上@ExceptionHandler即可:

@Controller
public class ExceptionHandlingController 

  // @RequestHandler methods
  ...

  // Exception handling methods

  // Convert a predefined exception to an HTTP Status code
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() 
    // Nothing to do
  

  // Specify the name of a specific view that will be used to display the error:
  @ExceptionHandler(SQLException.class,DataAccessException.class)
  public String databaseError() 
    // Nothing to do.  Returns the logical view name of an error page, passed to
    // the view-resolver(s) in usual way.
    // Note that the exception is _not_ available to this view (it is not added to
    // the model) but see "Extending ExceptionHandlerExceptionResolver" below.
    return "databaseError";
  

  // Total control - setup a model and return the view name yourself. Or consider
  // subclassing ExceptionHandlerExceptionResolver (see below).
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) 
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  

首先需要明确的一点是,在Controller方法中的@ExceptionHandler方法只能够处理同一个Controller中抛出的异常。这些方法上同时也可以继续使用@ResponseStatus注解用于返回指定的HTTP状态码,但同时还能够支持更加丰富的异常处理:

  • 渲染特定的视图页面
  • 使用ModelAndView返回更多的业务信息

大多数网站都会使用一个特定的页面来响应这些异常,而不是直接返回一个HTTP状态码或者显示Java异常调用栈。当然异常信息对于开发人员是非常有用的,如果想要在视图中直接看到它们可以这样渲染模板(以JSP为例):

<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>

<!--
Failed URL: $url
Exception:  $exception.message
<c:forEach items="$exception.stackTrace" var="ste">    $ste 
</c:forEach>
-->

全局异常处理

@ControllerAdvice提供了和上一节一样的异常处理能力,但是可以被应用于Spring应用上下文中的所有@Controller

@ControllerAdvice
class GlobalControllerExceptionHandler 
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() 
        // Nothing to do
    

Spring MVC默认对于没有捕获也没有被@ResponseStatus以及@ExceptionHandler声明的异常,会直接返回500,这显然并不友好,可以在@ControllerAdvice中对其进行处理(例如返回一个友好的错误页面,引导用户返回正确的位置或者提交错误信息):

@ControllerAdvice
class GlobalDefaultExceptionHandler 
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception 
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    

总结

Spring在异常处理方面提供了一如既往的强大特性和支持,那么在应用开发中我们应该如何使用这些方法呢?以下提供一些经验性的准则:

  • 不要在@Controller中自己进行异常处理逻辑。即使它只是一个Controller相关的特定异常,在@Controller中添加一个@ExceptionHandler方法处理。
  • 对于自定义的异常,可以考虑对其加上@ResponseStatus注解
  • 使用@ControllerAdvice处理通用异常(例如资源不存在、资源存在冲突等)

更多文章请访问天码营网站




以上是关于Spring MVC异常处理的主要内容,如果未能解决你的问题,请参考以下文章

spring mvc 异常统一处理方式

Spring MVC异常处理代码完整实例

spring mvc 异常如何处理

使用Spring MVC统一异常处理实战

Spring MVC异常处理

Spring MVC学习—项目统一异常处理机制详解与使用案例