错误处理机制提及来是每一个网站架构开发的核心部分,不少时候咱们并无去关注他们,其实错误在咱们平常访问过程当中时长出现,对错误机制进行了解也是开发一个好的网站所必备的技能之一。html
spring boot默认会根据不一样的请求客户端,返回不一样的结果: 一、状况一:返回一个默认的错误页面java
当咱们使用web访问出错的时候,会跳到这样的错误页面,其信息以下所示:web
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon Dec 17 14:50:33 CST 2018 There was an unexpected error (type=Bad Request, status=400). Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "aaa"
二、 状况二:返回json信息spring
当咱们使用其余的客户端,例如postman模拟请求的时候,返回的信息则是json数据格式:json
{ "timestamp": "2018-12-17T06:59:00.851+0000", "status": 404, "error": "Not Found", "message": "No message available", "path": "/somepage" }
这里要模拟一个
页面不存在的错误
错误,最好先把登陆过滤器关掉,不然请求任何不存在的页面都会给你过滤到登陆界面,不会出现错误信息。浏览器
咱们开发网站过程当中,显然不会使用这些默认方式,而是要本身去定制反馈结果的。但咱们首先仍是先去了解SpringBoot的默认错误处理过程,了解一下原理。springboot
参考自动配置类ErrorMvcAutoConfiguration
。咱们看看该自动配置类为容器中添加了以下组件:架构
查看BasicErrorController源码app
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { @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.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 去哪一个页面做为错误页面:包含页面的地址和内容 ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = this.getStatus(request); return new ResponseEntity(body, status); } }
能够知道,该组件用于默认处理/error
请求,其中须要留意:ide
error.xml
里配置的错误页面规则。一旦系统出现4XX或者5XX之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),使请求来到/error
。这时候,BasicErrorController控制器会处理这个请求。
观看上述代码咱们能够发现,之因此出现两种错误结果,无非就是对error进行处理的controller会根据不一样的请求头:
Accept: text/html
Accept:"*/*"
这二者进行不一样的反馈,前者返回错误页面信息,后者返回json数据。咱们来看看错误响应页面的视图解析器:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { Iterator var5 = this.errorViewResolvers.iterator(); ModelAndView modelAndView; do { if (!var5.hasNext()) { return null; } ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); modelAndView = resolver.resolveErrorView(request, status, model); } while(modelAndView == null); return modelAndView; }
这段代码拿到了异常视图解析器(ErrorViewResolvers
)类型来进行处理,当前咱们注册的是DefaultErrorViewResolver
,查看源码:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { //status.series() 状态码 modelAndView = this.resolve((String)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); return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); }
默认spring boot会去找到某个页面:error/状态码.html。
跳转到模板页面:error/状态码,也就是说,咱们若是想自定义错误页面的话,将错误页面命名为状态码.html,并放在模板文件夹(templates)的error文件夹下,发生此状态码的错误就会来到对应的页面;查看源码咱们也能够发现,命名为4xx.html(5xx.html)则能够处理全部以4(5)开头的错误码错误,即均可以跳到该页面;不过spring boot会优先寻找直接对应的错误页面,若是404错误会优先选取404.html做为错误页面;
错误页面能获取到的信息(DefaultErrorAttributes
):
即咱们能够在错误页面里获取到错误信息,示例以下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>This is 4xx error page:</h1> <p>*timestamp: [[${timestamp}]]</p> <p>*status: [[${status}]]</p> </body> </html>
即咱们没有对应的error错误文件夹,也能够放在静态资源文件夹下。
例如,放在static文件夹下,一样能够来到该页面,只不过不能被模板引擎渲染而已。
这种状况会来到SpringBoot默认的错误提示页面,该视图对象的信息咱们能够经过查看源码获悉,其位置位于ErrorMvcAutoConfiguration
中:
@Configuration @ConditionalOnProperty( prefix = "server.error.whitelabel", name = {"enabled"}, matchIfMissing = true ) @Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class}) protected static class WhitelabelErrorViewConfiguration { private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView(); protected WhitelabelErrorViewConfiguration() { } @Bean( name = {"error"} ) @ConditionalOnMissingBean( name = {"error"} ) public View defaultErrorView() { return this.defaultErrorView; } @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637); return resolver; } }
spring boot会根据请求头给予不一样的返回类型数据。上一节讲到的是定义错误页面,还差一种方式:即其余客户端访问状况下返回json数据的问题,这一节,来处理这个问题。即如何定制错误的json数据。
咱们先自定义一种异常,例如用户不存在的异常:
package com.zhaoyi.springboot.restweb.exception; public class UserNotExistException extends RuntimeException{ public UserNotExistException(){ super("用户不存在"); } }
而后在应用程序的某个地方抛出该异常:
@RequestMapping({"user"}) public String index(@RequestParam("user") String user){ if(user.equals("aaa")){ throw new UserNotExistException(); } return "index"; }
经过访问
/user?user=aaa
触发该异常。
显然,若是咱们不作任何处理,SpringBoot会默认将错误处理到咱们以前配置过的页面,运行时错误对应的是500,即会跳转到咱们的5xx页面:
this is 5xx error page: status ------ 500 message ------ 用户不存在
那么,咱们该如何将此错误自定义呢,能够运用springMVC的知识,在controller下面定义个异常处理器:
package com.zhaoyi.springboot.restweb.controller; import com.zhaoyi.springboot.restweb.exception.UserNotExistException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExcetionHandler { @ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String,Object> handlerException(Exception e){ Map<String, Object> map = new HashMap<>(); map.put("myCode", "custom code"); map.put("message", e.getMessage()); return map; } }
咱们在此访问一样的触发异常的地址,就能够如愿的获得本身想要的自定义错误信息了:
{"myCode":"custom code","message":"用户不存在"}
但这种方式有点问题,没有自适应效果,也就是咱们用浏览器也好,其余的客户端也好,返回的都是这段json数据。那么,咱们如何想springboot那样作到异常返回的自适应呢?(浏览器返回错误页面,其余客户端返回json数据)。很简单,转发到/error
,交由SpringBoot处理便可。
注意注释掉以前的处理代码。
这时候,咱们若是换用不一样的客户端访问就会获得相应的反馈了,好比用浏览器能够获得以下的返回数据:
<html> <body> <h1>Whitelabel Error Page</h1> <p>This application has no explicit mapping for /error, so you are seeing this as a fallback. </p> <div id='created'>Mon Dec 17 16:15:30 CST 2018</div> <div>There was an unexpected error (type=OK, status=200).</div><div>?????</div></body></html>
可是新的问题又出现了,即:咱们自定义的页面并无获得解析,springboot仍是默认使用了以前分析过的,什么都没有定义的时候跳转到空白错误页面的状况。
仔细观察咱们会发现,实际上是错误状态码有问题,这里是错误码已经由500变为了200.问题出在哪里呢?出在咱们在转发的时候,没有设置一个错误状态码:所以,咱们还须要在转发以前设置状态码。如何设置,先来查看springboot相关处理错误信息的源码
@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.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); }
继续定位源码this.getStatus(request);
:
protected HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } else { try { return HttpStatus.valueOf(statusCode); } catch (Exception var4) { return HttpStatus.INTERNAL_SERVER_ERROR; } } }
所以,咱们只须要在request
域中添加一个javax.servlet.error.status_code
属性,就能够以最优先的级别状况设置状态码了。因此,改造后的代码应该以下所示:
@ExceptionHandler(UserNotExistException.class) public String handlerException(Exception e, HttpServletRequest request){ Map<String, Object> map = new HashMap<>(); request.setAttribute("javax.servlet.error.status_code", 500); map.put("myCode", "custom code"); map.put("message", e.getMessage()); return "forward:/error"; }
这时候在运行发现能够调到咱们自定义的5xx.html错误页面了。显示以下:
this is 5xx error page: status ------ 500 message ------ 用户不存在 myCode ------
问题仍是有,咱们会发现,咱们自定义的数据不见了(myCode
),错误页面只能获取到SpringBoot默认写入的信息。所以咱们还得继续探索,如何才能既能调到自定义错误页面,又能携带咱们自定义的错误数据。
咱们知道,出现错误之后,会相应到/error请求,同时交由BasicErrorController进行处理,他在处理错误的时候进行了自适应处理,响应回来并能够获取的数据是getErrorAttributes(是BasicErrorController的父类AbstractErrorController中定义的)
获得的。
咱们则彻底能够编写一个ErrorController的实现类(或者继承BasicErrorController),放在容器中。想一想有点麻烦,固然,还有选择。
第二种办法,注意第一种方法的某句话errorAttributes.getErrorAttributes....
,页面上能用的数据,或者是json返回能用的数据都是经过他来获得的。查看ErrorAttribute的来源:
@Bean @ConditionalOnMissingBean( value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT ) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); }
容器中的DefaultErrorAttributes来进行数据处理的,因此,咱们本身配置一个实现了ErrorAttributes类型接口这样的Bean就能够了,可是为了方便,咱们最好继承spring boot默认使用的DefaultErrorAttributes
来实现便可。
自定义ErrorAttribute,改变默认行为
package com.zhaoyi.springboot.restweb.component; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.web.context.request.WebRequest; import java.util.Map; @Componet public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); // 这里随便写一个本身的 errorAttributes.put("someCode", "attribute add atrribute"); // 从request请求域中获取ext的值 errorAttributes.put("ext", webRequest.getAttribute("ext", WebRequest.SCOPE_REQUEST)); return errorAttributes; } }
注意:这是一个组件,不要忘记添加
@Componet
注解,否则没法加入到容器中。
在这里咱们使用request域传递信息,而且经过WebRequest.getAttribute("param", SCOPE)
获取其信息,WebRequest.SCOPE_REQUEST
的取值对应什么,点进WebRequest代码内容就能够看到对应信息了。这样,咱们还须要修改异常处理器的代码,以下所示:
@ExceptionHandler(UserNotExistException.class) public String handlerException(Exception e, HttpServletRequest request){ Map<String, Object> map = new HashMap<>(); request.setAttribute("javax.servlet.error.status_code", 500); map.put("myCode", "custom code"); map.put("message", e.getMessage()); request.setAttribute("ext", map); return "forward:/error"; }
使用postman访问异常页面,返回结果以下:
{ "timestamp": "2018-12-17T08:54:29.906+0000", "status": 500, "error": "Internal Server Error", "message": "用户不存在", "path": "/user", "someCode": "attribute add atrribute", "ext": { "myCode": "custom code", "message": "用户不存在" } }
错误先关的知识就到这里为止了,咱们还得继续往下探索,下面的内容会愈来愈有意思,他是什么呢?
—— 嵌入式Servlet容器配置修改。