深入理解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 = https://www.cnblogs.com/kkelin/archive/2022/12/13/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的條件下創建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 = https://www.cnblogs.com/kkelin/archive/2022/12/13/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的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.uj5u.com/houduan/539834.html
標籤:其他
上一篇:基于tcp協議的套接字通信
