深入理解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 style=\'white-space:pre-wrap;\'>\").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)
);
}
}
来源:https://www.cnblogs.com/kkelin/p/16978260.html
本站部分图文来源于网络,如有侵权请联系删除。