一、Java 异常机制概述

Spring Boot 的所有异常处理都基于 java 的。

1.1、Java 异常类图

image.png

  • Java 内部的异常类 Throwable 包括了 Exception 和 Error 两大类,所有的异常类都是 Object 对象。
  • Error 是不可捕捉的异常,通俗的理解就是由于 java 内部 jvm 引起的不可预见的异常,比如 java 虚拟机运行错误,当内存资源错误,将会出现 OutOfMemoryError。此时 java 虚拟机会选择终止线程。
  • Excetpion 异常是程序本身引起的,它又分为运行时异常 RuntimeException,和非运行时(编译时)IOException 等异常。
    • 运行时异常 RuntimeException 例如:除数为零、 ArrayIndexOutOfBoundException 异常。
    • 非运行异常都是可查可捕捉的。Java 编译器会告诉程序他错了,错在哪里,正确的建议什么。我们可以通过 throws 配合 try-catch 来处理,例如:IOException

二、Java异常处理机制

2.1、异常处理机制的分类

在 Java 应用程序中,异常处理机制为:抛出异常捕捉异常

  • 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

针对不同的异常类型,Java 对处理的要求不一样

  • Error 错误,由于不可捕捉,不可查询,Java 允许不做任何处理。
  • 对于运行时异常 RuntimeException 不可查询异常,Java 允许程序忽略运行时异常,Java 系统会自动记录并处理。
  • 对于所有可查异常都可捕捉

三、SpringBoot中的异常处理示例

在 Spring Boot 应用程序中,通常统一处理异常的方法有使用注解处理 @ControllerAdvice、@RestControllerAdvice
本示例主要目的处理我们日常 Spring Boot 中的异常处理:

  • AOP切面处理异常:在 Web 项目中通过 @ControllerAdvice、@RestControllerAdvice 实现全局异常处理,@ControllerAdvice 和 @RestControllerAdvice 的区别相当于 Controller 和 RestController 的区别。
  • 在 Web 项目中实现 404、500 等状态的页面单独渲染

3.1、创建基于 @RestControllerAdvice 全局异常类示例

@RestControllerAdvice 注解是 Spring Boot 用于捕获 @Controller 和 @RestController 层系统抛出的异常( 注意,如果已经编写了 try-catch 且在 catch 模块中没有使用 throw 抛出异常, 则 @RestControllerAdvice 捕获不到异常 )。
@ExceptionHandler 注解用于指定方法处理的 Exception 的类型

控制层 ThrowableController 编写了4个 api 方法,

  • /index 是正常的方法;
  • /err 是手动抛出异常;
  • /match 除数为0的异常 ;
  • /nocatch 用了 try-catch 但没有抛出异常,不会被捕捉。

四个 api 其中 /err、/match 会被 CatchExceptionController 捕捉。

3.1.1、ThrowableController

  1. package com.wells.demo.throwable.controller;
  2. import io.swagger.annotations.Api;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. /**
  9. * Created by Wells on 2019年01月14日
  10. */
  11. @Api(tags = "ThrowableController", description = "throwable")
  12. @RestController
  13. @RequestMapping("throwable")
  14. public class ThrowableController {
  15. @GetMapping("/index")
  16. public Map<String, Object> index() {
  17. Map<String, Object> map = new HashMap<>();
  18. map.put("status", "0");
  19. map.put("msg", "正常的输出");
  20. return map;
  21. }
  22. /**
  23. * 手动RuntimeException
  24. */
  25. @GetMapping("/runtimeException")
  26. public Map<Object, Object> err() {
  27. throw new RuntimeException("抛出一个异常");
  28. }
  29. /**
  30. * 这里抛出的是 RuntimeException 不可查异常,虽然没有使用 try-catch 来捕捉 但系统以及帮助我们抛出了一次
  31. * */
  32. @GetMapping("/match")
  33. public Map<String, Object> matcherr() {
  34. Map<String, Object> map = new HashMap<>();
  35. map.put("status", "0");
  36. map.put("msg", "正常的输出");
  37. int j = 0;
  38. Integer i = 9 / j;
  39. return map;
  40. }
  41. /**
  42. * 这里抛出的是 RuntimeException 不可查异 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常
  43. * */
  44. @GetMapping("/nocatch")
  45. public Map<String, Object> nocatch() {
  46. Map<String, Object> map = new HashMap<>();
  47. map.put("status", "0");
  48. map.put("msg", "正常的输出 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常,所以没有异常,因为这里抛出的是 RuntimeException 不可查异常,系统也不会报错。");
  49. int j = 0;
  50. try {
  51. Integer i = 9 / j;
  52. } catch (Exception e) {
  53. }
  54. return map;
  55. }
  56. }

3.1.2、CatchExceptionController

package com.wells.demo.throwable.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import java.util.HashMap;
import java.util.Map;

/**
 * Description AOP处理异常: RestControllerAdvice
 * Created by wells on 2020-07-14 06:23:34
 */

