最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

焦点速看:深入理解Whitelabel Error Page底层源码

来源:博客园

深入理解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 AbstractServletWebServerFactoryimplements 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()来获取路径。

@FunctionalInterfacepublic 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 ServletRegistrationBeanimplements 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 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的条件下创建DefaultErrorAttributesDefaultErrorViewResolverBasicErrorControllerView(名称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 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的ModelAndViewerror()用于处理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 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);}@RequestMappingpublic ResponseEntity> error(HttpServletRequest request) {HttpStatus status = getStatus(request);if (status == HttpStatus.NO_CONTENT) {return new ResponseEntity<>(status);}Map 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 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("

Whitelabel Error Page

").append( "

This application has no explicit mapping for /error, so you are seeing this as a fallback.

") .append("
").append(timestamp).append("
") .append("
There was an unexpected error (type=").append(htmlEscape(model.get("error"))) .append(", status=").append(htmlEscape(model.get("status"))).append(").
"); if (message != null) { builder.append("
").append(htmlEscape(message)).append("
"); } if (trace != null) { builder.append("
").append(htmlEscape(trace)).append("
"); } builder.append(""); 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 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 {    @Overridepublic Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {Map 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@Deprecatedpublic Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map errorAttributes = new LinkedHashMap<>();errorAttributes.put("timestamp", new Date());addStatus(errorAttributes, webRequest);addErrorDetails(errorAttributes, webRequest, includeStackTrace);addPath(errorAttributes, webRequest);return errorAttributes;}    private void addStatus(Map 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 reasonerrorAttributes.put("error", "Http Status " + status);}}private void addErrorDetails(Map 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 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字段信息的原因。

(五)底层源码核心流程

底层源码核心流程

  1. SpringBoot通过ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()向StandardContext中添加一个路径为/error为ErrorPage。
  2. 当服务器处理请求失败(HttpStatus错误、抛出异常)时,将通过StandardHostValve的custom()将请求转发到路径为/error的ErrorPage中。
  3. /error请求由BasicErrorController进行处理,通过errorHtml()返回一个StaticView,即Whitelabel Error Page。

向StandardContext添加的ErrorPage路径和BasicErrorController处理的请求路径均是从配置文件server.error.path中读取的。

(六)自定义拓展

  1. 修改server.error.path来实现自定义的错误转发路径。

server.error.path用于配置请求处理错误时转发的路径,默认值为/error。因此我们可以修改server.error.path的值来自定义错误转发路径,然后再通过自定义的Controller来对错误转发路径进行处理。

  1. 继承DefaultErrorAttributes并重写getErrorAttributes()来实现自定义异常属性。

在ErrorMvcAutoConfiguration中创建ErrorAttributes的Bean时使用了的@ConditionalOnMissBean注解,因此我们可以自定义一个ErrorAttributes的Bean来覆盖默认的DefaultErrorAttributes。通常的做法是继承DefaultErrorAttributes并重写getErrorAttributes()来实现自定义异常属性。

由于BasicErrorController的errorHtml()和error()内部均会调用ErrorAttributes的getErrorAttributes(),因此BasicErrorController将会调用我们自定义的ErrorAttributes的Bean的getErrorAttributes()来获取错误属性字段。

  1. 继承DefaultErrorViewResolver并重写resolveErrorView()来实现自定义异常视图。

BasicErrorController会调用ErrorViewResolver的resolveErrorView()来寻找合适的错误视图。DefaultErrorViewResolver默认会从resources目录中查找4xx.html、5xx.html页面。当无法找到合适的错误视图时,将自动返回一个名称为error的视图,此视图由StaticView解析,也就是Whitelabel Error Page。

在ErrorMvcAutoConfiguration中创建ErrorViewResolver的Bean时使用了@ConditionalOnMissBean注解,因此我们可以自定义一个ErrorViewResolver来覆盖默认的DefaultErrorViewResolver。通常的做法是继承DefaultErrorViewResolver并重写resolveErrorView()来实现自定义异常视图。

  1. 实现ErrorController接口来自定义错误映射处理。不推荐直接继承BasicErrorController。

在ErrorMvcAutoConfiguration中创建ErrorController的Bean时使用了@ConditionalOnMissBean注解,因此我们可以自定义一个ErrorController来覆盖默认的BasicErrorController。通常的做法是实现ErrorController接口来自定义错误映射处理。具体实现时可参考AbstractErrorController和BasicErrorController。

当服务器处理请求失败后,底层会将请求默认转发到/error映射中,因此我们必须提供一个处理/error请求映射的方法来保证对错误的处理。

在前后端分离项目中,前端与后端的交互通常是通过json字符串进行的。当服务器请求处理异常时,我们不能返回一个Whitelabel Error Page的HTML页面,而是返回一个友好的、统一的json字符串。为了实现这个目的,我们必须覆盖BasicErrorController来实现在错误时的自定义数据返回。

// 统一响应类@AllArgsConstructor@Datapublic static class Response {    private Integer code;    private String message;    private T data;}
// 自定义的ErrorController参考BasicErrorController、AbstractErrorController实现@RestController@RequestMapping("${server.error.path:${error.path:/error}}")@RequiredArgsConstructor@Slf4jpublic static class MyErrorController implements ErrorController {    private final DefaultErrorAttributes defaultErrorAttributes;    @Override    public String getErrorPath() {        // 忽略        return null;    }    @GetMapping    public Response error(HttpServletRequest httpServletRequest) {        // 获取默认的错误信息并打印异常日志        log.warn(String.valueOf(errorAttributes(httpServletRequest)));        // 返回统一响应类        return new Response<>(-1, "error", null);    }    private Map 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)        );    }}

关键词: 进行处理 自定义异常 字段信息