32、SpringMVC源码分析 - ErrorPage全局异常处理
前言
@ControllerAdvice 全局异常处理 和 自定义HandlerExceptionResolver 解析异常,这两种方式只能处理在处理请求请求到达了 DispatcherServlet ,并且出现了异常后进入processDispatchResult( ) 方法。
这两种方式不适用的场景:
1、 请求没有到达DispatcherServlet的核心流程,如在filter中抛出异常;
2、 请求进入processDispatchResult()方法处理异常,但是在处理过程中有抛出了异常,如在@ControllerAdvice方法中抛出了异常;
这个时候请求会进入到 ErrorPage 的处理流程。
一、ErrorPage初始化
1、 ErrorPageCustomizer;
实现了ErrorPageRegistrar 接口,重写了registerErrorPages( ) 方法,用于注册 ErrorPage
1、ErrorPageCustomizer 的定义
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//注册错误页面,默认Path 是 /error
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
2、、默认错误路径
public class ErrorProperties {
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
3、、创建 ErrorPageCustomizer
ErrorMvcAutoConfiguration.java
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
2、 ErrorPageRegistrarBeanPostProcessor;
ErrorPageRegistrar 的后置处理器,注册错误页面到web容器
public class ErrorPageRegistrarBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
private ListableBeanFactory beanFactory;
private List<ErrorPageRegistrar> registrars;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
Assert.isInstanceOf(ListableBeanFactory.class, beanFactory,
"ErrorPageRegistrarBeanPostProcessor can only be used with a ListableBeanFactory");
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ErrorPageRegistry) {
//bean是 ErrorPageRegistry 类型
postProcessBeforeInitialization((ErrorPageRegistry) bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private void postProcessBeforeInitialization(ErrorPageRegistry registry) {
//调用 ErrorPageRegistrar 的 registerErrorPages() 注册错误页面
for (ErrorPageRegistrar registrar : getRegistrars()) {
registrar.registerErrorPages(registry);
}
}
//从beanFactory中获取 ErrorPageRegistrar.class 类型的类,通过 order 排序
private Collection<ErrorPageRegistrar> getRegistrars() {
if (this.registrars == null) {
// Look up does not include the parent context
this.registrars = new ArrayList<>(
this.beanFactory.getBeansOfType(ErrorPageRegistrar.class, false, false).values());
this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE);
this.registrars = Collections.unmodifiableList(this.registrars);
}
return this.registrars;
}
}
ErrorPage 被添加到了 web容器中
AbstractConfigurableWebServerFactory.java
public void addErrorPages(ErrorPage... errorPages) {
Assert.notNull(errorPages, "ErrorPages must not be null");
this.errorPages.addAll(Arrays.asList(errorPages));
}
3、 将ErrorPage添加到DeploymentInfo中;
我这里使用的容器是 Undertow 服务,在创建服务的过程中
refresh( ) -> onRefresh( ) -> createWebServer( ) -> getWebServer( ) -> createManager( ) -> configureErrorPages( )
private void configureErrorPages(DeploymentInfo servletBuilder) {
//获取到刚才注册的 ErrorPage
for (ErrorPage errorPage : getErrorPages()) {
servletBuilder.addErrorPage(getUndertowErrorPage(errorPage));
}
}
封装成undertow api 的 ErrorPage
private io.undertow.servlet.api.ErrorPage getUndertowErrorPage(ErrorPage errorPage) {
if (errorPage.getStatus() != null) {
return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getStatusCode());
}
if (errorPage.getException() != null) {
return new io.undertow.servlet.api.ErrorPage(errorPage.getPath(), errorPage.getException());
}
return new io.undertow.servlet.api.ErrorPage(errorPage.getPath());
}
添加到DeploymentInfo 缓存起来
DeploymentInfo .java
public DeploymentInfo addErrorPage(final ErrorPage errorPage) {
this.errorPages.add(errorPage);
return this;
}
4、 ;
configureErrorPages( )执行后, ->manager.deploy(); -> initializeErrorPages(deployment, deploymentInfo);
1、initializeErrorPages()
private void initializeErrorPages(final DeploymentImpl deployment, final DeploymentInfo deploymentInfo) {
final Map<Integer, String> codes = new HashMap<>();
final Map<Class<? extends Throwable>, String> exceptions = new HashMap<>();
String defaultErrorPage = null;
for (final ErrorPage page : deploymentInfo.getErrorPages()) {
if (page.getExceptionType() != null) {
//exceptions 保存异常类型和路径的映射
exceptions.put(page.getExceptionType(), page.getLocation());
} else if (page.getErrorCode() != null) {
//codes 保存异常状态码和路径的映射
codes.put(page.getErrorCode(), page.getLocation());
} else {
//默认异常路径只能有一个
if (defaultErrorPage != null) {
throw UndertowServletMessages.MESSAGES.moreThanOneDefaultErrorPage(defaultErrorPage, page.getLocation());
} else {
defaultErrorPage = page.getLocation();
}
}
}
deployment.setErrorPages(new ErrorPages(codes, exceptions, defaultErrorPage));
}
5、 BasicErrorController;
请求出现错误时,且没有被其他的全局异常处理拦截到,会将请求流转到ErrorController 中的 /error 和 /errorHtml 接口
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
二、ErrorPage处理流程
当请求出现错误时,错误被抛给Undertow时,错误会被Undertow拦截到,下面从Undertow拦截到错误开始分析
1、 handleFirstRequest();
try {
...
}
} catch (Throwable t) {
servletRequestContext.setRunningInsideHandler(false);
AsyncContextImpl asyncContextInternal = servletRequestContext.getOriginalRequest().getAsyncContextInternal();
if(asyncContextInternal != null && asyncContextInternal.isCompletedBeforeInitialRequestDone()) {
asyncContextInternal.handleCompletedBeforeInitialRequestDone();
}
if(asyncContextInternal != null) {
asyncContextInternal.initialRequestFailed();
}
//by default this will just log the exception
boolean handled = exceptionHandler.handleThrowable(exchange, request, response, t);
if(handled) {
exchange.endExchange();
} else if (request.isAsyncStarted() || request.getDispatcherType() == DispatcherType.ASYNC) {
//异步请求
exchange.unDispatch();
servletRequestContext.getOriginalRequest().getAsyncContextInternal().handleError(t);
} else {
if (!exchange.isResponseStarted()) {
//重置 response
response.reset(); //reset the response
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
//清理了响应头,这里很重要,缺少了响应头,可能会引起一些其他的错误
exchange.getResponseHeaders().clear();
//获取新的请求错误路径
String location = servletContext.getDeployment().getErrorPages().getErrorLocation(t);
if (location == null) {
location = servletContext.getDeployment().getErrorPages().getErrorLocation(StatusCodes.INTERNAL_SERVER_ERROR);
}
if (location != null) {
RequestDispatcherImpl dispatcher = new RequestDispatcherImpl(location, servletContext);
try {
//将请求分发到错误路径上,也就是访问默认异常控制器 BasicErrorController
dispatcher.error(servletRequestContext, request, response, servletRequestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getServletInfo().getName(), t);
} catch (Exception e) {
UndertowLogger.REQUEST_LOGGER.exceptionGeneratingErrorPage(e, location);
}
} else {
if (servletRequestContext.displayStackTraces()) {
ServletDebugPageHandler.handleRequest(exchange, servletRequestContext, t);
} else {
servletRequestContext.getOriginalResponse().doErrorDispatch(StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR_STRING);
}
}
}
}
} finally {
...
}
2、 getErrorLocation();
优先根据异常类型查找路径,不存在时再根据状态码查找路径,都不存在时使用默认路径
public String getErrorLocation(final Throwable exception) {
if (exception == null) {
return null;
}
//todo: this is kinda slow, but there is probably not a great deal that can be done about it
String location = null;
for (Class c = exception.getClass(); c != null && location == null; c = c.getSuperclass()) {
location = exceptionMappings.get(c);
}
if (location == null && exception instanceof ServletException) {
Throwable rootCause = ((ServletException) exception).getRootCause();
//Iterate through any nested JasperException in case it is in JSP development mode
while (rootCause != null && rootCause instanceof ServletException && location == null) {
for (Class c = rootCause.getClass(); c != null && location == null; c = c.getSuperclass()) {
location = exceptionMappings.get(c);
}
rootCause = ((ServletException) rootCause).getRootCause();
}
if (rootCause != null && location == null) {
for (Class c = rootCause.getClass(); c != null && location == null; c = c.getSuperclass()) {
location = exceptionMappings.get(c);
}
}
}
if (location == null) {
location = getErrorLocation(StatusCodes.INTERNAL_SERVER_ERROR);
}
return location;
}
三、自定义ErrorPage
1、 自定义GlobalErrorPageRegistrar;
可以通过状态码或者具体的异常类型对应要访问的路径
@Configuration
@Slf4j
public class GlobalErrorPageRegistrar implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
//状态码和路径映射
registry.addErrorPages(
new ErrorPage(HttpStatus.BAD_REQUEST, "/400"),
new ErrorPage(HttpStatus.NOT_FOUND, "/404"),
new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500")
);
//异常类型和路径映射
registry.addErrorPages(
new ErrorPage(IllegalArgumentException.class, "/400"),
new ErrorPage(HttpTimeoutException.class, "/408")
);
}
}
2、 ErrorPageController;
配置错误的请求对应的方法
@RestController
public class ErrorPageController {
@RequestMapping(value = "/400", produces = {
MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity to400() {
return new ResponseEntity(400, "请求有误");
}
@RequestMapping(value = "/404", produces = {
MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity to404() {
return new ResponseEntity(404, "找不到资源");
}
@RequestMapping(value = "/408", produces = {
MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public ResponseEntity to408() {
return new ResponseEntity(408, "请求超时");
}
@RequestMapping(value = "/500", produces = {
MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity to500() {
return new ResponseEntity(500, "服务器错误");
}
}