day10-SpringBoot的异常处理

打印 上一主题 下一主题

主题 1001|帖子 1001|积分 3005

SpringBoot异常处理

1.基本介绍

默认情况下,SpringBoot提供/error处理所有错误的映射,也就是说当出现错误时,SpringBoot底层会请求转发到/error这个映射路径所关联的页面或者控制器方法。(默认异常处理机制)
要验证这个点,我们只需要设置一个拦截器,当每次请求时都在preHandle()中打印请求URI。在浏览器访问不存在的路径映射时:

  • 浏览器:SpringBoot会响应一个"whitelabel"的错误视图,并以HTML格式呈现

  • 服务器:后台输出请求的URI为

  • 整个过程:当浏览器访问不存在的路径映射时,就产生了错误。这时 SpringBoot(底层由默认错误视图解析器 DefaultErrorViewResolver 请求转发到 /error 路径关联的页面,该路径若没有被排除,也会经过拦截器处理)会立即去请求 /error 映射关联的资源(默认为"whitelabel" 错误视图),然后返回给浏览器,我们就看到了这个错误视图。
2.默认错误视图解析器

在SpringBoot发现浏览器请求的路径不存在后,底层发生了一系列的操作:
(1)当浏览器访问不存在的路径映射时,就产生了错误
(2)底层由 DefaultErrorViewResolver (默认错误视图解析器)先去遍历项目中设置的所有静态资源路径,尝试从这些静态资源路径中找到/error/404.html文件
  1. //第一次执行resolve()方法:
  2. //这里的viewName就是状态码
  3. private ModelAndView resolve(String viewName, Map<String, Object> model) {
  4.    String errorViewName = "error/" + viewName;//第一次:errorViewName=error/404
  5.    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
  6.          this.applicationContext);
  7.    if (provider != null) {
  8.       return new ModelAndView(errorViewName, model);
  9.    }
  10.    return resolveResource(errorViewName, model);//进入resolveResource()方法
  11. }
复制代码
遍历项目中设置的所有静态资源路径,尝试从这些静态资源路径中找到/error/404.html文件
  1. //第一次执行resolveResource()方法
  2. private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
  3.    //resources.getStaticLocations()就是项目中的静态资源路径,根据你的设置而变化
  4.    for (String location : this.resources.getStaticLocations()) {
  5.       try {
  6.          Resource resource = this.applicationContext.getResource(location);
  7.          resource = resource.createRelative(viewName + ".html");
  8.          if (resource.exists()) {
  9.              //如果找到了,就返回这个视图
  10.             return new ModelAndView(new HtmlResourceView(resource), model);
  11.          }
  12.       }
  13.       catch (Exception ex) {
  14.       }
  15.    }
  16.    return null;
  17. }
复制代码
(3)如果找不到,退而求其次,再遍历项目中设置的所有静态资源路径,尝试从这些静态资源路径中找到/error/4xx.html文件
  1. @Override
  2. public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
  3.     //再一次调用resolve()方法
  4.    ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
  5.    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
  6.       modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
  7.    }
  8.    return modelAndView;
  9. }
复制代码
  1. //第二次执行resolve()方法:
  2. //这里的viewName就是状态码
  3. private ModelAndView resolve(String viewName, Map<String, Object> model) {
  4.    String errorViewName = "error/" + viewName;//第二次:errorViewName=/error/4xx
  5.    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
  6.          this.applicationContext);
  7.    if (provider != null) {
  8.       return new ModelAndView(errorViewName, model);
  9.    }
  10.    return resolveResource(errorViewName, model);//进入resolveResource()方法
  11. }
