前言
对于web开发,要保证良好的用户体验和站点安全,需要对错误情况进行处理。虽然文档可能告诉你一部分细节操作,但如果不充分理解整个错误处理的流程,操作起来犹如盲人摸象。本文按照Servlet API、Spring Boot Web支持、Spring MVC的从底到上的逐层分析在使用Spring Boot时错误的处理流程,可能缺少一些操作细节,但了解了本质,在解决问题累计经验就不是什么难事了。
最底层Servlet错误处理
Servlet API提供了errorpage
的错误处理机制,提供了基于status code/exception type到resource的映射的错误处理方式。
<error-page>
<error-code>404</error-code>
<location>/pageNotFound</location>
</error-page>
Spring Boot的web部分基于Servlet API,自然而然,Servlet提供的异常机制是Spring Boot中web错误处理的基础。
但问题是现代Java Web开发早已抛弃了Servlet API的大部分功能,包括使用web.xml,而且servlet3.0也没有提供相关的Java接口来提供该功能。
错误状态码的补充
虽然我们习惯性的认为HTTP 4XX表示用户错误,HTTP 5XX表示服务器错误,但是对于Servlet API而言,是否需要作为错误情况处理不取决于其值,而是通过HttpServletResponse.sendError
方法来发起的,可选参数message
,并不是HTTP协议的部分,只是方便视图层使用。
public void sendError(int sc, String msg) throws
IOException;
如果我们直接返回内容,如:
return new ResponseEntity(HttpStatus.NOT_FOUND);
其并不会触发错误处理机制。
中层的Spring Boot Web错误处理
由于Servlet缺乏相关的Java API来满足该功能,故Spring Boot提供了对其的支持。同样支持对status code/exception进行错误处理,并同样采取转发到目标资源的方式。
处理机制
Spring Boot Web错误处理机制是独立于MVC框架的,它提供了两个抽象类ErrorPageRegistrar
和ErrorPageRegistry
,和一个模型类ErrorPage
。前两个抽象接口分别表示错误页面注册者和错误页面注册表。而ErrorPage封装了一个错误页面的映射信息。
嵌入式Servlet容器
对于jar包和war包,Spring Boot分别提供了两种方式来对以上定义的错误页面进行处理。当采取嵌入式Servlet容器时,容器工厂实现了错误页面注册表:
public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry {
}
容器工厂在初始化时会注册配置的ErrorPages,其在创建嵌入式Servlet容器时会进行错误页面的配置,这往往依赖于具体的底层实现,如Tomcat会调用其相关API转换并注册为其原生的ErrorPage实现。以Tomcat为例:
TomcatEmbeddedServletContainerFactory:
protected void configureContext(Context context,
ServletContextInitializer[] initializers) {
//...
for (ErrorPage errorPage : getErrorPages()) {
new TomcatErrorPage(errorPage).addToContext(context);
}
//...
}
TomcatErrorPage:
public void addToContext(Context context) {
Assert.state(this.nativePage != null,
"Neither Tomcat 7 nor 8 detected so no native error page exists");
if (ClassUtils.isPresent(ERROR_PAGE_CLASS, null)) {
org.apache.tomcat.util.descriptor.web.ErrorPage errorPage = (org.apache.tomcat.util.descriptor.web.ErrorPage) this.nativePage;
errorPage.setLocation(this.location);
errorPage.setErrorCode(this.errorCode);
errorPage.setExceptionType(this.exceptionType);
context.addErrorPage(errorPage);
}
else {
callMethod(this.nativePage, "setLocation", this.location, String.class);
callMethod(this.nativePage, "setErrorCode", this.errorCode, int.class);
callMethod(this.nativePage, "setExceptionType", this.exceptionType,
String.class);
callMethod(context, "addErrorPage", this.nativePage,
this.nativePage.getClass());//注册为容器原生的错误页面
}
}
这其实是容器对前面提到servlet-errorpage
的底层实现,只是Spring Boot对其封装了一层,还是通过容器对Servlet API的支持来实现错误处理。
war包
而如果是独立部署的war包,Spring Boot则使用Filter来实现。具体为ErrorPageFilter:
public class ErrorPageFilter implements Filter, ErrorPageRegistry {
//...
}
spring mvc-start
以上只是介绍了Spring Boot Web提供的类Servlet错误处理机制。对于Spring MVC,Spring Boot提供了具体的实现,可以在ErrorMvcAutoConfiguration
类找到。
下面简单的介绍相关配置信息,详细的可以参考Spring Boot的文档以及该配置文件源码。
ErrorMvcAutoConfiguration.ErrorPageCustomizer类
默认情况下注册了路径为/error的全局ErrorPage(全局意味则处理所有未匹配的情况)。
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
BasicErrorController
默认的ErrorController,会匹配/error
路径的请求,支持内容协商。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")//默认为“/error”
public class BasicErrorController extends AbstractErrorController {
//...
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {
//...
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
//...
}
}
最上层SpringMVC Web错误处理
理论上Spring Boot支持多种MVC框架,但一般采用SpringMVC。Spring MVC只提供了异常处理。
具体的细节,参考对SpringMVC异常处理机制的分析。这里不重复叙述。
总结
servlet-errorpage提供了对错误状态码和异常的处理。错误状态码的处理通过相关API的触发,逻辑实现由servlet容器提供。
由于Servlet 3.0缺乏errorpage的Java API,Spring Boot自己封装了一套。对于嵌入式Servlet容器,委托给Servlet容器对errorpage的底层支持;对于war包,通过Servlet Filter来实现。
Spring Boot对错误的默认处理机制是转发给映射到/error
的Controller来处理。
Spring MVC本身在Controller层的前端又提供了异常处理机制。