springboot对于404,500等错误处理非常人性化,如果你是用浏览器访问的就返回一个页面,如果是客户端就会返回json数据,如下
{ "timestamp": "2020-03-13T08:24:41.493+0000", "status": 404, "error": "Not Found", "message": "No message available", "path": "/crud/aaa" }
下面我们来简单的研究下原理,先看下错误处理器的自动配置类
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // 可以看出这个配置类是在webmvc自动配置之前加载的,这里主要加载了下面两个组件 @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { //将放回的错误信息封装进入默认的错误属性中 @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } //处理错误信息的controller @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())); }
//默认的错误页面定制器
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
//默认错误映射处理器
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}
}
而这个DefaultErrorAttributes类中,这个五个属性的添加正好和我们接收的json数据一一对应,错误信息就是封装到这里边的
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap(); errorAttributes.put("timestamp", new Date());//timestamp this.addStatus(errorAttributes, webRequest);//status this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);//error,message this.addPath(errorAttributes, webRequest);//path return errorAttributes; } }
我们再来看下BasicErrorController这个基础错误处理器
//处理所有的/error请求 @Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { //如果请求头接受方式为text/html,就是浏览器发送的,返回modelAndView就是默认的错误页面 @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, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = 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 = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<Map<String, Object>>(status); } Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } }
那么每当我们的请求发生错误或者异常时,是如何重定向到/error呢?DefaultErrorViewResolver 中
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); } private ModelAndView resolve(String viewName, Map<String, Object> model) { //默认SpringBoot可以去找到一个页面? error/404 String errorViewName = "error/" + viewName; //模板引擎可以解析这个页面地址就用模板引擎解析 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { //模板引擎可用的情况下返回到errorViewName指定的视图地址 return new ModelAndView(errorViewName, model); } //模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html return resolveResource(errorViewName, model); } }
没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;都没有返回默认错误页面
既然知道了错误处理机制,实际开发中我们可以自己定制error返回自定义信息
首先自定义异常处理器
@ControllerAdvice public class MyExceptionHandler { //处理自定义的异常 @ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ //传入我们自己的错误状态码 4xx 5xx request.setAttribute("javax.servlet.error.status_code",599); Map<String,Object> map = new HashMap<>(); map.put("code","user.notexist"); map.put("message","用户出错啦"); request.setAttribute("ext",map); //转发到/error return "forward:/error"; } }
但是此时DefaultErrorAttributes中只会封装默认的那五个值,所以我们需要自定义一个errorAttributes
@Component public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace); //我们的异常处理器携带的数据 Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0); map.put("ext",ext); return map; } }
至此就完成了自定义并且自适应的错误处理