不同的客户端,响应的结果不同
SpringBoot
根据 请求头的 Accept
来选择响应
Accept:指定客户端能够接收的内容类型。
可以看到,
只要发生 4XX 或者 5XX 类型的错误,SpringBoot都会返回一个状态码以及一个错误页面;
不同的Accept
,得到的响应结果不同。这其实都和Springboot 处理错误的机制有关。
SpringBoot 自动装配了处理所有错误的配置类
ErrorMvcAutoConfiguration
(1) ErrorPageCustomize
注册错误页面对象 ErrorPager
:把默认的/error
路径下对应的错误页面注入到Servlet
中,在请求访问异常的时候,会转发到 /error
路径下
(2) 发生 4xx
或者 5xx
错误
(3) 转发到/error
请求,被BasicErrorController
处理
(4)BasicErrorController
利用
SpringBoot 启动,SpringBoot 利用 ErrorMvcAutoConfiguration
往 IOC 容器中 注入组件,SpringBoot 的默认错误处理机制,依赖于这些组件。
ErrorPageCustomizer
:用于在 Servlet
中 注册错误页面对象 ErrorPage
BasicErrorController
: 统一异常处理 Controller,处理默认/error
请求DefaultErrorViewResolver
: 默认的错误视图解析器,解析要跳转到哪个错误页面、DefaultErrorAttributes
: 一个专门收集 error 发生时错误信息的bean,把错误信息注入到错误页面中ErrorPageCustomizer
是ErrorMvcAutoConfiguration
的一个静态内部类;
通过调用registerErrorPages
方法的getPath()
方法最终得到错误页面路径;
errorPageRegistry.addErrorPages(errorPage)
把/error
路径下对应的错误页面注入到Servlet
中,在请求访问异常的时候,会转发到 /error
路径下。
//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer
/**
* 注入 ErrorPageCustomizer 组件
*/
@Bean
public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
/**
* ErrorPageRegistrar 接口 用于定义在发生异常时该有哪些错误页面被添加
*
*/
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 根据错误页面的路径,实例化错误页面对象
// getPath()得到错误页面路径,如果没有自定义error.path属性,则去/error位置
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
// 添加处理异常时将使用的错误页对象
errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
}
public int getOrder() {
return 0;
}
}
// getPath() 来获取需要解析的错误页面路径
public class ErrorProperties {
@Value("${error.path:/error}")
private String path = "/error";
public String getPath() {
return this.path;
}
}
SpringBoot 统一的异常处理Controller
用于请求返回的 Controller类,根据HTTP请求可接受的格式(Accept)不同返回对应的信息,所以在使用浏览器和接口测试工具测试时返回结果存在差异。
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Controller
// 定义请求路径,如果没有配置server.error.path,默认为 /error
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// 返回 错误页面
// 如果 客户端可以接收的响应格式为 text/html
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 获取 错误状态码
HttpStatus status = this.getStatus(request);
// 获取 要返回的信息
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
// 设置 响应状态码
response.setStatus(status.value());
// 去哪个错误界面
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
// 返回视图,如果 自定义的页面模板不存在,则使用默认的 错误视图模板
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
// 返回 json 数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 获取 错误状态码
HttpStatus status = this.getStatus(request);
// 如果 是 204 ,就是表示服务器已成功完成请求,并且在响应正文中没有要发送的内容
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
// 如果是接受所有格式的HTTP请求 */*
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
// 响应 ResponseEntity extends HttpEntity
return new ResponseEntity(body, status);
}
}
}
可以看到这两个方法中都有一段共同的代码HttpStatus status = getStatus(request);
,它的作用就是为了给解析视图提供状态码,通过下面的源码request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)
可以看出来,SpringBoot首先是通过request域对象中获取状态码,但是这里有一个参数RequestDispatcher.ERROR_STATUS_COD
E,这个参数的具体值就是javax.servlet.error.status_code
所以我们在自定义异常处理器时,需要手动指定状态码,否则不生效
request.setAttribute(“javax.servlet.error.status_code”,5000);
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
public interface RequestDispatcher {
public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
}
一个专门收集 error 发生时错误信息的bean,把错误信息注入到错误页面中
类DefaultErrorAttributes
中的 getErrorAttributes
方法提供了默认的错误信息注入,可以获取到:
getErrorAttributes()方法获取timestamp:时间戳
addStatus()方法获取status:状态码
addErrorDetails()方法获取exception:异常对象
addStackTrace()方法获取trace:堆栈信息(能力尚浅,仅是猜测)
addExceptionErrorMessage()方法获取message:异常错误信息
addBindingResultErrorMessage()方法获取errors:JSR303校验信息
addPath()获取请求出错的路径path:错误路径
// org.springframework.boot.web.servlet.error.DefaultErrorAttributes
/** @deprecated */
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
// timestamp
errorAttributes.put("timestamp", new Date());
// status
this.addStatus(errorAttributes, webRequest);
// errorMessage
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
// path
this.addPath(errorAttributes, webRequest);
//
return errorAttributes;
}
如果Accept:text/html
请求,这时候就会进入到DefaultErrorViewResolver
的resolveErrorView
方法
分两种情况:
以上两种情况又各分为两种情况:
第一种:
可以通过模板引擎解析的页面的精确匹配,首先这个页面是需要放在 /template/error/ 路径下的,当请求进入后,模板引擎会根据状态码解析错误页面,精确匹配到路径下的页面,功能实现就是这句
this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext)
第二种:
模板引擎无法解析的页面和静态资源文件夹下也没有页面的情况,这时候
this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext)
会返回一个null,从而进入到 resolveResource
方法,通过遍历静态资源文件位置获取匹配的页面,如果没有页面,则会返回系统默认的视图对象。
通过上面遍历寻找页面可以得出一个结论:精确优先,寻找位置的优先级(从高到底):
META-INFO/resources/error/
项目路径下/resources/error/
项目路径下/static/error/
项目路径下/public/error/
最后没有找到对应的页面,SpringBoot则会使用默认的页面展示,也就是文章开头那个图
// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 调用resolve 方法,使用 HTTP完整状态码 检查是否有页面可以匹配
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 使用 HTTP 状态码 第一位匹配初始化中的参数创建视图对象
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
// resolve
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 拼接错误视图路径 /eroor/[viewname]
String errorViewName = "error/" + viewName;
// 使用模版引擎尝试创建视图对象
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
// 模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/[viewname]
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
// resolveResource
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
// 遍历静态资源文件夹,检查是否有存在视图
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
return null;
}
由上面的分析可知,当系统出现错误时,Spring Boot默认是去类路径下的/error
请求。对应的请求为error/状态码
,如果将错误页面命名为 错误状态码.html
放在模板引擎文件夹里面的/error
文件夹下,发生此状态码的错误就会来到对应的页面。
当需要定义错误页面时,可以使用4xx
和5xx
作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html)。
如果模板引擎具体的 /error
下有就是用对应的页面;
如果没有,则去项目的静态资源文件夹下找;
如果前面的路径下都没有,使用Spring Boot默认的错误提示页面。
其中从错误页面可以获取到的默认信息有:
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
此处理器不在请求到 /error ,不论客户端是啥,直接都统一返回json数据
自定义的UserNotExistException异常
// UserNotExistException
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {}
public UserNotExistException(String message) {
super(message);
}
}
全局异常处理器
// 全局异常处理器
@ControllerAdvice
public class SelfExceptionHandler {
/**
* 专门捕获 自定义的 UserNotExistException 异常,进行处理
*
* 这里捕获了异常,不论客户端是啥,是直接都统一返回json数据
*
* @param e Exception
* @return Map
*/
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>(4);
map.put("message",e.getMessage());
map.put("exception",e.getClass());
map.put("code",ResponseCode.FAILURE.getCode);
return map;
}
}
// 全局异常处理器
@ControllerAdvice
public class SelfExceptionHandler {
/**
*
* 利用 SpringBoot 的 BasicErrorController,
* 对 /error 请求请求进行自适应处理(text/html或者json)
*
* @param e Exception
* @param request HttpServletRequest
* @return 转发到 /error 请求
*/
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>(4);
// 先通过 Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code") 从request获取状态码
// HttpStatus status = this.getStatus(request) 获取状态码
// 设置 响应的状态码
request.setAttribute("javax.servlet.error.status_code", 5000);
//
map.put("message",e.getMessage());
map.put("exception",e.getClass());
map.put("code",ResponseCode.FAILURE.getCode());
// 需要响应的数据,添加到request 域,
// 提供给 SelfErrorAttribute 类 的 getErrorAttributes 方法获取
request.setAttribute("ext",map);
return "forward:/error";
}
}
将定制数据携带出去:出现错误以后,会来到/error
请求,会被BasicErrorController
处理,响应出去可以获取的数据是由getErrorAttributes()
得到。getErrorAttributes()
是AbstractErrorController
规定的方法,AbstractErrorController
是ErrorController
接口的实现类。
为了使用定制的错误响应,要进行如下步骤:
编写一个ErrorController
的实现类【或者是编写AbstractErrorController
的子类】,放在容器中
页面上能用的数据,或者是json返回能用的数据都是通过
errorAttributes.getErrorAttributes
得到。容器中DefaultErrorAttributes.getErrorAttributes()
;默认进行数据处理的;同样也可以自定义errorAttributes,通过getErrorAttributes()
同样可以获取到。
给容器中加入我们自己定义的ErrorAttributes
// 给容器中加入我们自己定义的ErrorAttributes
/**
* 自定义 ErrorAttribute
* 实现 DefaultErrorAttributes,用于添加或者改变 响应的errorAttributes
*/
@Component
public class SelfErrorAttributes extends DefaultErrorAttributes {
// 返回值的map就是页面和json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
// 添加响应参数
errorAttributes.put("param1", "param1");
// 从request区域中,获取 SelfExceptionHandler 类,传递过来的参数
// 我们的异常处理器携带的数据
// 0 就是用来确定request域对象的,这样我们就可以从request域对象中获取之前自定义的异常信息了
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
// 如果是 UserNotExistException 异常
if (ext != null) {
errorAttributes.put("ext", ext);
}
return errorAttributes;
}
}
WebRequest implement RequestAttributes
public interface RequestAttributes {
int SCOPE_REQUEST = 0;
int SCOPE_SESSION = 1;
}