Spring 全家桶之 Spring Boot 2.6.4(七)- Exception

2022-09-26 15:52:05 浏览数 (1)


一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。

一、Spring Boot 默认错误处理机制

创建工程

使用IDEA创建一个工程spring-boot-exception,只需要添加基本的依赖即可

Spring Boot 默认错误处理

在Web端请求Spring Boot服务出现错误时,Spring Boot默认会返回一个空白的错误页面

在其他客户端请求发生错误时会返回JSON格式的错误数据

这些都是在Spring Boot的自动配置类ErrorMvcAutoConfiguration中配置好的

ErrorMvcAutoConfiguration自动配置类中通过@Bean注解给容器中添加了一些组件

  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver
  • DefaultErrorAttributes

当请求发生错误时ErrorPageCustomizer会通过registerErrorPages()方法获取path路径

获取到的path具体位置/error

通过Debug也可以确定获取到分发请求的路径是/error,也就是说当出现错误会来到/error这个映射的方法中去处理异常

而容器中注册的另一个组件BasicErrorController中可以处理/error开头的所有请求,BasicErrorController通过@RequestMapping注解定义了能够处理的请求的路径

errorHtml方法返回一个ModelAndView,并且如果ModelAndView为空就new一个ModelAndView,并传入一个name为error的View组件,也就是默认的空白页面,就是在Web页面请求发生错误时返回的页面;而error()方法返回的是一个包含了Map的ResponseEntity,也就是在其他端请求发送错误时返回的JSON格式的错误消息

当在Web端请求发生错误时,请求头中的Accept字段的值是text/html,所以才能够返回html页面

并且@RequestMapping注解中制定了produces属性的值为”text/html“

