17《Spring Boot 入门教程》异常处理

1. 前言

程序中出现异常是普遍现象, Java 程序员想必早已习惯,根据控制台输出的异常信息,分析异常产生的原因,然后进行针对性处理的过程。

Spring Boot 项目中,数据持久层、服务层到控制器层都可能抛出异常。如果我们在各层都进行异常处理,程序代码会显得支离破碎,难以理解。

实际上,异常可以从内层向外层不断抛出,最后在控制器层进行统一处理。 Spring Boot 提供了全局性的异常处理机制,本节我们就分别演示下,默认情况、控制器返回视图、控制器返回 JSON 数据三种情况的异常处理方法。

2. Spring Boot 默认异常处理机制

Spring Boot 开发的 Web 项目具备默认的异常处理机制,无须编写异常处理相关代码,即可提供默认异常机制,下面具体演示下。

2.1 使用 Spring Initializr 创建项目

Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-default ,生成项目后导入 Eclipse 开发环境。

2.2 引入项目依赖

引入 Web 项目依赖即可。

实例:

        <!-- web项目依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

2.3 Spring Boot 默认异常处理

我们在启动项目, Spring Boot Web 项目默认启动端口为 8080 ,所以直接访问 http://127.0.0.1:8080 ,显示如下:

5eb80b58091e627c07350174.jpg

Spring Boot 默认异常信息提示页面

如上图所示,Spring Boot 默认的异常处理机制生效,当出现异常时会自动转向 /error 路径。

3. 控制器返回视图时的异常处理

在使用模板引擎开发 Spring Boot Web 项目时,控制器会返回视图页面。我们使用 Thymeleaf 演示控制器返回视图时的异常处理方式,其他模板引擎处理方式也是相似的。

3.1 使用 Spring Initializr 创建项目

Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-controller,生成项目后导入 Eclipse 开发环境。

3.2 引入项目依赖

引入 Web 项目依赖、热部署依赖。此处使用 Thymeleaf 演示控制器返回视图时的异常处理方式,所以引入 Thymeleaf 依赖。

实例:

        <!-- web项目依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <!-- ThymeLeaf依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

3.3 定义异常类

在异常处理之前,我们应该根据业务场景具体情况,定义一系列的异常类,习惯性的还会为各种异常分配错误码,如下图为支付宝开放平台的公共错误码信息。

5eb80d100974972d11550705.jpg

支付宝开放平台错误码

本节我们为了演示,简单的定义 2 个异常类,包含错误码及错误提示信息。

实例:

/**
 * 自定义异常
 */
public class BaseException extends Exception {
    /**
     * 错误码
     */
    private int code;
    /**
     * 错误提示信息
     */
    private String msg;

    public BaseException(int code, String msg) {
        super();
        this.code = code;
        this.msg = msg;
    }
    // 省略get set
}

实例:

/**
 * 密码错误异常
 */
public class PasswordException extends BaseException {
    public PasswordException() {
        super(10001, "密码错误");
    }
}

实例:

/**
 * 验证码错误异常
 */
public class VerificationCodeException extends BaseException {
    public VerificationCodeException() {
        super(10002, "验证码错误");
    }
}

3.4 控制器抛出异常

定义控制器 GoodsController ,然后使用注解 @Controller 标注该类,类中方法的返回值即为视图文件名。

在 GoodsController 类定义 4 个方法,分别用于正常访问、抛出密码错误异常、抛出验证码错误异常、抛出未自定义的异常,代码如下。

实例:

/**
 * 商品控制器
 */
@Controller
public class GoodsController {
    /**
     * 正常方法
     */
    @RequestMapping("/goods")
    public String goods() {
        return "goods";// 跳转到resource/templates/goods.html页面
    }

    /**
     * 抛出密码错误异常的方法
     */
    @RequestMapping("/checkPassword")
    public String checkPassword() throws PasswordException {
        if (true) {
            throw new PasswordException();// 模拟抛出异常,便于测试
        }
        return "goods";
    }

