0
点赞
收藏
分享

微信扫一扫

SpringBoot自定义错误页面与原理讲解

SpringBoot请求错误如404可能看到如下页面:

SpringBoot自定义错误页面与原理讲解_spring boot
有时可能需要自定义错误页面针对不同的http.status,如404/400。

【1】解决方法

① 注册错误页面

如下所示:

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/404");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
registry.addErrorPages(error400Page,error404Page,error500Page);
}
}

② controller进行拦截

然后你只需要写个controller拦截不同请求然后跳到不同的自定义错误页面即可,如下所示:

@RequestMapping("/error/{status}")
public String errorPage(@PathVariable Integer status){
switch (status){
case 401:
case 400:return "/error/404";
case 500:return "/error/500";
default:return "/error/default";
}
}

那么原理呢?

【2】原理讲解

① 启动SpringBoot,注册错误页面

如下图所示,启动项目时候再onRefresh方法中会创建一个WebServer,继而获取ServletWebServerFactory。
SpringBoot自定义错误页面与原理讲解_spring boot_02

1.1AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization

在创建bean-tomcatServletWebServerFactory时会调用AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization,如下所示:

@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

该方法会获取bean后置处理器,然后循环遍历调用每个bean后置处理器的postProcessBeforeInitialization方法。

1.2 ErrorPageRegistrarBeanPostProcessor.postProcessBeforeInitialization

当遍历到ErrorPageRegistrarBeanPostProcessor时会调用其postProcessBeforeInitialization方法,方法源码如下所示:

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ErrorPageRegistry) {
postProcessBeforeInitialization((ErrorPageRegistry) bean);
}
return bean;
}

方法会判断当前bean是否ErrorPageRegistry类型,如果是,则调用postProcessBeforeInitialization方法,源码如下所示:

private void postProcessBeforeInitialization(ErrorPageRegistry registry) {
for (ErrorPageRegistrar registrar : getRegistrars()) {
registrar.registerErrorPages(registry);
}
}

该方法会获取Registrars,然后循环遍历调用每一个注册器的registerErrorPages方法。获取注册其源码如下所示:

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;
}

故而,当我们的ErrorPageConfig 实现了ErrorPageRegistrar时,会被检测到并执行registerErrorPages方法。
SpringBoot自定义错误页面与原理讲解_spring boot_03
SpringBoot自定义错误页面与原理讲解_错误页面_04

② 把错误页面放到StandardContext.errorPageSupport中

StandardContext是什么?我们可以看下如下类继承示意图。
SpringBoot自定义错误页面与原理讲解_spring boot_05

在①中我们提到会注册错误页面​​registrar.registerErrorPages(registry);​​​,如下图所示此时的registry为TomcatServletWebServerFactory:
SpringBoot自定义错误页面与原理讲解_ide_06

我们再来看下TomcatServletWebServerFactory继承示意图(可以看到其父类AbstractConfigurableWebServerFactory实现了ErrorPageRegistry接口):
SpringBoot自定义错误页面与原理讲解_ide_07

2.1 AbstractConfigurableWebServerFactory.addErrorPages

方法源码如下:

@Override
public void addErrorPages(ErrorPage... errorPages) {
Assert.notNull(errorPages, "ErrorPages must not be null");
this.errorPages.addAll(Arrays.asList(errorPages));
}

也就说错误页面现在被放到了属性​​private Set<ErrorPage> errorPages = new LinkedHashSet<>();​​中。

2.2 TomcatServletWebServerFactory.configureContext

创建完TomcatServletWebServerFactory后会调用configureContext方法,如下图所示:
SpringBoot自定义错误页面与原理讲解_spring boot_08
在configureContext方法中会获取错误页面然后逐个调用StandardContext.addErrorPage方法添加到其​​​ErrorPageSupport errorPageSupport​​中。

configureContext方法中遍历错误页面如下所示:

for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}

StandardContext.addErrorPage方法源码如下所示:

@Override
public void addErrorPage(ErrorPage errorPage) {
// Validate the input parameters
if (errorPage == null)
throw new IllegalArgumentException
(sm.getString("standardContext.errorPage.required"));
String location = errorPage.getLocation();
if ((location != null) && !location.startsWith("/")) {
if (isServlet22()) {
if(log.isDebugEnabled())
log.debug(sm.getString("standardContext.errorPage.warning",
location));
errorPage.setLocation("/" + location);
} else {
throw new IllegalArgumentException
(sm.getString("standardContext.errorPage.error",
location));
}
}
//调用errorPageSupport.add
errorPageSupport.add(errorPage);
fireContainerEvent("addErrorPage", errorPage);
}

ErrorPageSupport.add方法如下所示:

public void add(ErrorPage errorPage) {
String exceptionType = errorPage.getExceptionType();
if (exceptionType == null) {
statusPages.put(Integer.valueOf(errorPage.getErrorCode()), errorPage);
} else {
exceptionPages.put(exceptionType, errorPage);
}
}

通过该方法可以看到,不止可以通过HTTP状态码定义错误页面,还可以通过异常类型进行定义。

那么ErrorPageSupport、statusPages、exceptionPages分别是什么呢?我们看下图示意:
SpringBoot自定义错误页面与原理讲解_tomcat_09

③ 错误页面如何被用到

在ResourceHttpRequestHandler.handleRequest方法处理请求时,找不到资源会调用​​response.sendError​​​方法:
SpringBoot自定义错误页面与原理讲解_ide_10
这里只需要关注这一点,无需关注细节,我们继续往下走。。。。一直走到StandardHostValve.status方法。

StandardHostValve.status中会对响应状态码进行处理。

private void status(Request request, Response response) {

int statusCode = response.getStatus();

// Handle a custom error page for this status code
Context context = request.getContext();
if (context == null) {
return;
}

/* Only look for error pages when isError() is set.
* isError() is set when response.sendError() is invoked. This
* allows custom error pages without relying on default from
* web.xml.
*/
if (!response.isError()) {
return;
}
//这里会从errorPageSupport.find(errorCode)获取到错误页
//根据错误码,比如404从statusPages获取对应的ErrorPage对象
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
// Look for a default error page
errorPage = context.findErrorPage(0);
}
if (errorPage != null && response.isErrorReportRequired()) {
response.setAppCommitted(false);
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
Integer.valueOf(statusCode));

String message = response.getMessage();
if (message == null) {
message = "";
}
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
errorPage.getLocation());
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
DispatcherType.ERROR);


Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
wrapper.getName());
}
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
request.getRequestURI());

//这里很重要,将会尝试跳转到我们自定义错误请求页面
if (custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException e) {
// Ignore
} catch (IOException e) {
container.getLogger().warn("Exception Processing " + errorPage, e);
}
}
}
}

如下图所示,在StandardHostValve.custom方法中将会调用ApplicationDispatcher.forwar进行请求转发。
SpringBoot自定义错误页面与原理讲解_ide_11


举报

相关推荐

0 条评论