首页IT科技如何深入理解中国式现代化新道路(深入理解Whitelabel Error Page底层源码)

如何深入理解中国式现代化新道路(深入理解Whitelabel Error Page底层源码)

时间2025-09-17 15:27:56分类IT科技浏览5346
导读:深入理解Whitelabel Error Page底层源码 (一)服务器请求处理错误则转发请求url...

深入理解Whitelabel Error Page底层源码

(一)服务器请求处理错误则转发请求url

StandardHostValveinvoke()方法将根据请求的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()具体由是由TomcatServletWebServerFactoryconfigureContext()方法来调用的         。

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()获取所有错误页面         ,然后再调用ContextaddErrorPage()来添加ErrorPage错误页面                         。

getErrorPages()中的错误页面是通过AbstractConfigurableWebServerFactoryaddErrorPages()来添加的                 。

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()实际上是由ErrorMvcAutoConfigurationErrorPageCustomizerregisterErrorPages()调用的。

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"; // ... } }

然后调用DispatcherServletPathgetRelativePath()来构建错误页面的完整路径。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                         、BasicErrorControllerView(名称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); } }

BasicErrorControllererrorHtml()中返回的是名称为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字段

BasicErrorControllererrorHtml()error()中                 ,内部均调用了AbstractErrorControllerErrorAttributes字段的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版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
如果启用宏功能(启用文件中的宏)