复制代码
遍历项目中设置的所有静态资源路径,尝试从这些静态资源路径中找到/error/4xx.html文件
  1. //第二次执行resolveResource()方法
  2. private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
  3.     //遍历项目中设置的所有静态资源路径
  4.    for (String location : this.resources.getStaticLocations()) {
  5.       try {
  6.          Resource resource = this.applicationContext.getResource(location);
  7.          resource = resource.createRelative(viewName + ".html");
  8.          if (resource.exists()) {
  9.              //如果找到了就返回
  10.             return new ModelAndView(new HtmlResourceView(resource), model);
  11.          }
  12.       }
  13.       catch (Exception ex) {
  14.       }
  15.    }
  16.    return null;
  17. }
复制代码
示例:

(4)以上两轮都找不到匹配的视图,就执行下面的方法(位于AbstractErrorController.java):
  1. protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
  2.       Map<String, Object> model) {
  3.    for (ErrorViewResolver resolver : this.errorViewResolvers) {
  4.       ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
  5.       if (modelAndView != null) {
  6.          return modelAndView;
  7.       }
  8.    }
  9.    return null;
  10. }
复制代码
如果以上方法仍然返回null,然后去BasicErrorController.java里执行如下方法:
  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3.    HttpStatus status = getStatus(request);
  4.    Map<String, Object> model = Collections
  5.          .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
  6.    response.setStatus(status.value());
  7.    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  8.     //modelAndView为null,产生一个默认的新的视图,并返回
  9.    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  10. }
复制代码
(5)此时浏览器接收到的视图就是这个默认的视图
3.拦截器VS过滤器


  • 使用范围不同
    (1)过滤器实现的是javax.servlet.Filter接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter的使用依赖于Tomcat等容器,Filter只能在web程序中使用
    (2)拦截器是一个Spring组件,由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。它不仅能应用在web程序中,也能用于Application等程序中。
  • 两者的触发时机也不同
    (1)过滤器Filter在请求进入容器后,在进入Servlet之前进行预处理。请求结束则是在servlet处理完以后。
    (2)拦截器Interceptor是在请求进入Servlet之后(即DispatcherServlet,前端控制器),在进入Controller之前进行预处理的,Controller中渲染了对应的视图之后请求结束。

  • 过滤器不会处理请求转发,拦截器会处理请求转发(前提是拦截器没有放行此请求)。原因是过滤器处理的是容器接收过来的外部请求,而请求转发是服务器内部,Servlet之间的处理。但拦截器是Spring的一个组件,依然会去处理请求转发,除非请求转发的路径被拦截器放行了。
例子演示1--过滤器与拦截器的执行顺序
我们分别在SpringBoot项目中配置一个过滤器和拦截器,测试它们的执行顺序
(1)创建过滤器
  1. package com.li.thymeleaf.filter;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.stereotype.Component;
  4. import javax.servlet.*;
  5. import java.io.IOException;
  6. /**
  7. * @author 李
  8. * @version 1.0
  9. */
  10. @Component
  11. @Slf4j
  12. public class MyFilter implements Filter {
  13.     @Override
  14.     public void init(FilterConfig filterConfig) throws ServletException {
  15.         log.info("MyFilter的init()被调用...");
  16.     }
  17.     @Override
  18.     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  19.         log.info("MyFilter的doFilter()被调用...");
  20.         filterChain.doFilter(servletRequest, servletResponse);
  21.     }
  22.     @Override
  23.     public void destroy() {
  24.         log.info("MyFilter的destroy()被调用...");
  25.     }
  26. }
复制代码
(2)创建拦截器
  1. package com.li.thymeleaf.interceptor;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.web.servlet.HandlerInterceptor;
  4. import org.springframework.web.servlet.ModelAndView;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. /**
  8. * @author 李
  9. * @version 1.0
  10. */
  11. @Slf4j
  12. public class MyInterceptor implements HandlerInterceptor {
  13.     @Override
  14.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  15.         log.info("MyInterceptor的preHandle()被执行...");
  16.         return true;
  17.     }
  18.     @Override
  19.     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  20.         log.info("MyInterceptor的postHandle()被执行...");
  21.     }
  22.     @Override
  23.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  24.         log.info("MyInterceptor的afterCompletion()被执行...");
  25.     }
  26. }