而在其他端Accept字段则为”*/*“,因此接收的是JSON格式的返回

二、Spring Boot 自定义错误页面

在BasicErrorController类中的errorHtml()方法中返回一个ModelAndView,也就是发生错误时We端显示的错误页面,而返回的这个ModelAndView首先是通过resolveErrorView()方法获取的

resolveErrorView()方法通过循环遍历所有的errorViewResolver,并调用errorViewResolver的resolverErrorView()方法来获ModelAndView,如果获取不到就返回null

errorViewResolver是ErrorViewResolver接口的实现类DefaultErrorViewResolver实例化来的,errorViewResolver通过调用自己的resolveErrorView()方法来回去ModelAndView,resolveErrorView()方法中又调用了resolve()方法来获取ModelAndView

resolve()方法首先是定义了一个errorViewName,既”error/“拼接通过参数传进来的viewName,viewName在resolveErrorView()方法中已经定义,既HttpStatus状态码或者SERIES_VIEWS枚举值

status状态码既404、405、500等,而SERIES_VIEWS枚举值为4xx、5xx

确定了errorViewName之后就是有模板引擎解析或者通过resolveResource方法遍历静态文件夹使用errorViewName拼接.html方式获取页面,从而获得一个ModelAndView并返回到BasicErrorController类的errorHtml()方法,最终返回到Web页面

自定义错误页面

根据上面的分析,想要自定义错误页面可以在templates文件夹下创建error文件夹,并且新增页面4xx.html

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>ERROR</title>
</head>
<body>
    <h1>Oops!404了,是不是没有写@RequestMapping</h1>
</body>
</html>

重启应用,在浏览器中输入一个该工程中不存在的地址

会自动跳转到4xx.html页面

新增controller包,增加一个HalloController并增加hallo()方法,该方法用于处理POST请求

代码语言:javascript复制
@RestController
public class HalloController {

    @PostMapping("/hallo")
    public String hallo(){
        return "success";
    }
}

重启应用,访问/hallo,还是会响应4xx.html页面,此时在error文件夹中新增一个405.html

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>ERROR 405</title>
</head>
<body>
    <h1>Oops! 405,是不是请求方式错了</h1>
</body>
</html>

重启应用,再次访问/hallo

页面显示为405.html,如果有具体的错误码命名的错误页面,会返回具体的错误码名字的页面,否则就返回4xx.html或则5xx.html,也就是精确匹配优先

自定义页面显示异常信息

在BasicErrorController中的errorHtml()方法中创建ModelAndView时,Model中会添加一些属性,也就是说页面能获取的信息都会放在model中,model的数据时通过调用getAttributes()方法获取的

这里调用了父类的getAttributes()方法

父类中又调用类ErrorAttribute的getAttributes()方法,DefaultErrorAttribute是ErrorAttribute接口的实现类,实际调用的是DefaultErrorAttribute类中的getErrorAttributes()方法来往Model中添加属性

addStatus()方法添加了一些status信息

Model中共添加了以下这些属性(信息):

timestamp:时间戳

status:Http状态码

error:错误提示

exception:异常对象

message:异常信息

errors:校验错误信息 可以在页面上取出这些信息。修改405.html,首先添加thymeleaf名称空间

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8"/>
  <title>ERROR 405</title>
</head>
<body>
  <h1 th:text="${status}">Oops! 405,是不是请求方式错了</h1>
  <h2 th:text="${timestamp}"></h2>
  <h2 th:text="${error}"></h2>
  <h2 th:text="${message}"></h2>
</body>
</html>

重启请求,访问/hallo

Model中添加的信息都能够正确的获取到

无模板引擎的情况

没有模板引擎的情况下,会遍历静态文件夹寻找页面

注释掉pom.xml文件中的thymeleaf的依赖,重启应用

无模板引擎无error文件夹的情况下

都没有的情况下,resolve()方法返回null

resolveErrorView()返回null就新建一个ModelAndView,传入name为error的View,这个View就是Spring Boot默认的空白页面

默认的错误页面error

默认的错误页面具体内容

三、Spring Boot 自定义JSON格式错误返回

自定义一个异常UserNotExistException

代码语言:javascript复制
public class UserNotExistException extends RuntimeException{

    public UserNotExistException(){
        super("用户不存在");
    }
}

在HalloController中定义一个方法,抛出500异常

代码语言:javascript复制
@RequestMapping("/hi")
public String hi(@RequestParam("user") String user){
    if (user.equals("stark")){
        throw new UserNotExistException();
    }
    return "Hi!";
}

定义5xx.html页面

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>5xx-Internal Serve Error</title>
</head>
<body>
    <h1 th:text="${status}">Oops! 5xx,是不是后端映射方法报错了</h1>
    <h2 th:text="${timestamp}"></h2>
    <h2 th:text="${error}"></h2>
    <h2 th:text="${message}"></h2>
    <h2 th:text="${info}"></h2>
</body>
</html>

定义一个异常处理器,在处理器中可以定义返回的JSON数据

代码语言:javascript复制
@ControllerAdvice
public class LilithExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Map<String, Object> handlerException(Exception e){
        Map<String,  Object> map = new HashMap<>();
        map.put("code", "user not exist");
        map.put("message", e.getMessage());
        return map;
    }
}    

重新启动应用,访问 http://localhost:8080/hi?user=stark

浏览器返回自定义的JSON格式数据,使用PostMan发送请求

PostMan也返回自定义的JSON格式数据。

浏览器和客户端返回的都是JSON格式数据,缺点是无法自适应,既根据Web和客户端返回页面或者JSON数据,想要能够自适应Web和客户端,转发到/error,使用/error的自适应处理

代码语言:javascript复制
@ResponseBody
public String handlerException(Exception e){
    Map<String,  Object> map = new HashMap<>();
    map.put("code", "user not exist");
    map.put("message", e.getMessage());
    return "forward:/error";
}

重新启动该应用,Web端可以返回页面

客户端可以返回JSON数据

但是Web返回的页面不是自定义的页面

之所以解析不到自定义的错误页面,是因为这里请求返回的是200,而error文件夹中并没有2xx.html这个页面,所以还是会返回默认的空白页面

要想解决这个问题就要重新定义HttpStatus状态码

根据获取状态码时使用的属性名,来设置自己的请求的状态码

代码语言:javascript复制
@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request){
    // 设置status
    request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 555);
    Map<String,  Object> map = new HashMap<>();
    map.put("code", "user not exist");
    map.put("message", e.getMessage());
    return "forward:/error";
}

再次重启应用,Web浏览器可以返回自定义的错误页面

客户端返回,可以返回JSON数据

但是客户端返回的JSON格式没有返回自定义的Key

BasicErrorController注册为容器中的组件是在没有ErrorController组件的情况下才会注册,因此可以自定义ErrorController替代BasicErrorController,重写errorHtml()方法,重新定义返回页面,重写error()方法,重新定义返回的JSON格式,这就是太复杂了。

页面上能够使用的数据或者JSON返回的数据都是通过errorAttributes.getErrorAttributes()获取的

因此可以重写ErrorAttribute替代原来的DefaultErrorAttribute,将自定义的JSON中的Key设置到返回的数据中

代码语言:javascript复制
@Component
public class LilithErrorAttributes extends DefaultErrorAttributes {

    // 返回的Map就是页面和JSON能够获取的字段
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        //添加要显示在JSON格式中的Key
        map.put("name", "stark");
        // 从请求中获取异常处理保存的信息
        Map info = (Map)webRequest.getAttribute("info", 0 );
        map.put("info", info);
        return map;
    }
}

修改异常处理方法,将异常信息保存到request中

代码语言:javascript复制
@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request){
    request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 555);
    Map<String,  Object> map = new HashMap<>();
    map.put("code", "自定义的信息,这个用户不能存在,别试了");
    map.put("msg", e.getMessage());
    // 将map保存到request中
    request.setAttribute("info", map);
    return "forward:/error";
}

重新启动应用,客户端请求 http://localhost:8080/hi?user=stark

在页面取出自定义的信息,在5xx.html的body标签中增加

代码语言:javascript复制
<h2 th:text="${info}"></h2>

取出页面信息,重新启动并在浏览器输入 http://localhost:8080/hi?user=stark

0 人点赞