@RestControllerAdvice(annotations = RestController.class)
public class CatchExceptionController {
    private static final Logger logger = LoggerFactory.getLogger(CatchExceptionController.class);

    /**
     * 处理所有的 RestController 层面的异常
     * */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public final Map<String, Object> handleAllExceptions(Exception ex, WebRequest request) {
        logger.error(ex.getMessage());
        Map<String, Object> map = new HashMap<>();
        map.put("status", -1);
        map.put("msg", ex.getLocalizedMessage());
        return map;
    }
}

3.1.1、Postman测试

http://localhost:8081/throwable/index

{
    "msg": "正常的输出",
    "status": "0"
}

http://localhost:8081/throwable/err

{
    "msg": "抛出一个异常",
    "status": -1
}

http://localhost:8081/throwable/match

{
    "msg": "/ by zero",
    "status": -1
}

http://localhost:8081/throwable/nocatch

{
    "msg": "正常的输出 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常",
    "status": "0"
}

3.2、创建基于 @ControllerAdvice 全局异常类 并且 返回ModelView 示例

  • @ControllerAdvice 和 @RestControllerAdvice 其实就是 @Controller 和 @RestController 的区别。
  • 无论是 @ControllerAdvice 还是 @RestControllerAdvice 都是可以捕捉 @Controller 和 @RestController 抛出的异常。
  • RestController也可以进行ModelView处理

不同的是,@Controller 异常,我们往往需要更加友好的界面。下面我们使用了 thymeleaf 模板来重新定义 /error 默认路由。

控制层 IndexController 编写了2个方法:

  • error.html 在 rerouces/templates/(默认目录)目录下,必须引入 thymeleaf 组件;也可以在application.properties中设置自定义目录;
  • /index 是正常的方法;
  • /index/err 是人为抛出异常,会被 CatchViewThrowableController 捕捉;
  • /index/match 除数为0的异常,会被 CatchViewThrowableController 捕捉;

3.2.1、pom设置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-boot</artifactId>
        <groupId>com.wells.demo</groupId>
        <version>1.0.1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>throwable</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>
</project>

3.2.2、application.properties设置

server.port=8081

# 控制彩色中断输出
spring.output.ansi.enabled=ALWAYS

spring.resources.static-locations=classpath:/views/
spring.thymeleaf.prefix=classpath:/views/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=LEGACYHTML5
spring.thymeleaf.cache=false

3.2.3、ViewThrowableController

package com.wells.demo.throwable.controller;

import io.swagger.annotations.Api;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

/**
 * Created by Wells on 2019年01月14日
 */

@Api(tags = "ThrowableController", description = "throwable")
@Controller
@RequestMapping("view/throwable")
public class ViewThrowableController {
    @GetMapping("/index")
    public String index(Model model) {
        model.addAttribute("msg","这是一个index页面的正常消息");
        return "index";
    }

    /**
     * 手动RuntimeException
     */
    @GetMapping("/runtimeException")
    public Map<Object, Object> err() {
        throw new RuntimeException("抛出一个异常");
    }

    /**
     * 这里抛出的是 RuntimeException 不可查异常,虽然没有使用 try-catch 来捕捉 但系统以及帮助我们抛出了一次
     * 这里抛出异常了,不会最终返回 index,而是走到了全局异常处理中
     * */
    @GetMapping("/match")
    public String matcherr() {
        int j = 0;
        Integer i = 9 / j;
        return "index";
    }
}

3.2.4、CatchViewThrowableController

package com.wells.demo.throwable.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

/**
 * Description AOP处理异常: ControllerAdvice
 * Created by wells on 2020-07-14 06:23:34
 */

@ControllerAdvice(annotations = Controller.class)
public class CatchViewThrowableController {
    private static final Logger logger = LoggerFactory.getLogger(CatchViewThrowableController.class);
    public static final String DEFAULT_ERROR_VIEW = "error";

    /**
     * 处理所有的 RestController 层面的异常
     * */
    @ExceptionHandler(Exception.class)
    public final ModelAndView handleAllExceptions(Exception ex, HttpServletRequest request) {
        logger.error(ex.getMessage());
        ModelAndView modelAndView = new ModelAndView();

        // 将异常信息设置如modelAndView
        modelAndView.addObject("msg", ex);
        modelAndView.addObject("url", request.getRequestURL());
        modelAndView.setViewName(DEFAULT_ERROR_VIEW);

        // 返回ModelAndView
        return modelAndView;
    }
}

3.3、根据不同响应码返回不同页面或者处理

略,参考:https://www.cnblogs.com/fishpro/p/spring-boot-study-throwable.html#_label3

代码示例:SpringBoot 异常处理

总结

一般工程中不会用到返回页面的处理,因为前后端已经分离,不会耦合在一起; 主要会用到全局异常处理

  • 全局异常处理的思想也是AOP:监听 Controller 注解,然后处理异常,我们也可以封装自己的自定义注解,使用AOP实现;

参考

https://www.cnblogs.com/fishpro/p/spring-boot-study-throwable.html