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

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

时间2025-05-02 12:50:01分类IT科技浏览3953
导读:深入理解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
js处理树形结构类数据(javascript生成一棵树) win10系统激活密钥最新(最新windows10各版本激活密钥 附激活工具)