SpringBoot请求错误如404可能看到如下页面:
有时可能需要自定义错误页面针对不同的http.status,如404/400。
【1】解决方法
① 注册错误页面
如下所示:
@Componentpublic 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。
1.1AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization
在创建bean-tomcatServletWebServerFactory时会调用AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization,如下所示:
@Overridepublic 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方法,方法源码如下所示:
@Overridepublic 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 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方法。
② 把错误页面放到StandardContext.errorPageSupport中
StandardContext是什么?我们可以看下如下类继承示意图。
在①中我们提到会注册错误页面registrar.registerErrorPages(registry);,如下图所示此时的registry为TomcatServletWebServerFactory:
我们再来看下TomcatServletWebServerFactory继承示意图(可以看到其父类AbstractConfigurableWebServerFactory实现了ErrorPageRegistry接口):
2.1 AbstractConfigurableWebServerFactory.addErrorPages
方法源码如下:
@Overridepublic void addErrorPages(ErrorPage... errorPages) { Assert.notNull(errorPages, "ErrorPages must not be null"); this.errorPages.addAll(Arrays.asList(errorPages));}
也就说错误页面现在被放到了属性private Set errorPages = new LinkedHashSet<>();中。
2.2 TomcatServletWebServerFactory.configureContext
创建完TomcatServletWebServerFactory后会调用configureContext方法,如下图所示:
在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分别是什么呢?我们看下图示意:
③ 错误页面如何被用到
在ResourceHttpRequestHandler.handleRequest方法处理请求时,找不到资源会调用response.sendError方法:
这里只需要关注这一点,无需关注细节,我们继续往下走。。。。一直走到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进行请求转发。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://blog.csdn.net/j080624/article/details/109197726