spring boot默认异常处理机制原理 实现自适应、自定义的异常处理

不想听原理可以直接看总结

原理部分

spring boot默认处理

  1. 浏览器,返回一个默认的错误页面
    在这里插入图片描述
  2. 如果是其他客户端,默认响应一个json数据
    在这里插入图片描述

源码分析

在spring boot中,关于错误的自动配置类是 ErrorMvcAutoConfiguration
这个自动配置类, 给容器中加入了这几个关键组件:

  1. ErrorPageCustomizer
    这个类的作用是定制错误的响应规则。在它的的registerErrorPages方法里的getPath方法指定了处理错误默认发送的请求为/error请求
  2. BasicErrorController
    这个类就是来处理/error请求的,它有如下两个方法:
@RequestMapping(produces = "text/html") // 指定响应HTML数据
	public ModelAndView errorHtml(HttpServletRequest request,
			HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	@ResponseBody // 指定响应json数据
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<Map<String, Object>>(body, status);
	}

spring boot 默认处理/error请求时,会根据响应头来判断发送请求的是浏览器还是app,然后响应HTML或者json数据。
浏览器这部分代码处理完请求之后返回了一个ModelAndView 指定响应的页面,这个页面会去resolveErrorView这个方法里面找。

protected ModelAndView resolveErrorView(HttpServletRequest request,
			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}

这个方法遍历所有的ErrorViewResolver,从这里面找返回的页面。在没有指定的情况下,默认来到了第三个注入的组件

  1. DefaultErrorViewResolver
    这个组件的作用是指定浏览器响应的错误页面。
    我们来看他是如何指定的:
	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
			// 将状态码转换为视图名称
		ModelAndView modelAndView = resolve(String.valueOf(status), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

可以看出,默认是将响应的状态码指定为视图名称,也就是说我们如果想自定义浏览器的错误响应页面,只需要在静态资源文件夹下创建4xx, 5xx等页面,当发生对应状态码异常时就会来到对应页面。

实战部分

定制浏览器错误页面

  • 没有模板引擎的情况下
    将错误页面命名为 错误状态码.html 放在静态资源文件夹里面的 error文件夹下,发生此状态码的错误就会来到对应的页面。
    ps:如果建立名为4xx.html的文件,当发生以4开头的状态码错误时,在没有被精确匹配的情况下,就会来到这个页面。
  • 有模板引擎的情况下
    当然要把错误状态码.html文件放在模板引擎文件夹下。
    有模板引擎的优势在于:你可在错误页面获取一些讯息:
    • status:状态码
    • timestamp:时间戳
    • error:错误提示
    • exception:异常对象
    • message:异常消息
    • errors:JSR303数据校验的错误都在这里
  • 如果以上两个地方都没找到,就会来到默认一开始的页面。

定制json错误消息

假设我们现在有一个UserNotExistException

public class UserNotExistException extends RuntimeException {

    private String id;

    public String getId() {
        return id;
    }

    public UserNotExistException(String id) {
        super("用户不存在");
        this.id = id;
    }
}

我们可以利用springMvc的ControllerAdvice来定制这个异常的处理

@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {

    @ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
    @ResponseBody // 已json的形式返回
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 指定响应的错误状态码为404,不指定默认是200
    public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", ex.getId());
        result.put("message", ex.getMessage());
        return result;
    }
}

这么写,我们app端访问该异常时能够正确返回我们定制的json数据。
在这里插入图片描述
但是存在一个问题:浏览器出现该异常时返回的也是json数据,而不是我们定制的404.html页面,失去了spring boot默认的自适应效果。
在这里插入图片描述

如何达到自适应效果

我们知道spring boot在处理/error请求时,默认就是自适应的。
所以我们想要得到自适应效果,只需要将请求转发到/error就好了。

@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {

    @ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
    @ResponseStatus(HttpStatus.NOT_FOUND) // 指定响应的错误状态码,不指定默认是200
    public String handleUserNotExistException(UserNotExistException ex) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", ex.getId());
        result.put("message", ex.getMessage());
        return "forward:/error";
    }
}

这样确实能达到自适应效果,但又出现新的问题

  1. 设置响应状态码的注解不起作用,还是默认的200。导致无法定位我们为浏览器设置的404.html界面。
  2. 我们自定义数据不起作用
