如何深入理解中国式现代化新道路(深入理解Whitelabel Error Page底层源码)
深入理解Whitelabel Error Page底层源码
(一)服务器请求处理错误则转发请求url
StandardHostValve的invoke()方法将根据请求的url选择正确的Context来进行处理 。在发生错误的情况下 ,内部将调用status()或throwable()来进行处理 。具体而言 ,当出现HttpStatus错误时,则将由status()进行处理。当抛出异常时 ,则将由throwable()进行处理 。status()和throwable()的内部均是通过Context来查找对应的ErrorPage ,并最终调用custom()来进行处理 。custom()用于将请求转发到ErrorPage错误页面中 。
在SpringBoot项目中 ,如果服务器处理请求失败 ,则会通过上述的过程将请求转发到/error中 。
final class StandardHostValve extends ValveBase { private void status(Request request, Response response) { // ... Context context = request.getContext(); // ... // 从Context中查找ErrorPag ErrorPage errorPage = context.findErrorPage(statusCode); // ... // 调用custom() custom(request, response, errorPage); // ... } protected void throwable(Request request, Response response, Throwable throwable) { // ... // 从Context查找ErrorPage ErrorPage errorPage = context.findErrorPage(throwable); // ... // 调用custom() custom(request, response, errorPage); // ... } private boolean custom(Request request, Response response, ErrorPage errorPage) { // ... // 请求转发 rd.forward(request.getRequest(), response.getResponse()); // ... } }(二)路径为/error的ErrorPage
为了能在Context中查找到ErrorPage ,则必须先通过addErrorPage()来添加ErrorPage 。在运行时 ,Context具体由StandardContext进行处理 。
public class StandardContext extends ContainerBase implements Context, NotificationEmitter { private final ErrorPageSupport errorPageSupport = new ErrorPageSupport(); @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(errorPage); fireContainerEvent("addErrorPage", errorPage); } }addErrorPage()具体由是由TomcatServletWebServerFactory的configureContext()方法来调用的 。
public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); if (context instanceof TomcatEmbeddedContext) { TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context; embeddedContext.setStarter(starter); embeddedContext.setFailCtxIfServletStartFails(true); } context.addServletContainerInitializer(starter, NO_CLASSES); for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) { context.addLifecycleListener(lifecycleListener); } for (Valve valve : this.contextValves) { context.getPipeline().addValve(valve); } 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); } for (MimeMappings.Mapping mapping : getMimeMappings()) { context.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); } configureSession(context); new DisableReferenceClearingContextCustomizer().customize(context); for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) { customizer.customize(context); } } }先调用getErrorPages()获取所有错误页面 ,然后再调用Context的addErrorPage()来添加ErrorPage错误页面 。
getErrorPages()中的错误页面是通过AbstractConfigurableWebServerFactory的addErrorPages()来添加的 。
public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory { @Override public void addErrorPages(ErrorPage... errorPages) { Assert.notNull(errorPages, "ErrorPages must not be null"); this.errorPages.addAll(Arrays.asList(errorPages)); } }addErrorPages()实际上是由ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()调用的。
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) { ErrorPage errorPage = new ErrorPage( this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } }在registerErrorPages()中 ,先从ServerProperties中获取ErrorProperties ,又从ErrorProperties中获取path,而path默认为/error 。可通过在配置文件中设置server.error.path来进行配置 。
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { public class ErrorProperties { // ... @Value("${error.path:/error}") private String path = "/error"; // ... } }然后调用DispatcherServletPath的getRelativePath()来构建错误页面的完整路径。getRelativePath()调用getPrefix()用于获取路径前缀 ,getPrefix()又调用getPath()来获取路径 。
@FunctionalInterface public interface DispatcherServletPath { default String getRelativePath(String path) { String prefix = getPrefix(); if (!path.startsWith("/")) { path = "/" + path; } return prefix + path; } default String getPrefix() { String result = getPath(); int index = result.indexOf(*); if (index != -1) { result = result.substring(0, index); } if (result.endsWith("/")) { result = result.substring(0, result.length() - 1); } return result; } }DispatcherServletPath实际上是由DispatcherServletRegistrationBean进行处理的 。而DispatcherServletRegistrationBean的path字段值由构造函数给出。
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet> implements DispatcherServletPath { private final String path; public DispatcherServletRegistrationBean(DispatcherServlet servlet, String path) { super(servlet); Assert.notNull(path, "Path must not be null"); this.path = path; super.addUrlMappings(getServletUrlMapping()); } }而DispatcherServletRegistrationBean实际上是在DispatcherServletAutoConfiguration中的DispatcherServletRegistrationConfiguration创建的 。
@Configuration(proxyBeanMethods = false) @Conditional(DispatcherServletRegistrationCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; } }因此创建DispatcherServletRegistrationBean时 ,将从WebMvcProperties中获取path 。默认值为/,可在配置文件中设置spring.mvc.servlet.path来配置 。也就是说getPrefix()返回值就是/ 。
@ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { // ... private final Servlet servlet = new Servlet(); // ... public static class Servlet { // ... private String path = "/"; } // ... }最终在ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()中注册的错误页面路径为将由两个部分构成 ,前缀为spring.mvc.servlet.path ,而后缀为server.error.path 。前者默认值为/,后者默认值为/error 。因此 ,经过处理后最终返回的ErrorPath的路径为/error 。
SpringBoot会通过上述的过程在StandardContext中添加一个路径为/error的ErrorPath 。当服务器发送错误时 ,则从StandardContext中获取到路径为/error的ErrorPath ,然后将请求转发到/error中 ,然后由SpringBoot自动配置的默认Controller进行处理 ,返回一个Whitelabel Error Page页面 。
(三)Whitelabel Error Page视图
SpringBoot自动配置ErrorMvcAutoConfiguration。并在@ConditionalOnMissingBean的条件下创建DefaultErrorAttributes 、DefaultErrorViewResolver 、BasicErrorController和View(名称name为error)的Bean组件 。
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), errorViewResolvers.orderedStream().collect(Collectors.toList())); } @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final StaticView defaultErrorView = new StaticView(); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } } }BasicErrorController是一个控制器组件 ,映射值为${server.error.path:${error.path:/error}} ,与在StandardContext中注册的ErrorPage的路径一致 。BasicErrorController提供两个请求映射的处理方法errorHtml()和error()。errorHtml()用于处理浏览器访问时返回的HTML页面 。方法内部调用getErrorAttributes()和resolveErrorView() 。当无法从resolveErrorView()中获取任何ModelAndView时 ,将默认返回一个名称为error的ModelAndView。error()用于处理ajax请求时返回的响应体数据 。方法内部调用getErrorAttributes()并将返回值作为响应体返回到客户端中 。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { @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); } }在BasicErrorController的errorHtml()中返回的是名称为error的ModelAndView ,因此Whitelabel Error Page页面就是由于名称为error的View提供的 。在ErrorMvcAutoConfiguration已经自动配置一个名称为error的View,具体为ErrorMvcAutoConfiguration.StaticView ,它的render()方法输出的就是Whitelabel Error Page页面 。
private static class StaticView implements View { private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8); private static final Log logger = LogFactory.getLog(StaticView.class); @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = getMessage(model); logger.error(message); return; } response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); Object timestamp = model.get("timestamp"); Object message = model.get("message"); Object trace = model.get("trace"); if (response.getContentType() == null) { response.setContentType(getContentType()); } builder.append("<html><body><h1>Whitelabel Error Page</h1>").append( "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>") .append("<div id=created>").append(timestamp).append("</div>") .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error"))) .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>"); if (message != null) { builder.append("<div>").append(htmlEscape(message)).append("</div>"); } if (trace != null) { builder.append("<div>").append(htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); response.getWriter().append(builder.toString()); } }SpringBoot会通过上述的过程在Context中添加一个路径为/error的ErrorPath 。当服务器发送错误时 ,则从Context中获取到路径为/error的ErrorPath,然后将请求转发到/error中 ,然后由SpringBoot自动配置的BasicErrorController进行处理 ,返回一个Whitelabel Error Page页面,并且在页面中通常还包含timestamp、error 、status 、message 、trace字段信息 。
(四)Whitelabel Error Page字段
在BasicErrorController的errorHtml()和error()中 ,内部均调用了AbstractErrorController的ErrorAttributes字段的getErrorAttributes() 。
public abstract class AbstractErrorController implements ErrorController { private final ErrorAttributes errorAttributes; protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) { WebRequest webRequest = new ServletWebRequest(request); return this.errorAttributes.getErrorAttributes(webRequest, options); } }在ErrorMvcAutoConfiguration中自动配置了ErrorAttributes的Bean ,即DefaultErrorAttributes 。在DefaultErrorAttributes中通过getErrorAttributes()来获取所有响应字段 。getErrorAttributes()先添加timestamp字段 ,然后又调用addStatus() 、addErrorDetails() 、addPath()来添加其他字段。
@Order(Ordered.HIGHEST_PRECEDENCE) public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); if (Boolean.TRUE.equals(this.includeException)) { options = options.including(Include.EXCEPTION); } if (!options.isIncluded(Include.EXCEPTION)) { errorAttributes.remove("exception"); } if (!options.isIncluded(Include.STACK_TRACE)) { errorAttributes.remove("trace"); } if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) { errorAttributes.put("message", ""); } if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } return errorAttributes; } @Override @Deprecated 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; } private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { // Unable to obtain a reason errorAttributes.put("error", "Http Status " + status); } } private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) { Throwable error = getError(webRequest); if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = error.getCause(); } errorAttributes.put("exception", error.getClass().getName()); if (includeStackTrace) { addStackTrace(errorAttributes, error); } } addErrorMessage(errorAttributes, webRequest, error); } private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { String path = getAttribute(requestAttributes, RequestDispatcher.ERROR_REQUEST_URI); if (path != null) { errorAttributes.put("path", path); } } }因此SpringBoot会通过上述过程 ,向BasicErrorController注入DefaultErrorAttributes的Bean ,然后调用其getErrorAttributes()来获取所有的字段信息 ,最后通过StaticView的render()将字段信息输出到Whitelablel Error Page页面中 ,这就是为什么Whitelabel Error Page会出现timestamp 、error 、status 、message 、trace字段信息的原因 。
(五)底层源码核心流程
底层源码核心流程
SpringBoot通过ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()向StandardContext中添加一个路径为/error为ErrorPage 。 当服务器处理请求失败(HttpStatus错误、抛出异常)时 ,将通过StandardHostValve的custom()将请求转发到路径为/error的ErrorPage中。 /error请求由BasicErrorController进行处理 ,通过errorHtml()返回一个StaticView,即Whitelabel Error Page 。向StandardContext添加的ErrorPage路径和BasicErrorController处理的请求路径均是从配置文件server.error.path中读取的 。
(六)自定义拓展
修改server.error.path来实现自定义的错误转发路径。server.error.path用于配置请求处理错误时转发的路径 ,默认值为/error 。因此我们可以修改server.error.path的值来自定义错误转发路径 ,然后再通过自定义的Controller来对错误转发路径进行处理 。
继承DefaultErrorAttributes并重写getErrorAttributes()来实现自定义异常属性 。在ErrorMvcAutoConfiguration中创建ErrorAttributes的Bean时使用了的@ConditionalOnMissBean注解,因此我们可以自定义一个ErrorAttributes的Bean来覆盖默认的DefaultErrorAttributes 。通常的做法是继承DefaultErrorAttributes并重写getErrorAttributes()来实现自定义异常属性 。
由于BasicErrorController的errorHtml()和error()内部均会调用ErrorAttributes的getErrorAttributes() ,因此BasicErrorController将会调用我们自定义的ErrorAttributes的Bean的getErrorAttributes()来获取错误属性字段 。
继承DefaultErrorViewResolver并重写resolveErrorView()来实现自定义异常视图 。BasicErrorController会调用ErrorViewResolver的resolveErrorView()来寻找合适的错误视图 。DefaultErrorViewResolver默认会从resources目录中查找4xx.html 、5xx.html页面 。当无法找到合适的错误视图时 ,将自动返回一个名称为error的视图,此视图由StaticView解析 ,也就是Whitelabel Error Page。
在ErrorMvcAutoConfiguration中创建ErrorViewResolver的Bean时使用了@ConditionalOnMissBean注解 ,因此我们可以自定义一个ErrorViewResolver来覆盖默认的DefaultErrorViewResolver 。通常的做法是继承DefaultErrorViewResolver并重写resolveErrorView()来实现自定义异常视图 。
实现ErrorController接口来自定义错误映射处理。不推荐直接继承BasicErrorController 。在ErrorMvcAutoConfiguration中创建ErrorController的Bean时使用了@ConditionalOnMissBean注解 ,因此我们可以自定义一个ErrorController来覆盖默认的BasicErrorController 。通常的做法是实现ErrorController接口来自定义错误映射处理。具体实现时可参考AbstractErrorController和BasicErrorController 。
当服务器处理请求失败后 ,底层会将请求默认转发到/error映射中 ,因此我们必须提供一个处理/error请求映射的方法来保证对错误的处理 。
在前后端分离项目中 ,前端与后端的交互通常是通过json字符串进行的 。当服务器请求处理异常时 ,我们不能返回一个Whitelabel Error Page的HTML页面 ,而是返回一个友好的 、统一的json字符串 。为了实现这个目的 ,我们必须覆盖BasicErrorController来实现在错误时的自定义数据返回 。
// 统一响应类 @AllArgsConstructor @Data public static class Response<T> { private Integer code; private String message; private T data; } // 自定义的ErrorController参考BasicErrorController、AbstractErrorController实现 @RestController @RequestMapping("${server.error.path:${error.path:/error}}") @RequiredArgsConstructor @Slf4j public static class MyErrorController implements ErrorController { private final DefaultErrorAttributes defaultErrorAttributes; @Override public String getErrorPath() { // 忽略 return null; } @GetMapping public Response<Void> error(HttpServletRequest httpServletRequest) { // 获取默认的错误信息并打印异常日志 log.warn(String.valueOf(errorAttributes(httpServletRequest))); // 返回统一响应类 return new Response<>(-1, "error", null); } private Map<String, Object> errorAttributes(HttpServletRequest httpServletRequest) { return defaultErrorAttributes.getErrorAttributes( new ServletWebRequest(httpServletRequest), ErrorAttributeOptions.of( ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.STACK_TRACE, ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.BINDING_ERRORS) ); } }创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!