41.异常处理流程

默认异常情况

在SpringBoot的项目中,如果出现异常,那么默认是如下白板页面。
在这里插入图片描述
那么我们一般并不会使用默认的错误页面,而都是自定义错误页面。

定制错误页面
定制错误页面也有两类情况。

1、如果们使用模板引擎的情况下,那么我们可以在 templates 目录下创建一个 error 目录,在里面创建 404.html 页面,这样如果出现 404 的问题,就会跳转到该页面。

但是4开头的错误和5开头的错误页是很多的,一个一个去写,比较麻烦,所以SpringBoot也帮我们提供了通用的写法,即我们可以创建 4xx.html 和 5xx.html 页面,通用性更强。如果400和4xx同时存在,则精确匹配优先。

2、如果没有使用模板引擎的情况下,那么我们就要在默认的静态资源文件夹下创建 error 目录,再创建对应的 4xx 或 5xx 页面。
如果模板引擎无法解析,且静态资源文件夹下也没有我们指定的错误页面,那么就来到了SpringBoot的默认错误白板页面。

使用 thymeleaf 模板引擎,我们可以通过行内写法,来获取一些错误的数据,更多详细信息可以查看thymeleaf官网,我这里就直接给出使用方法。

                    <h1>status:[[${status}]]</h1>
                    <h1>timestamp:[[${timestamp}]]</h1>
                    <h1>exception:[[${exception}]]</h1>
                    <h1>message:[[${message}]]</h1>

还要注意的一点是,SpringBoot2.x 后,我们需要在 application.properties中写如下配置:

# 页面上获异常对象,true表示能得到,默认是得不到
# 具体都在这个配置文件中看到 ErrorProperties
server.error.include-exception=true
# 页面上获取错误信息对象,always表示一直开启能获取
server.error.include-message=always

定制异常信息
我们现在可以自定义异常页面了,那么我们如何自定义异常信息呢?那么我们就可以来自定义一个异常处理器,在一个类上标注 @ControllerAdvice 注解,则表示该类是用来处理异常的。

第一种方式
我们可以直接给页面返回 json 数据,这样不管是浏览器端还是手机或者其它客户端访问返回的都是统一的 json 数据格式。

    @ExceptionHandler(UserNotExistException.class)
    @ResponseBody 
    // 异常信息通过参数 Exception 获取
    public Map<String, Object> handleException(Exception e){
        Map<String, Object> map = new HashMap<>();
        map.put("code", "403");
        map.put("message", e.getMessage());
        return map;
    }

浏览器页面显示如下:

postman模拟其他客户端发送请求返回如下:
在这里插入图片描述

但是这样其实并不雅观,手机还好,但浏览器访问时,应该返回一个页面,而不是这么一行json串,所以要进行适配。

第二种方式
不使用 @ResponseBody 将数据写出去,而是将请求转发到 /error 路径,这个 /error 是 SpringBoot处理异常默认的访问路径,SpringBoot会帮我们自动做适配。这样浏览器返回的就是一个错误页面了。

    @ExceptionHandler({UserNotExistException.class})
    public String handleException(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "203");
        map.put("message", "用户出错啦");
        return "forward:/error";
    }

浏览器效果如下

在这里插入图片描述
postman模拟其他客户端如下:
在这里插入图片描述

还是有点问题,因为对异常进行了处理,这里状态码是 200 ,并不是 500,所以并没有跳转到我们自定义的错误页面。
查看源码:
在 org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController 类中,下面方法用来得到错误页面。

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }


HttpStatus status = getStatus(request); 用来获取状态码。点进 getStatus()方法发现,它获取的是一个请求域中的静态常量值:
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
值为:
String ERROR_STATUS_CODE = "javax.servlet.error.status_code";

所以我们将请求转发给 SpringBoot 默认的处理异常的访问路径时,需要自己手动设置状态码。 如果不设置的话,默认就是 200 了。

   

 @ExceptionHandler({UserNotExistException.class})
    public String handleException(Exception e, HttpServletRequest request) {
        request.setAttribute("javax.servlet.error.status_code", 400);
        Map<String, Object> map = new HashMap<>();
        map.put("code", "203");
        map.put("message", "用户出错啦");
        return "forward:/error";
    } 



