Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 374|回复: 0

Spring Boot异常处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-7-7 10:11:30 | 显示全部楼层 |阅读模式

    一.默认映射

    我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容。

    选择一个之前实现过的Web应用为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如:

    1 @RequestMapping("/hello")
    2 public String hello() throws Exception {
    3     throw new Exception("发生错误");
    4 }

    此时,可以看到类似下面的报错页面,该页面就是Spring Boot提供的默认error映射页面。

     

    alt=默认的错误页面

    二.映射到页面

    虽然,Spring Boot中实现了默认的error映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示。

    下面我们以之前的Web应用例子为基础,进行统一异常处理的改造。

    • 创建全局异常处理类:通过使用@ControllerAdvice定义统一的异常处理类,而不是在每个Controller中逐个定义。@ExceptionHandler用来定义函数针对的异常类型,最后将Exception对象和请求URL映射到error.html
     1 @ControllerAdvice
     2 class GlobalExceptionHandler {
     3 
     4     public static final String DEFAULT_ERROR_VIEW = "error";
     5 
     6     @ExceptionHandler(value = Exception.class)
     7     public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
     8         ModelAndView mav = new ModelAndView();
     9         mav.addObject("exception", e);
    10         mav.addObject("url", req.getRequestURL());
    11         mav.setViewName(DEFAULT_ERROR_VIEW);
    12         return mav;
    13     }
    14 
    15 }
    • 实现error.html页面展示:在templates目录下创建error.html,将请求的URL和Exception对象的message输出。
     1 <!DOCTYPE html>
     2 <html>
     3 <head lang="en">
     4     <meta charset="UTF-8" />
     5     <title>统一异常处理</title>
     6 </head>
     7 <body>
     8     <h1>Error Handler</h1>
     9     <div th:text="${url}"></div>
    10     <div th:text="${exception.message}"></div>
    11 </body>
    12 </html>

    启动该应用,访问:http://localhost:8080/hello,可以看到如下错误提示页面。

    alt=自定义的错误页面

    通过实现上述内容之后,我们只需要在Controller中抛出Exception,当然我们可能会有多种不同的Exception。然后在@ControllerAdvice类中,根据抛出的具体Exception类型匹配@ExceptionHandler中配置的异常类型来匹配错误映射和处理。

    三.返回JSON格式

    在上述例子中,通过@ControllerAdvice统一定义不同Exception映射到不同错误处理页面。而当我们要实现RESTful API时,返回的错误是JSON格式的数据,而不是HTML页面,这时候我们也能轻松支持。

    在上述例子中,通过@ControllerAdvice统一定义不同Exception映射到不同错误处理页面。而当我们要实现RESTful API时,返回的错误是JSON格式的数据,而不是HTML页面,这时候我们也能轻松支持。

    本质上,只需在@ExceptionHandler之后加入@ResponseBody,就能让处理函数return的内容转换为JSON格式。

    下面以一个具体示例来实现返回JSON格式的异常处理。

    • 创建统一的JSON返回对象,code:消息类型,message:消息内容,url:请求的url,data:请求返回的数据
     1 public class ErrorInfo<T> {
     2 
     3     public static final Integer OK = 0;
     4     public static final Integer ERROR = 100;
     5 
     6     private Integer code;
     7     private String message;
     8     private String url;
     9     private T data;
    10 
    11     // 省略getter和setter
    12 
    13 }
    • 创建一个自定义异常,用来实验捕获该异常,并返回json
    1 public class MyException extends Exception {
    2 
    3     public MyException(String message) {
    4         super(message);
    5     }
    6     
    7 }
    • Controller中增加json映射,抛出MyException异常
    1 @Controller
    2 public class HelloController {
    3 
    4     @RequestMapping("/json")
    5     public String json() throws MyException {
    6         throw new MyException("发生错误2");
    7     }
    8 
    9 }
    • MyException异常创建对应的处理
     1 @ControllerAdvice
     2 public class GlobalExceptionHandler {
     3 
     4     @ExceptionHandler(value = MyException.class)
     5     @ResponseBody
     6     public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception {
     7         ErrorInfo<String> r = new ErrorInfo<>();
     8         r.setMessage(e.getMessage());
     9         r.setCode(ErrorInfo.ERROR);
    10         r.setData("Some Data");
    11         r.setUrl(req.getRequestURL().toString());
    12         return r;
    13     }
    14 
    15 }
    • 启动应用,访问:http://localhost:8080/json,可以得到如下返回内容:
    {
        code: 100,
        data: "Some Data",
        message: "发生错误2",
        url: "http://localhost:8080/json"
    }

    代码示例至此,已完成在Spring Boot中创建统一的异常处理,实际实现还是依靠Spring MVC的注解,更多更深入的使用可参考Spring MVC的文档。

    本文的相关例子可以查看下面仓库中的chapter3-1-6目录:

    附:

    在探寻spring的异常处理机制的时候,我分别使用了三种方式。三种方式都是使用的@ExceptionHandler注解。
    当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。
    首先需要为自己的系统设计一个自定义的异常类,通过它来传递状态码。
     1 /** * Created by liuruijie.
     2  * 自定义异常
     3  */
     4 public class SystemException extends RuntimeException{
     5     private String code;//状态码
     6     public SystemException(String message, String code) {
     7         super(message);
     8         this.code = code;
     9     }
    10     public String getCode() {
    11         return code;
    12     }
    13 }

    第一种思路,设计一个基类。

     1 /**
     2  * Created by liuruijie.
     3  * 处理异常的类,需要处理异常的Controller直接继承这个类
     4  */
     5 public class BaseController {
     6     /**
     7      * 处理Controller抛出的异常
     8      * @param e 异常实例
     9      * @return Controller层的返回值
    10      */
    11     @ExceptionHandler
    12     @ResponseBody
    13     public Object expHandler(Exception e){
    14         if(e instanceof SystemException){
    15             SystemException ex= (SystemException) e;
    16             return WebResult.buildResult().status(ex.getCode())
    17                             .msg(ex.getMessage());
    18         }else{
    19             e.printStackTrace();
    20             return WebResult.buildResult().status(Config.FAIL)
    21                             .msg("系统错误");
    22         }
    23     }
    24 }

    之后所有需要异常处理的Controller都继承这个类,从而获取到异常处理的方法。
    虽然这种方式可以解决问题,但是极其不灵活,因为动用了继承机制就只为获取一个默认的方法,这显然是不好的。

    第二种思路,将这个基类变为接口,提供此方法的默认实现(也就是接口中的default方法,java8开始支持接口方法的默认实现)

     1 /**
     2  * Created by liuruijie.
     3  * 接口形式的异常处理
     4  */
     5 public interface DataExceptionSolver {
     6     @ExceptionHandler
     7     @ResponseBody
     8     default Object exceptionHandler(Exception e){
     9         try {
    10             throw e;
    11         } catch (SystemException systemException) {
    12             systemException.printStackTrace();
    13             return WebResult.buildResult().status(systemException.getCode())
    14                     .msg(systemException.getMessage());
    15         } catch (Exception e1){
    16             e1.printStackTrace();
    17             return WebResult.buildResult().status(Config.FAIL)
    18                     .msg("系统错误");
    19         }
    20     }
    21 }
    这种方式虽然没有占用继承,但是也不是很优雅,因为几乎所有的Controller都需要进行异常处理,于是我每个Controller都需要去写implement DataExceptionSolver,这显然不是我真正想要的。况且这种方式依赖java8才有的语法,这是一个很大的局限。
     
    第三种思路,使用加强Controller做全局异常处理。
    所谓加强Controller就是@ControllerAdvice注解,有这个注解的类中的方法的某些注解会应用到所有的Controller里,其中就包括@ExceptionHandler注解。
    于是可以写一个全局的异常处理类:
     1 /**
     2  * Created by liuruijie on 2016/12/28.
     3  * 全局异常处理,捕获所有Controller中抛出的异常。
     4  */
     5 @ControllerAdvice
     6 public class GlobalExceptionHandler {
     7    //处理自定义的异常
     8    @ExceptionHandler(SystemException.class) 
     9    @ResponseBody
    10    public Object customHandler(SystemException e){
    11       e.printStackTrace();
    12       return WebResult.buildResult().status(e.getCode()).msg(e.getMessage());
    13    }
    14    //其他未处理的异常
    15    @ExceptionHandler(Exception.class)
    16    @ResponseBody
    17    public Object exceptionHandler(Exception e){
    18       e.printStackTrace();
    19       return WebResult.buildResult().status(Config.FAIL).msg("系统错误");
    20    }
    21 }

    这个类中只处理了两个异常,但是已经满足了大部分需要,如果还有需要特殊处理的地方,可以再加上处理的方法就行了。第三种实现方式是目前我知道的最优雅的方式了。
    如此,我们现在的Controller中的方法就可以很简洁了,比如处理登陆的逻辑就可以这样简单的写:

     1 /**
     2  * Created by liuruijie on 2016/12/28.
     3  * 账号
     4  */
     5 @RestController
     6 @RequestMapping("passport")
     7 public class PassportController {
     8     PassportService passportService;
     9         @RequestMapping("login")
    10     public Object doLogin(HttpSession session, String username, String password){
    11         User user = passportService.doLogin(username, password);
    12         session.setAttribute("user", user);
    13         return WebResult.buildResult().redirectUrl("/student/index");
    14     }
    15 }

    而在passprotService的doLogin方法中,可能会抛出用户名或密码错误等异常,然后就会交由GlobalExceptionHandler去处理,直接返回异常信息给前端,然后前端也不需要关心是否返回了异常,因为这些都已经定义好了。
    前端js代码只需要这样写:

    1 //登陆
    2 AJAX.POST("/passport/login", {
    3     username:name,
    4     password:psw
    5 })

    一个异常在其中流转的过程为:
    比如doLogin方法抛出了自定义异常,其code为:FAIL,message为:用户名或密码错误,由于在controller的方法中没有捕获这个异常,所以会将异常抛给GlobalExceptionHandler,然后GlobalExceptionHandler通过WebResult将状态码和提示信息返回给前端,前端通过默认的处理函数,弹框提示用户“用户名或密码错误”。而对于这样的一次交互,我们根本不用编写异常处理部分的逻辑。

    到这里,代码已经简洁了很多,而且重用性大大提高。

    HTTP状态码
    100 到199 的状态码代表信息,描述对于请求的处理。
    200 到 299 的状态码表示客户端发来的请求已经被接收并正确处理。
    300 到 399 的状态码表示客户端需要进一步的处理才能完成请求,比如重定向到另一个地址。
    400 到 499 的状态码表示客户端的请求有错误,需要修正。404就是这种情况。
    500 到 599 的状态码表示服务器在处理客户端请求时发生了内部错误。

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

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

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-1 08:46 , Processed in 0.062760 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表