BasicErrorController:处理默认/error请求
@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 它其实就是一个用来处理/error请求的控制器
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
那么我们的异常是如何发送请求的呢?这又是个什么操作?
- 发出一个请求,DispatcherServlet
- 然后后面交给了请求处理方法
- 在处理方法的时候出现了异常,那么就转发请求给/error交给DispatcherServlet处理
- DispatcherServlet 找到对应的handler来处理/error这个请求
- 最后handler交给我们的统一异常处理的控制器:BasicErrorController来进行处理
那么BasicErrorController又是怎么处理/error这个请求的呢?
我们可以先看看里面接受请求的方法:
@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);
}
那么errorHTML是如何定制我们的响应页面的呢?
resolveErrorView方法解析视图:
sprotected 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;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
啊!到这一步,我们的 BasicErrorController 源码 的 errorHtml 方法就刨析完毕了!
通过以上源码的刨析也已经能够知道如何去定制自己的异常页面了,包括其中的原理等等!
王二麻子我爱你:什么?你说你还不会自己定制异常页面?那。。。。要不先自己在看看源码摸索一哈子?
王二麻子:死亡凝视——(?_?)——竿绫量逻辑——(╯‵□′)╯︵┻━┻
王二麻子我爱你:好好好,那就手把手带着操作一边就是了——(~o ̄3 ̄)~
王二麻子我爱你:那接下来来手操一边定制错误页面——(?゚ヮ゚)?
? 如果有模板引擎的情况下,可以通过error/状态码的形式来进行控制,也就是说,我们可以将错误页面命名为“错误状态码.html”,然后放在模板引擎文件夹(即templates目录下)里面的error文件夹下,没有error文件夹就创建一个,发生此状态码的错误就会来到 对应的页面,因为SpringBoot规则中已经默认规定好了。
? 更准确的将我们可以使用4xx和5xx作为错误页面的文件名,进而来匹配该种类型的所有错误,匹配的时候遵循精确优先(优先寻找精确的状态码.html),像下面这样
遍历优先级,例如,404:
<templates>/error/404.html<ext>
<static>/error/404.html
<templates>/error/4xx.html<ext>
<static>/error/4xx.html
我们在默认的错误页面中可以获得如下信息:
- timestamp:时间戳
- tstatus:状态码
- terror:错误提示
- texception:异常对象
- tmessage:异常消息
- terrors:JSR303数据校验的错误都在这里
这里要说明一下的是,如果我们项目中没有使用模板引擎(或者模板引擎找不到这个错误页面),就会去静态资源文件夹下找。如果静态资源文件夹中也没有错误页面,就是默认来到SpringBoot默认的错误提示页面。
接下来的测试就不浪费时间了,自己启动springboot发个错误请求玩玩
王二麻子:我想给这个玩意配个日志——(づ ̄ 3 ̄)づ
王二麻子我爱你:啊这,要不咱先总结一下上面剖析源码的内容吧是——( ̄﹃ ̄)
王二麻子:也行,先消化消化上面的内容是——(??? )
王二麻子我爱你:关于配日志这块是属于最后的一部分知识点——定制异常处理——o((>ω< ))o
**总结:**从errorHTML方法可以得出结论,我们需要使用自定义的页面响应错误只需要在对应的路径上创建对应的错误代码的页面就行了,但是如果想要记录日志就需要自己定制了,定制这一块待会再说,先不急
接下来来聊聊 BasicErrorController 的另外一个接受请求的方法:error( )
这里先补充一点前面有提到过的知识,error方法的作用:当 /error 这个请求所希望响应的参数非text/html 都会由 error() 这个方法来处理。这里我们主要了解一下它是如何返回json数据的,然后找到规律,最后自己定制响应的json数据
@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);
}
简单了解完了getErrorAttributes这个方法后,现在深入其中探寻其中的奥妙:
首先看一下 getErrorAttributeOptions.class 这个类
它可以动态控制返回的异常信息
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
return options;
}
通过这个就可以使用配置类动态控制异常返回的属性
王二麻子:有个问题,那么我该如何定制自己的 异常json响应信息呢?我不希望返回Response这个对象,我希望返回我自己定义的对象,封装我想要返回的信息
王二麻子我爱你:这个问题问的好,接下来我们将要覆盖原本的这个BasicErrorController 的自动配置,自己注册一个ErrorController.class类型的bean来定制异常处理——(?ω?`)o
接下来就到重点了!定制自己的统一异常处理
@Controller
@RequestMapping("/error")
public class CustomErrorController extends AbstractErrorController {
public CustomErrorController(ErrorAttributes errorAttributes,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
}
@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, true));
System.out.println("========"+model);
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
@ResponseBody
public Msg error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new Msg(204,"NO_CONTENT");
}
Map<String, Object> body = getErrorAttributes(request, true);
System.out.println("========"+body);
return new Msg((Integer)body.get("status"),body.get("message").toString());
}
@Override
public String getErrorPath() {
return null;
}
}
然后发送请求:
成功返回自定义的对象,message怎么没有东西呢?
我们来看看这句代码里面的数据:
Map<String, Object> body = getErrorAttributes(request, true);
System.out.println("========"+body);
可以看到,我们这里底层给我们返回的异常信息message也是为空——§( ̄▽ ̄)§
为什么没有message信息呢?
因为我们传入了true,这个true只包含了cludeStackTrace这个信息
Map<String, Object> body = getErrorAttributes(request, true);
ErrorAttributeOptions of = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.STACK_TRACE, ErrorAttributeOptions.Include.BINDING_ERRORS, ErrorAttributeOptions.Include.EXCEPTION);
Map<String, Object> body = getErrorAttributes(request, of);
@RequestMapping
@ResponseBody
public Msg error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new Msg(204,"NO_CONTENT");
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions());
System.out.println("========"+body);
return new Msg((Integer)body.get("status"),body.get("message").toString());
}
protected ErrorAttributeOptions getErrorAttributeOptions() {
ErrorAttributeOptions of = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.STACK_TRACE, ErrorAttributeOptions.Include.BINDING_ERRORS, ErrorAttributeOptions.Include.EXCEPTION);
return of;
}
启动测试:
成功返回完整异常信息
王二麻子:还有记录异常日志呢?—— (¬︿??¬☆)
王二麻子我爱你:差点忘记了,到这一步那就简单了,定义一下日志对象,然后将我们的异常信息(map)遍历输出到日志就可以了——( ̄▽ ̄ )ゞ*
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping
@ResponseBody
public Msg error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new Msg(204,"NO_CONTENT");
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions());
System.out.println("========"+body);
for (Map.Entry<String, Object> entry : body.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
logger.info(key+":"+value);
}
return new Msg((Integer)body.get("status"),body.get("message").toString());
}
重启测试:
成功输出记录异常日志