来到了自定义的错误页面。

第三种方式

第二种方式还有一点缺陷,就是我们无法将我们自定义的信息携带到错误页面上。我map中存放的code和message都没在错误页面上显示。
查看源码:

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

出现错误以后,会来到/error请求,会被BasicErrorController处理,在 BasicErrorController中,这两个方法中写到异常页面的数据,都是由 getErrorAttributes() 来获取的。

在 ErrorMvcAutoConfiguration 错误的mvc自动创装配类中,我们可以看到,

   

 @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
            ObjectProvider<ErrorViewResolver> errorViewResolvers) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
                errorViewResolvers.orderedStream().collect(Collectors.toList()));
    } 

 

BasicErrorController 类上标注了 @ConditionalOnMissingBean,所以想要将自定义的异常信息携带出去,我们可以写一个 ErrorController 的实现类,放到容器中,这样就会使用我们实现类的逻辑,如果我们重写,那么就用SpringBoot自带的了,这是第一种方式。

第二种方式:
编写一个ErrorController的实现类过于麻烦,一般采取第二种方式。
之前说过,异常页面获取的数据,都是通过 getErrorAttributes() 来进行获取的。此方法是在 AbstractErrorController类中定义的。

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
        WebRequest webRequest = new ServletWebRequest(request);
        return this.errorAttributes.getErrorAttributes(webRequest, options);
    }



这个 errorAttributes 就是 private final ErrorAttributes errorAttributes;
而这个 ErrorAttributes 也是SpringBoot帮我们添加了,用@ConditionalOnMissingBean 注解标注,那么我们可以自定义一个,使用我们自己的逻辑。

    

@Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }



在DefaultErrorAttributes中的 getErrorAttributes()方法中,定义了这些数据,所以我们在错误页面就可以看到这些信息。

       

 private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        addPath(errorAttributes, webRequest);
        return errorAttributes;
    } 



那么我们就自己写一个 ErrorAttributes的实现类。为了方便,我这直接继承SpringBoot默认的实现类。

// 给容器中加入我们自定义的 ErrorAttribute

@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        // 先调用父方法,获取之前原有的数据
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        // 在之前原有的数据上进行添加
        map.put("tel", "12345678910");
        return map;
    }



可以看到,自定义的数据也是添加成功。

自定义的数据添加是成功了,但是我们在错误解析器中定义的map数据还是没拿到,我们可以如下配置。

@ExceptionHandler({UserNotExistException.class})
    public String handleException(Exception e,HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user not exist");
        map.put("message", "用户出错啦");
        // 手动设置状态码,不然被错误解析后状态码默认为200
        request.setAttribute("javax.servlet.error.status_code",400); 
        // 将我们写的map数据保存到 请求域中。
        request.setAttribute("ext", map);
        return "forward:/error";
    }



我们是将请求转发给 /error 请求,而之前也说了,异常的信息都是由 getErrorAttributes() 方法返回的map提供的,所以我们来到自己自定义的ErrorAttribute,在重写的 getErrorAttributes() 方法中,我们要拿出我们之前保存在请求域中的值。

在重写的方法中,入参有一个 WebRequest 对象,该对象就是对 request 的一种包装,通过 WebRequest 对象来 getAttribute(); 获取之前保存在请求域中的信息,但是这里还要传入一个参数,也就是scope,表示范围。 0代表 request,1代表session。我们传入0即可。

// 给容器中加入我们自定义的 ErrorAttribute
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
    @Override
    // 这里返回的map就是页面和json能够获取的所有数据信息
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        // 先调用父方法,获取之前原有的数据
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        // 在之前原有的数据上进行添加
        map.put("tel", "12345678910");
        // 这就是我们的异常处理器携带的数据
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        // 再放入整个要返回的map中
        map.put("ext", ext);
        return map;
    }
}

在这里插入图片描述

在这里插入图片描述

可以看到,不管是浏览器还是其他客户端,都能返回我们自定义的数据了。

总结

了解SpringBoot的异常处理流程,通过定制 ErrorAttribute 来自定义异常信息,这样我们就能随心所以的来定义页面要显示的信息了。

posted @ 2022-08-08 14:16  随遇而安==  阅读(138)  评论(0编辑  收藏  举报