复制代码
在配置类中注册拦截器,注入到spring容器中
  1. package com.li.thymeleaf.config;
  2. import com.li.thymeleaf.interceptor.MyInterceptor;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. /**
  7. * @author 李
  8. * @version 1.0
  9. */
  10. @Configuration
  11. public class WebConfig implements WebMvcConfigurer {
  12.     @Override
  13.     public void addInterceptors(InterceptorRegistry registry) {
  14.         //addInterceptor注册自定义拦截器
  15.         //addPathPatterns指定拦截器规则(拦截所有请求/**)
  16.         registry.addInterceptor(new MyInterceptor())
  17.                 .addPathPatterns("/**");
  18.     }
  19. }
复制代码
(3)浏览器请求某一个资源,后台输出如下:
例子演示2--过滤器不会处理请求转发,拦截器会处理请求转发
我们在浏览器请求一个不存在的资源如http://localhost:8080/xxx,后台输出如下:
这是因为SpringBoot底层处理了/error,进行了请求转发(默认错误视图解析器请求转发到 /error 路径关联的页面)。而过滤器不会处理请求转发,因此可以看到途中只有拦截器被调用了两次。过滤器只在外部请求资源/xxx的时候被调用了一次。
4.自定义异常页面

4.1自定义异常页面说明

Spring Boot Reference Documentation
如果要显示给定状态代码的自定义 HTML 错误页,可以将文件添加到目录中。 错误页面可以是静态 HTML(即,添加到任何静态资源目录下),也可以是使用模板构建的。 文件名应为确切的状态代码或系列掩码。/error
例如,要映射到静态 HTML 文件,目录结构如下所示:404
  1. src/
  2. +- main/
  3.      +- java/
  4.      |   + <source code>
  5.      +- resources/
  6.          +- public/
  7.              +- error/
  8.              |   +- 404.html
  9.              +- <other public assets>
复制代码
要使用 Mustache 模板映射所有错误,目录结构如下所示:5xx
  1. src/
  2. +- main/
  3.      +- java/
  4.      |   + <source code>
  5.      +- resources/
  6.          +- templates/
  7.              +- error/
  8.              |   +- 5xx.mustache
  9.              +- <other templates>
复制代码

  • 如果发生了404错误,优先匹配 404.html,如果没有,则匹配 4xx.html,再没有则使用默认方式显示错误
  • 500.html 和 5xx.html 是一样的逻辑
4.2应用实例

需求:自定义404.html、500.html、4xx.html、5xx.html,当发生相应错误时,显示自定义的页面信息。
(1)4xx.html,使用thymeleaf标签取出状态码和错误信息,其他页面同理
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>4xx</title>
  6. </head>
  7. <body bgcolor="#cedafe">
  8.     <br/><br/><hr/>
  9.     <h1>4xx.html :)</h1>
  10.     状态码:<h1 th:text="${status}"></h1>
  11.     错误信息:<h1 th:text="${error}"></h1>
  12.     <hr/>
  13. </body>
  14. </html>
复制代码
(2)5xx.html
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>5xx</title>
  6. </head>
  7. <body bgcolor="#cedafe">
  8.     <br/><br/><hr/>
  9.     <h1>5xx.html :(</h1>
  10.     状态码:<h1 th:text="${status}"></h1>
  11.     错误信息:<h1 th:text="${error}"></h1>
  12.     <hr/>
  13. </body>
  14. </html>
复制代码
(3)404.html
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>404</title>
  6. </head>
  7. <body bgcolor="#cedafe">
  8.     <br/><br/><hr/>
  9.     <h1>404 Not Found~~</h1>
  10.     状态码:<h1 th:text="${status}"></h1>
  11.     错误信息:<h1 th:text="${error}"></h1>
  12.     <hr/>
  13. </body>
  14. </html>
复制代码
(4)500.html
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>500</title>
  6. </head>
  7. <body bgcolor="#cedafe">
  8.     <br/><br/><hr/>
  9.     <h1>500 服务器内部发生错误 :(</h1>
  10.     状态码:<h1 th:text="${status}"></h1>
  11.     错误信息:<h1 th:text="${error}"></h1>
  12.     <hr/>
  13. </body>
  14. </html>
复制代码
(5)模拟500和405错误
  1. package com.li.thymeleaf.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.PostMapping;
  5. /**
  6. * @author 李
  7. * @version 1.0
  8. */
  9. @Controller
  10. public class MyErrorController {
  11.     //模拟一个服务器内部错误500
  12.     @GetMapping("/abc")
  13.     public String abc() {
  14.         int i = 10 / 0;
  15.         return "manage";
  16.     }
  17.     //如果get方式请求此路径,会产生405的客户端错误
  18.     @PostMapping("/xyz")
  19.     public String xyz() {
  20.         return "manage";
  21.     }
  22. }
复制代码
(6)浏览器访问不存在的资源时,显示的是404.html。因为发生错误时首先会在静态资源目录中按照:404.html-->4xx.html的顺序寻找视图。
(7)服务器内部错误-500错误:
(8)如果出现出现的是4开头的错误,就会返回4xx.html
5.全局异常处理

5.1全局异常说明

在 Java 程序发生异常时,可以通过全局异常来捕获处理异常。
在 SpringBoot 中通过 @ControllerAdvice(修饰的类即为全局异常处理器)加上@ExceptionHandler(修饰方法)处理全局异常,它的底层由 ExceptionHandlerExceptionResolver 类支撑。

  • 默认异常处理机制,是通过不同的状态码(status),确定要返回的页面
  • 而全局异常的处理,则是根据 Java异常种类,来显式指定返回的错误页面
  • 全局异常处理优先级 > 默认异常处理优先级
5.2全局异常-应用实例

需求:演示全局异常使用,当发生类似 ArithmeticException、NullPointException 时,不使用默认异常机制(通过状态码)匹配的 xxx.html,而是通过全局异常机制显式指定的错误页面。
(1)创建全局异常处理器 GlobalExceptionHandler.java
  1. package com.li.thymeleaf.exception;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.ui.Model;
  4. import org.springframework.web.bind.annotation.ControllerAdvice;
  5. import org.springframework.web.bind.annotation.ExceptionHandler;
  6. /**
  7. * @author 李
  8. * @version 1.0
  9. */
  10. @ControllerAdvice //标识一个全局异常处理器/对象,标识的类将会被注入到spring容器中
  11. @Slf4j
  12. public class GlobalExceptionHandler {
  13.     /**
  14.      * 编写方法,处理指定异常(这里要处理的异常由你指定)
  15.      * @param e     表示发生异常后传递的异常对象
  16.      * @param model 将异常信息放入model,传递给下一个页面
  17.      * @return
  18.      */
  19.     @ExceptionHandler({ArithmeticException.class, NullPointerException.class})
  20.     public String handleException(Exception e, Model model) {
  21.         log.info("异常信息={}", e.getMessage());
  22.         //model的数据会自动放入request域中
  23.         model.addAttribute("msg", e.getMessage());
  24.         return "/error/global";//指定转发到global.html
  25.     }
  26. }
复制代码
(2)global.html
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>全局异常</title>
  6. </head>
  7. <body bgcolor="#cedafe">
  8.     <br/><br/><hr/>
  9.     <h1>发生了全局异常/错误 :(</h1>
  10.     错误信息:<h1 th:text="${msg}"></h1>
  11.     <hr/>
  12. </body>
  13. </html>
复制代码
(3)模拟一个500错误
(4)浏览器访问,可以看到显示的是global.html而不是500.html,因为全局异常处理的优先级高于默认异常处理
5.3拓展

全局异常的两个注解是通过 ExceptionHandlerExceptionResolver 类来支撑的,该类有一个重要方法:doResolveHandlerMethodException()
执行上述方法时,会获取异常发生的方法以及发生的异常。并在返回的模型和视图类也会带有这两个数据,我们可以根据这两个信息进行日志输出,在异常发生时可以迅速定位处理异常:
日志输出:
6.自定义异常的处理

6.1自定义异常说明


  • 如果SpringBoot提供的异常不能满足开发需求,我们也可以自定义异常。
  • 自定义异常处理(若采用默认的处理机制) = 自定义异常类 + @ResponseStatus
    @ResponseStatus 底层是 ResponseStatusExceptionResolve,底层调用 response.sendError(statusCode,resolvedReason);
  • 自定义异常的处理方式:

    • 当抛出自定义异常时,仍然会根据状态码,去匹配使用 xxx.html 显示(默认的异常处理机制)。
    • 或者将自定义异常类,放在你创建的全局异常处理器中进行处理(全局异常处理机制)

6.2自定义异常-应用实例

需求:自定义一个异常类 AccessException,当用户访问某个无权访问的路径时,抛出该异常,显示自定义异常的状态码。
(1)自定义异常类:AccessException.java
  1. package com.li.thymeleaf.exception;
  2. import org.springframework.http.HttpStatus;
  3. import org.springframework.web.bind.annotation.ResponseStatus;
  4. /**
  5. * @author 李
  6. * @version 1.0
  7. * 自定义的异常类
  8. * (1)如果继承Exception,属于编译异常
  9. * (2)如果继承RuntimeException,属于运行异常(一般来说都是继承RuntimeException)
  10. * (3)@ResponseStatus(value = HttpStatus.FORBIDDEN)
  11. *             指定发生此异常时,通过http协议返回的的状态码(403-Forbidden)
  12. */
  13. @ResponseStatus(value = HttpStatus.FORBIDDEN)
  14. public class AccessException extends RuntimeException {
  15.     public AccessException() {
  16.     }
  17.     //提供一个构造器,可以指定信息
  18.     public AccessException(String message) {
  19.         super(message);
  20.     }
  21. }
复制代码
(2)在Controller中模拟发生异常
  1. //模拟发生 AccessException
  2. @GetMapping("/errTest")
  3. public String test(String name) {
  4.     if (!"tom".equals(name)) {
  5.         throw new AccessException();
  6.     }
  7.     return "manage";//视图地址
  8. }
复制代码
(3)4xx.html(略)
(4)浏览器访问localhost:8080/errTest?name=jack,返回的页面如下:
因为返回的状态码为403,根据默认的异常处理机制,这里会寻找4xx.html页面返回给浏览器。如果是全局异常处理机制的话就会走全局异常处理的流程。
6.3注意事项和使用细节

如果把自定义异常类放在全局异常处理器,因为全局异常处理优先级高,因此会走全局异常的处理机制。
比如在上一个例子中,添加如下代码:
  1. package com.li.thymeleaf.exception;
  2. import ...
  3. /**
  4. * @author 李
  5. * @version 1.0
  6. * 全局异常处理器
  7. */
  8. @ControllerAdvice
  9. public class GlobalExceptionHandler {
  10.     @ExceptionHandler({AccessException.class})
  11.     public String handleException(Exception e, Model model,HandlerMethod handlerMethod) {
  12.         log.info("出现异常的方法={}", handlerMethod.getMethod());
  13.         log.info("异常信息={}", e.getMessage());
  14.         //model的数据会自动放入request域中
  15.         model.addAttribute("msg", e.getMessage());
  16.         return "/error/global";//指定转发到global.html
  17.     }
  18. }
复制代码
浏览器访问localhost:8080/errTest?name=jack,返回的页面如下:(global.html)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

用多少眼泪才能让你相信

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表