一、SpringBoot提供的默认错误处理
1、在浏览器端访问时,出现错误时响应一个错误页面:
2、在其他客户端访问时,响应json数据:
3、错误处理机制的原理,参照错误自动配置类——ErrorMvcAutoConfiguration,在错误自动配置类中,配置了以下组件:
①ErrorPageCustomizer:定制错误的响应规则
@Value("${error.path:/error}")
private String path = "/error";
从主配置文件中获取error.path的值,如果没有则默认"/error",即一旦系统出现4xx或5xx之类的错误时就会发送/error请求,或者我们自定义的error.path的值的请求,该请求会有BasicErrorController类处理
②BasicErrorController:处理默认的/error请求,但是该类提供了两种/error请求的处理方式,一种产生"text/html"响应(浏览器端请求),一种产生JSON格式数据
@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 = getStatus(request);
Map 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//处理其他客户端发送的请求
public ResponseEntity
那SpringBoot怎么区分请求是来自浏览器还是其他客户端呢?是根据请求头来判断的,来自浏览器的请求的请求头Accept携带有浏览器请求的信息:
其他客户端中Accept请求头的信息:没有要求优先接收html数据
这两个方法会根据错误的状态码判断去哪个错误页面或者响应什么JSON数据,响应页面的解析:遍历所有的错误视图解析器(ErrorViewResolver),如果得到异常视图则返回,否则返回null,这里的ErrorViewResolver就是下面注册的组件DefaultErrorViewResolver,也就是说去往哪个错误页面是由DefaultErrorViewResolver解析得到的
protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response, HttpStatus status, Map model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
③DefaultErrorViewResolver:4xx为客户端错误,5xx为服务端错误
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
...
static {
Map views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map 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;
}
private ModelAndView resolve(String viewName, Map 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);
}
...
}
从这段代码中可以看出:在解析错误视图的时候会调用resolve方法,将状态码拼接到error/后面作为视图名称返回,也就是说SpringBoot默认会在error文件夹下找状态码对应的错误页面,同时如果模板引擎可用则返回到模板引擎指定的文件夹下的errorViewName视图,否则会执行下面这段代码:从所有的静态资源文件夹下找对应的视图(.html),找到则返回,找不到返回null
private ModelAndView resolveResource(String viewName, Map 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;
}
④DefaultErrorAttributes:帮我们在页面共享信息,视图中会有哪些数据是在该类中封装的
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
出错时,SpringBoot为我们放入的数据有:这些数据我们都可以在页面中获取到
timestamp(时间戳)、status(状态码)、error(错误信息)
status:[[${status}]]
timestamp:[[${timestamp}]]
整体步骤:系统出现4xx或者5xx之类的错误时,ErrorPageCustomizer就会生效从而定制错误的响应规则,就会来到/error
请求,该请求会被BasicErrorController处理,处理的结果由DefaultErrorViewResolver返回
二、如何定制错误处理
1、如何定制错误的页面
①有模板引擎时:在模板引擎静态资源文件夹(templates)下新建一个error文件夹,并提供与错误码对应的错误页面即可(error/状态码.html)
也可以提供一个4xx.html来响应以4开头的状态码页面,此时若有精确匹配的则优先使用精确匹配的:
②没有模板引擎:可以放在任何一个静态资源文件夹下,因为视图解析时会一个个的解析,解析到则返回,解析不到则返回null,注意必须是静态资源文件夹下的error/状态码.html,若所有的静态资源文件夹下都没有error/状态码.html则会响应SpringBoot为我们提供的默认错误页面,也即是下面的代码
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"Whitelabel Error Page
"
+ "This application has no explicit mapping for /error, so you are seeing this as a fallback.
"
+ "${timestamp}"
+ "There was an unexpected error (type=${error}, status=${status})."
+ "${message}");
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
2、如何定制错误的JSON数据
①编写一个自定义异常
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {
super("用户不存在!");
}
}
②编写一个异常处理器
a、异常处理方法使用@ResponseBody
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)//处理的异常
public Map handlerException(Exception e){
Map map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
//map.put("exception",e);
return map;
}
}
异常的处理方法中通过map放置异常信息,这个map中的key和value就是我们能够在出现异常时定制的信息
结果:通过postMan请求和通过浏览器请求都会是如下结果,并没有自适应效果(浏览器访问应该响应页面),这是因为在异常拦截器中使用了@ResponseBody
{
"code":"user.notExist",
"message":"用户不存在!"
}
b、不使用@ResponseBody,而将请求转发至/error,达到自适应效果
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//处理的异常
public String handlerException(Exception e){
Map map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
//map.put("exception",e);
return "forward:/error";
}
}
SpringBoot提供了处理/error请求的Controller,自适应效果会在该Controller中实现,但此时响应错误页面并不是我们自定义的错误页面,而是SpringBoot提供的错误页面,且状态码为200:
这是因为我们在异常处理方法中没有设置状态码,修改一下处理方法:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//处理的异常
public String handlerException(Exception e, HttpServletRequest request){
request.setAttribute("javax.servlet.error.status_code",400);
Map map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
return "forward:/error";
}
}
错误状态码的key值是BasicErrorController在解析错误时需要从请求中获取的:
@RequestMapping
@ResponseBody
public ResponseEntity
此时再在浏览器中访问时:
但是在postMan中请求时会发现并没有将我们自定义的定制信息(code、message等)带回到响应中来,
③如果我们想使用自适应同时又想定制错误信息,有一下两种方式:出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法):
a、在容器中添加ErrorController的实现类或者继承ErrorController的子类AbstractErrorController
b、页面上能获取到的数据,或者是json返回的数据都是通过errorAttributes.getErrorAttributes()得到的,SpringBoot容器中通过DefaultErrorAttributes.getErrorAttributes()进行数据处理,因此我们可以自定义ErrorAttributes,重写该类的getErrorAttributes方法,在从父类获取的map中塞入定制的信息即可:
@Component//给容器中加入自定义的错误属性类ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map map = super.getErrorAttributes(webRequest,includeStackTrace);
map.put("company","bdm");
return map;
}
}
但此时已然只能放置一些公共的定制信息,不能放入一些具体的信息,比如在出现某个异常时的提示信息,此时需要我们改动一下异常处理器和上面的ErrorAttributes,将异常处理器中的map放入请求对象中携带到ErrorAttributes中:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//处理的异常
public String handlerException(Exception e, HttpServletRequest request){
request.setAttribute("javax.servlet.error.status_code",400);
Map map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
return "forward:/error";
}
}
@Component//给容器中加入自定义的错误属性类ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map map = super.getErrorAttributes(webRequest,includeStackTrace);
map.put("company","bdm");
Map ext = (Map)webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
这样我们就可以在页面和响应的json中获取到定制的错误信息了: