Springboot异常处理只会@ControllerAdvice+@ExceptionHandler?还远远不够!

  • 2019 年 10 月 5 日
  • 筆記

当系统出现异常时候,或404,或500,默认返回的错误页面通常非常简陋,用户也看不懂,这时候我们想通过一些手段,提示用户访问的资源不存在,或者请稍后再试。

同时有个统一的异常处理机制可以提高我们系统的健壮性,微服务化之后系统之间的调用结果会影响到整个服务的可用性。如果被调用方出现异常没有返回统一的异常处理结果,很容易会调用方疑惑,然后滚大整个异常,这时候你看到整个服务之间都在报错,这不是我们想看到的~

那么基于springboot,我们有多少种异常处理方式呢?

静态处理

这是一种比较偷懒也是最简单的处理方式,直接放置一个静态的页面。我们静态看到有些项目直接就返回一个大大的404图片作为异常的处理显示,其实就是这里说到的静态处理方式。

我们来看下错误页面的存放位置:

可以看到,我是存放在了static目录的error文件夹下,新建了一个404.html用于处理404错误。既然是静态页面,那么就不能使用动态渲染,所以通常静态的异常页面都会写得比较死,要么就直接就是一个404图片。

静态页面中如果写了中文,这是显示的内容容易乱码,我们只需在配置文件application.properties中添加以下encoding代码:

spring.http.encoding.force=true

我们先来访问一个不存在的路径http://localhost:8080/xxxx,看下效果:

  • 未处理前:
  • 静态处理后:

我们的404.html页面起作用啦,如果不存在404.html,或者出现401异常的时候,系统就会自动匹配到4xx.html页面,所以这个4xx相当于可以通配处理所有的客户端错误:4xx。类似的500.html和5xx.html处理服务器错误:5xx。

好,上面的静态处理异常我们已经可以懂了,那么你知道它的原理吗?

其实在springboot项目启动的时候,会去加载异常处理的默认配置ErrorMvcAutoConfiguration,而在ErrorMvcAutoConfiguration里面,有个默认的异常处理控制器BasicErrorController(org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController),我们在这构造方法中打个端点,可以看到异常处理器errorViewResolvers的resourceProperties中就默认初始化好了所有可以存放静态异常页面的地方。

然后你再把端点打在ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)方法上,你就会清晰看到,其实springboot项目会循环搜索这4个位置的文件夹,看时候有404.html页面,如果有就直接返回,没有就返回异常的默认处理页面。

总结一下:静态处理的错误页面可以存放4个位置,分别按先后顺序搜索

/META-INF/resources/error/404.html -> /resources/error/404.html  -> /static/error/404.html -> /public/error/404.html

当找不到精确匹配404.html的时候,就会找通配的4xx.html。

ok,静态处理就先讲到这里~

动态处理

刚才我们说了一种静态处理异常的方式,但是缺点很明显,不能在静态页面中动态渲染数据啊!这无疑是比较致命的,有什么办法让页面能动态处理呢?

这里给大家介绍4种方式

  • 直接在templates下写error页面,如果freemaker的error.ftl
  • 重写ErrorController,覆盖BasicErrorController
  • 继承ErrorPageRegistrar,重写registerErrorPages方法
  • @ControllerAdvice+@ExceptionHandler组合

1、直接写error.ftl

这个其实和静态处理中一样,页面处理器在静态资源中找不到对应的页面之后就会直接去templates下找view直接返回,默认的名字就叫做error,所以当我们直接在tempates下写error.ftl时候,我们就可以直接展示动态错误处理页面了。

但是这样我们直接返回页面,没办法自己控制错误的业务逻辑处理,所以,只有当我们出现错误之后没有相关的处理,我们才这样去展示。

2、重写ErrorController

在静态处理代码分析的时候我们说到了项目启动时候就会自动加载默认异常处理配置ErrorMvcAutoConfiguration,会默认加载BasicErrorController作为异常处理的控制器。那么我们想要自己定义处理逻辑的话,我们就直接覆盖掉BasicErrorController,然后重写一个就好了。

我们先来看下ErrorController长什么样子的

  • org.springframework.boot.web.servlet.error.ErrorController
@FunctionalInterface  public interface ErrorController {      String getErrorPath();  }

getErrorPath()其实表示的就是出现异常之后应该调用的链接,所以当我们如果返回的链接是/error时候,我们应该新建一个controller处理方法对应/error链接。

总结上面的逻辑,我写了如下代码:

  • 1、实现ErrorController接口
  • 2、重写getErrorPath()方法
  • 3、定义web页面异常处理和异步异常处理方法
@Configuration  @Controller  public class IErrorController implements ErrorController {        private final static String ERROR_PATH = "/error";        @Override      public String getErrorPath() {          return ERROR_PATH;      }        @RequestMapping(value = ERROR_PATH, produces = "text/html")      public String errorView(HttpServletRequest request) {            Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);          Object errorMess = request.getAttribute(RequestDispatcher.ERROR_MESSAGE);            request.setAttribute("message", "这是错误提示!!" + errorMess);            if (status != null) {              Integer statusCode = Integer.valueOf(status.toString());                if (statusCode == HttpStatus.NOT_FOUND.value()) {                  return "/common/404";              } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {                  return "/common/500";              }          }          return "/common/error";      }        @ResponseBody      @RequestMapping(value = ERROR_PATH)      public Object errorJson() {          return "这里放回json数据~";      }  }

在templates/common文件夹下建好对应的页面:

渲染结果:

3、继承ErrorPageRegistrar

ErrorPageRegistrar是一个错误页面的注册器,在ErrorMvcAutoConfiguration中我们依然可以找到对应的源码代码,它默认帮我们写了一个ErrorPageCustomizer用于处理注册的错误页面,我们可以看到启动时候,会默认先把/error页面注册进去。

接下来,我们来重写一个ErrorPageRegistrar,先来看下接口的源代码:

public interface ErrorPageRegistrar {     void registerErrorPages(ErrorPageRegistry registry);  }

只有一个方法registerErrorPages,参数是错误页面注册中心ErrorPageRegistry。接下来我们自己来定义一个类。

  • com.example.config.MyErrorPageRegistrar
@Component  public class MyErrorPageRegistrar implements ErrorPageRegistrar {        @Override      public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {            ErrorPage page404 = new ErrorPage(HttpStatus.NOT_FOUND, "/404");          ErrorPage page500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500");            System.out.println("初始化了!");          errorPageRegistry.addErrorPages(page404, page500);      }  }

我们定义了两个错误页面,一个ErrorPage 404,还要ErrorPage 500,分别链接到/404,还有/500,所以需要给controller定义两个方法:

@Controller  public class IndexController {        @RequestMapping("/404")      public String error404(HttpServletRequest request) {          request.setAttribute("message", "ErrorPageRegistrar的404页面");          return "common/404";      }        @RequestMapping("/500")      public String error500() {          return "common/500";      }    }

这样的话,就会分别链接到IndexController的方法里面,然后再跳转到对应的页面中。这就实现了错误页面的动态处理了。

我试了一下,当implements ErrorControllerimplements ErrorPageRegistrar两种方式同时存在的时候,优先通过ErrorPageRegistrar来处理异常。这点得牢记哈。

4、@ControllerAdvice+@ExceptionHandler组合

接下来再聊聊一个人人都应懂得@ControllerAdvice+@ExceptionHandler组合。

其实不一定需要组合来一起用,当我们需要在某个特定控制器里面处理特定异常时候,我们的@ExceptionHandler可以直接写在controller中,这样的话@ExceptionHandler就只能处理这个单个controller的异常。

那有时候我们想全局处理所有的控制器的异常,于是就有了@ControllerAdvice,它会控制器增强,会应用到所有的controller上,这样就实现了我们想要的全局异常处理。

@Slf4j  @ControllerAdvice  public class GlobalExceptionHandler {        @ExceptionHandler(value = Exception.class)      public ModelAndView defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, Exception e) {            log.error("------------------>捕捉到全局异常", e);            if (req.getHeader("accept").contains("application/json")  || (req.getHeader("X-Requested-With")!= null                  && req.getHeader("X-Requested-With").contains("XMLHttpRequest") )) {              try {                  System.out.println(e.getMessage());                  Result result = Result.fail(e.getMessage(), "some error data");                    resp.setCharacterEncoding("utf-8");                  PrintWriter writer = resp.getWriter();                  writer.write(JSONUtil.toJsonStr(result));                  writer.flush();              } catch (IOException i) {                  i.printStackTrace();              }              return null;          }            if(e instanceof HwException) {              //...          }            ModelAndView mav = new ModelAndView();          mav.addObject("exception", e);          mav.addObject("message", e.getMessage());          mav.addObject("url", req.getRequestURL());          mav.setViewName("error");          return mav;      }    }

当然了,这样的@ExceptionHandler(value = Exception.class)比价偷懒,你完全可以给value赋不同的Exception,然后针对不同的Exception类型做不同的处理。

下面是renren-fast项目的全局异常处理,我们来学习学习:

  • https://gitee.com/renrenio/renren-fast.git
@RestControllerAdvice  public class RRExceptionHandler {     private Logger logger = LoggerFactory.getLogger(getClass());       /**      * 处理自定义异常      */     @ExceptionHandler(RRException.class)     public R handleRRException(RRException e){        R r = new R();        r.put("code", e.getCode());        r.put("msg", e.getMessage());          return r;     }       @ExceptionHandler(NoHandlerFoundException.class)     public R handlerNoFoundException(Exception e) {        logger.error(e.getMessage(), e);        return R.error(404, "路径不存在,请检查路径是否正确");     }       @ExceptionHandler(DuplicateKeyException.class)     public R handleDuplicateKeyException(DuplicateKeyException e){        logger.error(e.getMessage(), e);        return R.error("数据库中已存在该记录");     }       @ExceptionHandler(AuthorizationException.class)     public R handleAuthorizationException(AuthorizationException e){        logger.error(e.getMessage(), e);        return R.error("没有权限,请联系管理员授权");     }       @ExceptionHandler(Exception.class)     public R handleException(Exception e){        logger.error(e.getMessage(), e);        return R.error();     }  }

你可以看到,他对异常的类型分得很多,很清楚,这样我们就可以让异常展示的结果越具体。

总结

好了,不容易呀,终于写完了,2个多小时,坚持原创的第二天(20190918),打卡打卡。希望你们会喜欢。