在我们使用Spring Boot的过程中肯定有遇到过发生错误的时候,这个时候可能页面上出现的就是Spring Boot默认的错误界面,有的时候我们希望能显示我们自己设置的错误界面,或者携带我们自己的错误响应参数,比如查询用户是否存在的时候,如果用户不存在,控制器可以选择抛出一个用户不存在异常,这个时候我们希望错误响应参数可以携带一个message响应参数表示用户不存在,而不是Spring Boot为我们配置好的那几个参数,这个时候我们应该这么做呢,这就是本文的目的。
如果要配置自己的错误界面,首先我们需要先了解一下Spring Boot是如何处理错误的。Spring Boot在自动配置类ErrorMvcAutoConfiguration中为我们添加了4个错误处理相关的组件,具体可以在IDEA中使用Ctrl+N来查找这个类,他们的作用可以查看下面代码中的注释:
1、DefaultErrorAttributes
//帮我们设置错误响应参数;
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
2、BasicErrorController
//处理/error请求,根据请求头中的信息(Accept参数)来判断优先返回html页面,还是JSON数据
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = "text/html")//产生html类型的数据;浏览器发送的请求来到这个方法处理
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 ? new ModelAndView("error", model) : modelAndView);
}
@RequestMapping
@ResponseBody //产生json数据,其他客户端来到这个方法处理;
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
}
3、ErrorPageCustomizer
/**
配置错误发生后,到哪去。这里是系统出现错误以后来到error请求进行处理;
也就是到上面的BasicErrorController处理器,(相当于以前web.xml注册的错误页面规则)。
可以看到这个参数是可配置的。
**/
@Value("${error.path:/error}")
private String path = "/error";
4、DefaultErrorViewResolver
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> 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<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);
}
错误响应步骤如下:
一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理;如果决定产生html数据,而不是直接返回JSON数据,就使用DefaultErrorViewResolver试图解析器来解析我们的ModelAndView,最终决定返回哪个html文件。我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html,比如发生404错误,有404.html就不去4xx.html了);我们将这些错误响应的.html文件放到静态资源路径下的/error文件夹下即可。
通过上面的描述如果需要返回自己的错误返回界面,直接在静态资源文件夹下新建/error文件夹,将自己的错误响应.html文件放到里面即可,这些.html文件可能是4xx.html、404.html、5xx.html、500.html。具体响应哪个.html文件根据错误代码来决定。如果需要自定义错误响应信息,那么你还需要像下面这样做:
1、只响应JSON数据,而不管请求的是什么(比如请求.html页面,也返回JSON数据)
首先新建@ControllerAdvice类,而后在其中写@ExceptionHandler方法即可,如下:
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}
关于@ControllerAdvice注解更加详细的解释你可以看这里,在本文章中主要是结合@ExceptionHandler注解来处理错误。另外UserNotExistException是我们自定义的一个错误,由Controller抛出。
2、自适应响应
上面处理的方式有一个问题,就是不管是页面请求,还是JSON数据请求,我们都是返回JSON数据,如何做到请求JSON数据就返回JSON数据,请求页面就返回页面呢?我们可以回到上面说到的BasicErrorController组件,这个组件会自适应的处理/error请求,所以我们可以转发到/error界面,如下所示:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",500);
//转发到/error
return "forward:/error";
}
}
但是,这个时候我们又无法响应我们自己定制的错误响应数据了,我们希望不但可以自适应,又可以携带我们自定义的数据。如果你足够细心的话,可以发现,在上面BasicErrorController组件中,不论是响应HTML页面还是反回JSON数据,都会调用getErrorAttributes这个方法,来设置错误响应数据,这个方法来自容器中的DefaultErrorAttributes组件,这个组件会在我们没有在容器中添加ErrorAttributes组件的前提下添加到容器中作为默认的ErrorAttributes以返回错误响应数据;所以我们如果要响应我们自己的数据,我们就要写一个ErrorAttributes,并且实现它的getErrorAttributes方法,然后加入到容器中,为了简单起见我们直接,继承DefaultErrorAttributes然后重写getErrorAttributes方法即可:
//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
//保留默认的错误处理数据
Map<String, Object> map = super.getErrorAttributes(requestAttributes
,includeStackTrace);
//由具体的错误处理器填入的数据,第二个参数的意思是scope,也就是从哪个域中获取,0:request,1:session, .....
map.put("ext", RequestAttributes.getAttribute("ext", 0));
//所有错误都会填入的错误响应数据
map.put("company","BaiDu");
return map;
}
}
上面注释中说到的 “由具体的错误处理器填入的数据” 的意思是如下:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",500);
//填入额外的错误响应处理数据:
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
request.setAttribute("ext", map);
//转发到/error
return "forward:/error";
}
}
这样一来我们就不但可以自适应响应数据,还可以定制自己的错误响应信息了。
总结:
上面详细讲述了Spring Boot中发生错误后,如何处理的的流程,并且自定义错误响应数据和界面。读者可以选择自己需要的功能来选择如何处理,主要是懂得原理,这些处理方式就能使用的游刃有余。简单分为以下两点:
- 不需要自定义错误响应数据,只要到我们自己的错误响应界面,则编写错误代码对应的HTML文件,并将这些错误响应的.html文件放到静态资源路径下的/error文件夹下即可。
- 需要自定义错误响应数据,并且自适应响应,则在容器中加入我们自己的ErrorAttributes组件即可。