    /**
     * 抛出验证码错误异常的方法
     */
    @RequestMapping("/checkVerification")
    public String checkVerification() throws VerificationCodeException {
        if (true) {
            throw new VerificationCodeException();// 模拟抛出异常,便于测试
        }
        return "goods";
    }

    /**
     * 抛出未自定义的异常
     */
    @RequestMapping("/other")
    public String other() throws Exception {
        int a = 1 / 0;// 模拟异常
        return "goods";
    }
}


3.5 开发基于 @ControllerAdvice 的全局异常类

@ControllerAdvice 注解标注的类可以处理 @Controller 标注的控制器类抛出的异常,然后进行统一处理。

实例:

/**
 * 控制器异常处理类
 */
@ControllerAdvice(annotations = Controller.class) // 全局异常处理
public class ControllerExceptionHandler {
    @ExceptionHandler({ BaseException.class }) // 当发生BaseException类(及其子类)的异常时,进入该方法
    public ModelAndView baseExceptionHandler(BaseException e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("code", e.getCode());
        mv.addObject("message", e.getMessage());
        mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
        return mv;
    }

    @ExceptionHandler({ Exception.class }) // 当发生Exception类的异常时,进入该方法
    public ModelAndView exceptionHandler(Exception e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("code", 99999);// 其他异常统一编码为99999
        mv.addObject("message", e.getMessage());
        mv.setViewName("myerror");// 跳转到resource/templates/myerror.html页面
        return mv;
    }
}


按照 ControllerExceptionHandler 类的处理逻辑,当发生 BaseException 类型的异常时,会跳转到 myerror.html 页面,并显示相应的错误码和错误信息;当发生其他类型的异常时,错误码为 99999 ,错误信息为相关的异常信息。

3.6 开发前端页面

在 resource/templates 下分别新建 goods.html 和 myerror.html 页面,作为正常访问及发生异常时跳转的视图页面。

实例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>goods.html页面</title>
</head>
<body>
    <div>商品信息页面</div>
</body>
</html>
代码块12345678910

实例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>myerror.html页面</title>
</head>
<body>
    错误码:
    <span th:text="${code}"></span> 
    错误信息:
    <span th:text="${message}"></span>
</body>
</html>

3.7 测试

启动项目,分别访问控制器中的 4 个方法,结果如下:

5eba698b0933eda107420150.jpg

访问正常方法 /goods


5eba69970951143407420149.jpg

访问抛出自定义异常的方法 /checkPassword

5eba699d0995f62f07420151.jpg

et)

访问抛出自定义异常的方法 /checkVerification

5eba69a40962c06207440152.jpg

访问抛出未自定义异常的方法 /other

可见,当控制器方法抛出异常时,会按照全局异常类设定的逻辑统一处理。

4. 控制器返回 JSON 数据时的异常处理

在控制器类上添加 @RestController 注解,控制器方法处理完毕后会返回 JSON 格式的数据。

此时,可以使用 @RestControllerAdvice 注解标注的类 ,来捕获 @RestController 标注的控制器抛出的异常。

4.1 使用 Spring Initializr 创建项目

Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-exception-restcontroller,生成项目后导入 Eclipse 开发环境。

4.2 引入项目依赖

引入 Web 项目依赖、热部署依赖即可。

实例:

        <!-- web项目依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

4.3 定义异常类

还是使用上文中定义的异常类即可。

4.4 统一控制器返回数据格式

这时候,我们就需要思考一个问题了。前端请求后端控制器接口后,怎么区分后端接口是正常返回结果,还是发生了异常?

不论后端接口是正常执行,还是中间发生了异常,最好给前端返回统一的数据格式,便于前端统一分析处理。

OK,此时我们就可以封装后端接口返回的业务逻辑对象 ResultBo ,代码如下:

实例:

/**
 * 后端接口返回的统一业务逻辑对象
 */
public class ResultBo<T> {

    /**
     * 错误码 0表示没有错误(异常) 其他数字代表具体错误码
     */
    private int code;
    /**
     * 后端返回消息
     */
    private String msg;
    /**
     * 后端返回的数据
     */
    private T data;