如何传递状态码信息给/error请求

我们再回头看一下BasicErrorController里的方法,看它是如何获取状态码的。然后我们就发现了HttpStatus status = getStatus(request);方法。点进去一看:

	protected HttpStatus getStatus(HttpServletRequest request) {
		Integer statusCode = (Integer) request
				.getAttribute("javax.servlet.error.status_code");
				...
	}

原来它是从request域中按照这个javax.servlet.error.status_code属性获取的。
所以我们把它的参数HttpServletRequest也拿到我们的ExceptionHandler方法中。
然后设置request域中属性的值

    @ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
    public String handleUserNotExistException(UserNotExistException ex, HttpServletRequest request) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", ex.getId());
        result.put("message", ex.getMessage());
        request.setAttribute("javax.servlet.error.status_code", 404);
        return "forward:/error";
    }

成功传入状态码。但是没有自定义数据。

最终版:自适应、自定义的异常处理

我们已经完成自适应了,接下来再说自定义。
再回头看一看BasicErrorController的源码。看它是怎么获取响应数据的。
然后我们发现,不管是浏览器返回的model还是app返回的map。数据都是由getErrorAttributes这个方法获取的。点进来。

	protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
			boolean includeStackTrace) {
		RequestAttributes requestAttributes = new ServletRequestAttributes(request);
		return this.errorAttributes.getErrorAttributes(requestAttributes,
				includeStackTrace);
	}

看一下返回的这个this.errorAttributes:

public abstract class AbstractErrorController implements ErrorController {

	private final ErrorAttributes errorAttributes;

	...

	public AbstractErrorController(ErrorAttributes errorAttributes) {
		this(errorAttributes, null);
	}
	...
}

我们发现这个ErrorAttributes是从容器中获取的。
而ErrorAttributes在我们一开始说的ErrorMvcAutoConfiguration这个自动配置类中第一个注入的就是DefaultErrorAttributes!

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

我们可以看到在它的getErrorAttributes方法中,设置了我们可以在模板引擎中获取的数据。

	@Override
	public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, requestAttributes);
		addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
		addPath(errorAttributes, requestAttributes);
		return errorAttributes;
	}

也就是说,如果我们要自定义消息,只需要重写这个getErrorAttributes方法就好了。
先在ControllerExceptionHandler的handleUserNotExistException方法中,在转发请求之前添加一行代码:request.setAttribute("extResult", result);
将我们自定义的数据一并转发过去。

然后继承DefaultErrorAttributes重写getErrorAttributes方法。

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        // 父类返回的map
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        // 自定义的添加数据
        map.put("author", "joker");
        // 从请求域中获得我们自己添加的数据
        Map<String, Object> extResult = (Map<String, Object>) requestAttributes.getAttribute("extResult", 0);
        map.put("extResult", extResult);
        return map;
    }
}

总结:如何实现自适应、自定义的异常处理

  1. 编写一个异常类
public class UserNotExistException extends RuntimeException {

    private String id;

    public String getId() {
        return id;
    }

    public UserNotExistException(String id) {
        super("用户不存在");
        this.id = id;
    }
}
  1. 写一个ControllerAdvice指定一个ExceptionHandler方法处理该异常
@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {

    @ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
    public String handleUserNotExistException(UserNotExistException ex, HttpServletRequest request) {
        // 自定义消息
        Map<String, Object> result = new HashMap<>();
        result.put("id", ex.getId());
        result.put("message", ex.getMessage());
        // 设置错误状态码
        request.setAttribute("javax.servlet.error.status_code", 404);
        // 将自定义消息放入转发域
        request.setAttribute("extResult", result);
        // 转发到/error请求,使其获得自适应效果
        return "forward:/error";
    }
}
  1. 编写一个类继承DefaultErrorAttributes重写getErrorAttributes方法。
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        // 父类返回的map
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        // 自定义的添加数据
        map.put("author", "joker");
        Map<String, Object> extResult = (Map<String, Object>) requestAttributes.getAttribute("extResult", 0);
        map.put("extResult", extResult);
        return map;
    }
}
  1. 在模板引擎文件夹或者静态资源文件夹中加入 错误状态码.html
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值