一、Spring Boot错误处理原理
Spring Boot的错误处理自动配置类:org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration(在spring-boot-autoconfigure.jar下),这个类注册了很多关于错误处理的组件。主要的有四个DefaultErrorAttributes、BasicErrorController、ErrorPageCustomizer、DefaultErrorViewResolver。
public class ErrorMvcAutoConfiguration {
...
//注入DefaultErrorAttributes组件
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(
this.serverProperties.getError().isIncludeException());
}
//注入BasicErrorController 组件
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
//@Bean放在方法上,如果参数类型所对应的实例在spring容器中只有一个,则默认选择这个实例。如果有多个,则需要根据参数名
//称来选择(参数名称就相当于是spring的配置文件中的bean的id)
//errorAttributes从容器中注入的其实就是DefaultErrorAttributes(实现ErrorAttributes )
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
//注入ErrorPageCustomizer 组件
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}
@Configuration
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
//注入DefaultErrorViewResolver 组件
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
}
}
...
}
Spring boot错误处理步骤:
1、当程序发生异常或者错误,ErrorPageCustomizer组件就会生效,它的作用是定制错误的响应规则。
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
...
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//this.properties.getError().getPath())的值是"/error"
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
...
}
这段代码说明当系统出现错误后就去到/error请求进行处理。
2、/error请求就会被BasicErrorController处理,处理完后返回响应页面。
@Controller
//如果server.error.path获取不到就用${error.path:/error},error.path获取不到就用/error,说明这个controller的地址是/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
//MediaType.TEXT_HTML_VALUE="text/html"
//如果是浏览器的请求就走这个方法
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
//构建错误数据,保存到model中
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//构建ModelAndView即处理完返回的视图,也就是响应页面
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//如果找不到返回error视图
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//如果是客户端的请求就走这个方法
@RequestMapping
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<>(body, status);
}
...
}
3、响应页面的规则,实际上是通过DefaultErrorViewResolver组件来处理的。上面代码中的resolveErrorView方法就是返回响应页面的。它是BasicErrorController的父类AbstractErrorController中的方法。
public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
private final List<ErrorViewResolver> errorViewResolvers;
...
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//获取所有的ErrorViewResolver,包括DefaultErrorViewResolver,所以我们可以自己定制自己的ErrorViewResolver组件
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}
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);
}
//构建响应页面规则,HttpStatus 是错误的状态码:404、500等。
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
//调用resolve方法创建错误视图
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
//如果没有这个视图并且静态资源中也没有错误页面(具体状态码.html例如:404.html),而且状态码是4开头或者5开头。
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//创建error/4xx或者error/5xx视图。
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误视图例:error/404
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) {
// this.resourceProperties.getStaticLocations()获取的是默认的静态资源文件夹:public、static这些。
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//错误页面,例:error/400.html
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//如果存在这个html页面就用这个,否则返回null
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
}
DefaultErrorViewResolver先去模板引擎(以thymeleaf为例)默认文件夹下(templates)找”error/状态码(404、500等).html"文件,没有,则去Spring Boot默认静态文件夹下找,没有,再去找”error/4xx(5xx).html"文件,找到了则创建对应的视图,没找到返回null。而在BasicErrorController的resolveErrorView方法中有一句:
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
表示如果找不到视图,则创建默认视图"error"。而在ErrorMvcAutoConfiguration.class中引入了error视图:
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
error视图的类型是StaticView,这个类的作用就是生成Spring Boot默认的错误页面。
4、错误数据
从前面的代码中我们知道,通过@Bean注解,我们把DefaultErrorAttributes注入到了BasicErrorController中,而在BasicErrorController的errorHtml方法中,调用getErrorAttributes方法,此方法再调用DefaultErrorAttributes的getErrorAttributes方法获取了封装好的错误数据据。
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);`在这里插入代码片`
return errorAttributes;
}
自定义错误页面:
综上所述,可以知道当有模板引擎时,将错误页面命名为"错误状态码.html"放在模板引擎文件夹里面的error文件夹下,发生此状态码的错误就会来到对应的页面,找不到会去Spring Boot默认的静态资源文件夹下找,也可以使用4xx.html/5xx.html作为错误页面的文件名来匹配4类型和5类型的所有错误,但是精确优先(优先寻找精确的状态码.html)。没有模板引擎时直接去Spring Boot默认的静态资源文件夹下找。如果都没有就生成默认错误页面。
二、自定义异常和错误数据
上面我们说过,Spring Boot的错误数据处理是通过DefaultErrorAttributes类来实现的,我们可以通过继承DefaultErrorAttributes来扩展自己的错误数据处理。这时DefaultErrorAttributes会失效(@ConditionalOnMissingBean)。
@Component//将MyErrorAttributes加到容器中
public class MyErrorAttributes extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 在DefaultErrorAttributes的基础上做扩展。
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company", "comDMF");//自己的错误记录数据thymeleaf使用${company}取出。
return map;
}
}
自定义异常:
1、定义异常类
public class UserNotExistException extends RuntimeException{
public UserNotExistException(){
super("用户不存在!");
}
}
2、创建异常处理类
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)//处理UserNotExistException异常
public Map<String, Object> handleException(Exception e){
Map<String, Object> map = new HashMap<>();
map.put("code", "userNotExist");
map.put("message", e.getMessage());
return map;
}
}
3、创建测试的action方法
//当user参数为aaa时抛出UserNotExistException异常
@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user") String user){
if("aaa".equals(user)){
throw new UserNotExistException();
}
return "hello world!";
}
当程序发生UserNotExistException异常时,就会调用MyExceptionHandler的handleException来处理,返回json数据,而不是错误页面,如果想要让自定义异常经过默认的错误处理流程,返回错误页面,并且携带自己定制的错误数据可以这样做。
1、转发到"/error"请求,因为"/error"请求默认由BasicErrorController处理,这样就可以经过默认的错误处理流程。
2、自定义状态码,如果不设置状态码默认是200,就会跳到Spring Boot的默认错误页面。
3、将自己的错误数据存到request中,然后在自己的ErrorAttributes类里取出存到model里。
实例:
@ControllerAdvice
public class MyExceptionHandler {
//处理UserNotExistException异常
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e,HttpServletRequest request){
//设置状态码
request.setAttribute("javax.servlet.error.status_code", 500);
//设置自己的错误数据,存到request中
Map<String, Object> map = new HashMap<>();
map.put("code", "userNotExist");
map.put("message", e.getMessage());
request.setAttribute("ext", map);
//转发到/error请求
return "forward:/error";
}
}
//必须要将自己的组件加到容器中,才会生效
@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.put("company", "comDMF");
//取出异常处理器携带的数据,存到map中
Map<String, Object> ext = (Map<String, Object>)webRequest.getAttribute("ext", 0);
map.put("ext", ext);
return map;
}
}
状态码这么设置是因为在BasicErrorController的errorHtml方法中通过getStatus方法取状态码:
HttpStatus status = getStatus(request);
getStatus是BasicErrorController的父类AbstractErrorController中的方法:
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
所以设置状态码时设置名为"javax.servlet.error.status_code"。