    /**
     * 无参数构造函数
     */
    public ResultBo() {
        this.code = 0;
        this.msg = "操作成功";
    }

    /**
     * 带数据data构造函数
     */
    public ResultBo(T data) {
        this();
        this.data = data;
    }

    /**
     * 存在异常的构造函数
     */
    public ResultBo(Exception ex) {
        if (ex instanceof BaseException) {
            this.code = ((BaseException) ex).getCode();
            this.msg = ex.getMessage();
        } else {
            this.code = 99999;// 其他未定义异常
            this.msg = ex.getMessage();
        }
    }
    // 省略 get set
}

4.5 控制器抛出异常

定义控制器 RestGoodsController ,并使用 @RestController 注解标注。在其中定义 4 个方法,然后分别用于正常访问、抛出密码错误异常、抛出验证码错误异常,以及抛出不属于自定义异常类的异常。

实例:

/**
 * Rest商品控制器
 */
@RestController
public class RestGoodsController {
    /**
     * 正常方法
     */
    @RequestMapping("/goods")
    public ResultBo goods() {
        return new ResultBo<>(new ArrayList());// 正常情况下应该返回商品列表
    }

    /**
     * 抛出密码错误异常的方法
     */
    @RequestMapping("/checkPassword")
    public ResultBo checkPassword() throws PasswordException {
        if (true) {
            throw new PasswordException();// 模拟抛出异常,便于测试
        }
        return new ResultBo<>(true);// 正常情况下应该返回检查密码的结果true或false
    }

    /**
     * 抛出验证码错误异常的方法
     */
    @RequestMapping("/checkVerification")
    public ResultBo checkVerification() throws VerificationCodeException {
        if (true) {
            throw new VerificationCodeException();// 模拟抛出异常,便于测试
        }
        return new ResultBo<>(true);// 正常情况下应该返回检查验证码的结果true或false
    }

    /**
     * 抛出未自定义的异常
     */
    @RequestMapping("/other")
    public ResultBo other() throws Exception {
        int a = 1 / 0;// 模拟异常
        return new ResultBo<>(true);
    }
}

4.6 开发基于 @RestControllerAdvice 的全局异常类

@RestControllerAdvice 注解标注的类可以处理 RestController 控制器类抛出的异常,然后进行统一处理。

实例:

/**
 * Rest控制器异常处理类
 */
@RestControllerAdvice(annotations = RestController.class) // 全局异常处理
public class RestControllerExceptionHandler {
    /**
     * 处理BaseException类(及其子类)的异常
     */
    @ExceptionHandler({ BaseException.class })
    public ResultBo baseExceptionHandler(BaseException e) {
        return new ResultBo(e);
    }

    /**
     * 处理Exception类的异常
     */
    @ExceptionHandler({ Exception.class })
    public ResultBo exceptionHandler(Exception e) {
        return new ResultBo(e);
    }
}

4.7 测试

启动项目,分别尝试访问控制器中的 4 个接口,结果如下。

5eba79eb09e0a45f07410151.jpg

访问正常方法 /goods

5eba79f509f15d7507390152.jpg

访问抛出异常的方法 /checkPassword

5eba79fe0950fa0a07400146.jpg

访问抛出异常的方法 /checkVerification

5eba7a0409f3972f07370151.jpg

访问抛出异常的方法 /other

5. 小结

Spring Boot 的默认异常处理机制,实际上只能做到提醒开发者 “这个后端接口不存在” 的作用,作用非常有限。

所以我们在开发 Spring Boot 项目时,需要根据项目的实际情况,定义各类异常,并站在全局的角度统一处理异常。

不管项目有多少层次,所有异常都可以向外抛出,直到控制器层进行集中处理。

  • 对于返回视图的控制器,如果没发生异常就跳转正常页面,如果发生异常可以自定义错误信息页面。
  • 对于返回 JSON 数据的控制器,最好是定义统一的数据返回格式,便于前端根据返回信息进行正常或者异常情况的处理。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容