今天終於把 boot 的異常處理完全研究透了:
boot提供了很多錯誤的處理工作。默認情況下,我們會看到一個whiteLabel(白標)的頁面。 這個可能不是我們所需。因此我們需要定制。我於是做了個深入的研究。
boot 的錯誤,入口,顯然是ErrorMvcAutoConfiguration。 它在WebMvcAutoConfiguration 配置之前完成 : @AutoConfigureBefore(WebMvcAutoConfiguration.class) 在ErrorMvcAutoConfiguration中, 還注冊了很多的 error 相關bean:
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); // 注冊了一個 專門收集 error 發生時錯誤信息的bean DefaultErrorAttributes 實現了HandlerExceptionResolver, 通過對異常的處理, 填充 錯誤屬性 ErrorAttributes 。 這個是boot 中的 controller 出現異常的時候, 會使用到的 public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { } @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), // 注冊 BasicErrorController。 BasicErrorController 完成對所有 controller 發生異常情況的處理, 包括 異常和 4xx, 5xx 子類的 。 this.errorViewResolvers); } @Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); //注冊 錯誤頁面的 定制器。 后面會再次討論這個 Customizer } @Bean public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() { return new PreserveErrorControllerTargetClassPostProcessor(); // 這個有些難懂, 略去 } DefaultErrorViewResolverConfiguration @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, // DefaultErrorViewResolver 是什么? 它提供了對err ViewResolver 處理的默認支持, 基本上就是說 它會返回一個 error view this.resourceProperties); // DefaultErrorViewResolver 會作為 ErrorViewResolver 注入到 ErrorMvcAutoConfiguration 的構造中去。 而 errorViewResolvers 其實就是直接 交給了 BasicErrorController。 也就是說, BasicErrorController 處理錯誤的時候, 會使用 DefaultErrorViewResolver 提供的內容來進行 頁面渲染。 } 說說 DefaultErrorViewResolver, 它是一個純 boot 的內容。 它的處理方式是比較古怪的。它專門處理發生 error時候的 view 你給我一個error view, 我 就先去 error/ 目錄下面去找 error/ + viewName + .html 的文件(這里的viewName通常是 404 ,500 之類的 錯誤的response status code), 找到了 就直接展示(渲染)它。 否則就 嘗試去匹配4xx, 5xx,然后去找error/4xx.html或者 error/5xx.html 兩個頁面,找到了就展示它。 BasicErrorController 就是一個 Controller: @Controller @RequestMapping("${server.error.path:${error.path:/error}}") // 這個我們都看得懂吧!!! public class BasicErrorController extends AbstractErrorController { 關鍵是其中的兩個方法: @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); // 這里的 model 是相關錯誤信息 response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); // 這個完成了具體的 處理過程 return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); // 如果找不到, 那還是返回一個 new ModelAndView("error", model) 吧
} @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); // 這里相對上面的方法,簡單很多, 它不會去使用 viewResolver 去處理, 因為它不需要任何的 view ,而是直接返回 text 格式數據, 而不是 html 格式數據 return new ResponseEntity<Map<String, Object>>(body, status); } resolveErrorView 方法是 AbstractErrorController 提供的: protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { // 正是這里 用到了之前 的 errorViewResolvers ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; }
上面的 resolver 是之前注冊的 DefaultErrorViewResolver, 其resolveErrorView 方法是:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status), model);
if(modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null?new ModelAndView(errorViewName, model):this.resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations(); // 靜態的 location 包括 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location); //
resource = resource.createRelative(viewName + ".html");
if(resource.exists()) { // 資源必須要存在, 才會返回,
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
;
}
}
return null; // 如果 各個靜態目錄下都沒有找到那個 html 文件, 那么就還是 返回null, 交給白標吧 !!
}
默認情況下, 我們的靜態location 也不會有 什么404.html 之類的 錯誤展示的文件,因為我們不知道啊。。
那么, boot 也只有使用 白標了: @Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean(BeanNameViewResolver.class) public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); // 匹配 順序是 。。。 return resolver; } } WhitelabelErrorViewConfiguration 注冊了 一個View, 同時 注冊了BeanNameViewResolver,如果之前沒有注冊的話。 這樣做的意義呢? 別忘了 BeanNameViewResolver 也是可以對View 進行處理的, 它的處理方式是 根據 view 的name 查找 對應的bean。 這里 defaultErrorView 也是一個bean, 其名字是 error。 整個意思就是說, 如果 發現請求是 /error , 那么 如果其他 ViewResolver 處理不了, 那么我來處理吧。 我這么處理呢? 我就 把 SpelView 渲染到 瀏覽器吧。 所以,我們可以看到, WhitelabelErrorView 通常是異常處理的最后一個 圍牆, 因為 BeanNameViewResolver 的優先級比較低 SpelView 實現了 View , 主要就是完成了對 頁面的渲染, 提供了一個 render 方法。 ErrorPageCustomizer 非非非常常常關鍵!!!!!!! : private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath()); // 正是這里, 把 /error 這樣的errorpage 注冊到了 servlet 容器, 使得它異常的時候, 會轉發到/error errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } } 雖然,我們現在已經配置了 BasicErrorController, 沒錯,它默認會對 /error請求 進行處理。但是 springMVC 可沒說404,4xx或500,5xx等系統異常就 轉發請求給 /error 吧, springMVC是通過HandlerExceptionResolver 來處理異常的, 而且只處理異常, 不處理 404 之類的。 那么這個工作是誰完成的呢? 沒錯, 應該就是 boot 了吧! 但是, 具體呢? registerErrorPages 方法就是關鍵。 ErrorPage 沒什么特別的,可以看做是一個簡單的 javabean: private final HttpStatus status; private final Class<? extends Throwable> exception; private final String path; 僅僅是包含3個屬性的 bean 而已。 關鍵是errorPageRegistry.addErrorPages(errorPage); errorPageRegistry 是關鍵,但是它是一個參數,是誰調用這個方法的呢? 答案在 EmbeddedServletContainerAutoConfiguration 之中。 它 為 tomcat, jetty, undertow 分別配置了 EmbeddedServletContainerFactory, 然后 : @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (this.beanFactory == null) { return; } registerSyntheticBeanIfMissing(registry, "embeddedServletContainerCustomizerBeanPostProcessor", EmbeddedServletContainerCustomizerBeanPostProcessor.class); registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class); } 其中ErrorPageRegistrarBeanPostProcessor 完成了對 errorPageRegistry 的處理 : public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if(bean instanceof ErrorPageRegistry) { // ErrorPageCustomizer 是一個 ErrorPageRegistry, 因為這里會攔截到之前注冊的 ErrorPageCustomizer bean this.postProcessBeforeInitialization((ErrorPageRegistry)bean); } return bean; } private void postProcessBeforeInitialization(ErrorPageRegistry registry) { Iterator var2 = this.getRegistrars().iterator(); while(var2.hasNext()) { ErrorPageRegistrar registrar = (ErrorPageRegistrar)var2.next(); // ErrorPageRegistrar 又是什么? ErrorPageCustomizer 正是它的實現!! registrar.registerErrorPages(registry); // 這里調用之前ErrorPageCustomizer的 方法的具體實現。 } } private Collection<ErrorPageRegistrar> getRegistrars() { if(this.registrars == null) { this.registrars = new ArrayList(this.beanFactory.getBeansOfType(ErrorPageRegistrar.class, false, false).values()); Collections.sort(this.registrars, AnnotationAwareOrderComparator.INSTANCE); this.registrars = Collections.unmodifiableList(this.registrars); } return this.registrars; } 對於EmbeddedServletContainerCustomizerBeanPostProcessor, 其實它是boot提供的另外一種方式的 錯誤處理 。 顧名思義, 它就是對內嵌 的 servlet 容器的 定制器: public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if(bean instanceof ConfigurableEmbeddedServletContainer) { // 一定要注意 ConfigurableEmbeddedServletContainer 是什么?ConfigurableEmbeddedServletContainer就是 J2EE容器,另外它是 ErrorPageRegistry 的子接口 this.postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer)bean); } return bean; } private void postProcessBeforeInitialization(ConfigurableEmbeddedServletContainer bean) { Iterator var2 = this.getCustomizers().iterator(); while(var2.hasNext()) { EmbeddedServletContainerCustomizer customizer = (EmbeddedServletContainerCustomizer)var2.next(); // 強轉 customizer.customize(bean); // } } private Collection<EmbeddedServletContainerCustomizer> getCustomizers() { if(this.customizers == null) { this.customizers = new ArrayList(this.beanFactory.getBeansOfType(EmbeddedServletContainerCustomizer.class, false, false).values()); Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE); this.customizers = Collections.unmodifiableList(this.customizers); } return this.customizers; } 所以來說, 這里也真是夠繞的了。 EmbeddedServletContainerCustomizer 是什么鬼? 它是一個接口,提供了方法: public interface EmbeddedServletContainerCustomizer { void customize(ConfigurableEmbeddedServletContainer var1); // 定制器,定制什么呢? 答案是 對 容器進行定制。 故這里需要一個 可config 的內嵌servlet 容器 } 因為 ConfigurableEmbeddedServletContainer 是很強大的, 故 customize 方法也變得強大了: public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry { void setContextPath(String var1); void setDisplayName(String var1); void setPort(int var1); void setSessionTimeout(int var1); void setSessionTimeout(int var1, TimeUnit var2); void setPersistSession(boolean var1); void setSessionStoreDir(File var1); void setAddress(InetAddress var1); void setRegisterDefaultServlet(boolean var1); void setErrorPages(Set<? extends ErrorPage> var1); void setMimeMappings(MimeMappings var1); void setDocumentRoot(File var1); void setInitializers(List<? extends ServletContextInitializer> var1); void addInitializers(ServletContextInitializer... var1); void setSsl(Ssl var1); void setSslStoreProvider(SslStoreProvider var1); void setJspServlet(JspServlet var1); void setCompression(Compression var1); void setServerHeader(String var1); void setLocaleCharsetMappings(Map<Locale, Charset> var1); } 於是,我們可以利用 EmbeddedServletContainerCustomizer, 然后間接利用 ConfigurableEmbeddedServletContainer , 做各種定制化。 另外 它還實現了ErrorPageRegistry, 於是,我們可以利用它 進行異常頁面的處理: @Configuration public class ErrorPageConfig implements EmbeddedServletContainerCustomizer { @Override public void customize(ConfigurableEmbeddedServletContainer container) { container.addErrorPages( new ErrorPage(HttpStatus.BAD_REQUEST, "/4O0.html"), new ErrorPage(HttpStatus.UNAUTHORIZED, "/4O1.html"), new ErrorPage(HttpStatus.NOT_FOUND, "/404/"), new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html") ); } } // 參考 http://blog.csdn.net/devday/article/details/60143966 這真的是太TM 靈活了! addErrorPages 是container 完成的, 但是這里的 container 僅僅還是一個 boot 的內容, 沒有實質性的 servlet容器的東西。 那么 /error 請求到底是如何被 servlet容器 處理的呢? addErrorPages 是由ConfigurableEmbeddedServletContainer的子類 AbstractConfigurableEmbeddedServletContainer 實現的, 它提供了 errorPages 屬性,關鍵的一個 getErrorPages方法。 然而它仍然只是boot 的范疇。 我相信它沒有實質作用。 注意到,前文已經提到,boot 其實已經 提供了3個 ConfigurableEmbeddedServletContainer的實現, tomcat是 TomcatEmbeddedServletContainerFactory 。 這里,真正起作用的是 TomcatEmbeddedServletContainerFactory 。 它在 configureContext (也就是初始化容器, 做配置的時候) 時, 調用getErrorPages方法。 然后: var4 = this.getErrorPages().iterator(); while(var4.hasNext()) { ErrorPage errorPage = (ErrorPage)var4.next(); (new TomcatErrorPage(errorPage)).addToContext(context); // 看到沒, 這里又是一個反向調用。 正是這里,完成了 將 errorPage 交給 servlet容器。 注意, 顯然, 這里的 context 就是 servlet容器吧! } public void addToContext(Context context) { Assert.state(this.nativePage != null, "Neither Tomcat 7 nor 8 detected so no native error page exists"); if(ClassUtils.isPresent("org.apache.tomcat.util.descriptor.web.ErrorPage", (ClassLoader)null)) { org.apache.tomcat.util.descriptor.web.ErrorPage errorPage = (org.apache.tomcat.util.descriptor.web.ErrorPage)this.nativePage; errorPage.setLocation(this.location); errorPage.setErrorCode(this.errorCode); errorPage.setExceptionType(this.exceptionType); context.addErrorPage(errorPage); // Context 其實是提供 addErrorPage的方法的 } else { this.callMethod(this.nativePage, "setLocation", this.location, String.class); this.callMethod(this.nativePage, "setErrorCode", Integer.valueOf(this.errorCode), Integer.TYPE); this.callMethod(this.nativePage, "setExceptionType", this.exceptionType, String.class); this.callMethod(context, "addErrorPage", this.nativePage, this.nativePage.getClass()); } } 這里的 Context 僅僅還是一個接口, 實現應該是 org.apache.catalina.core.StandardContext , 這個, 顯然, 就是純正的 j2ee 的內容的吧 : public void addErrorPage(ErrorPage errorPage) { if(errorPage == null) { throw new IllegalArgumentException(sm.getString("standardContext.errorPage.required")); } else { String location = errorPage.getLocation(); if(location != null && !location.startsWith("/")) { if(!this.isServlet22()) { throw new IllegalArgumentException(sm.getString("standardContext.errorPage.error", new Object[]{location})); } if(log.isDebugEnabled()) { log.debug(sm.getString("standardContext.errorPage.warning", new Object[]{location})); } errorPage.setLocation("/" + location); } String exceptionType = errorPage.getExceptionType(); HashMap var4; if(exceptionType != null) { var4 = this.exceptionPages; synchronized(this.exceptionPages) { this.exceptionPages.put(exceptionType, errorPage); } } else { var4 = this.statusPages; synchronized(this.statusPages) { this.statusPages.put(Integer.valueOf(errorPage.getErrorCode()), errorPage); } } this.fireContainerEvent("addErrorPage", errorPage); } }
它提供了一個 HashMap 的 exceptionPages, 專門存儲 錯誤界面。 那么 這些exceptionPages 具體又是什么時候起作用的呢? 它提供了 findErrorPage 方法 :
public ErrorPage findErrorPage(String exceptionType) { HashMap var2 = this.exceptionPages; synchronized(this.exceptionPages) { return (ErrorPage)this.exceptionPages.get(exceptionType); } } public ErrorPage[] findErrorPages() { ... }
那么, 你一定又會問, findErrorPage 是什么時候被調用的呢? 我通過打斷點進行調試, 終於發現, 原來是在StandardHostValve 這里: public final void invoke(Request request, Response response) throws IOException, ServletException { Context context = request.getContext(); if(context == null) { response.sendError(500, sm.getString("standardHost.noContext")); } else { if(request.isAsyncSupported()) { request.setAsyncSupported(context.getPipeline().isAsyncSupported()); } boolean asyncAtStart = request.isAsync(); boolean asyncDispatching = request.isAsyncDispatching(); try { context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); if(asyncAtStart || context.fireRequestInitEvent(request.getRequest())) { try { if(asyncAtStart && !asyncDispatching) { if(!response.isErrorReportRequired()) { throw new IllegalStateException(sm.getString("standardHost.asyncStateError")); } } else { context.getPipeline().getFirst().invoke(request, response); // 這里調用 valve 進行處理 } } catch (Throwable var10) { ExceptionUtils.handleThrowable(var10); this.container.getLogger().error("Exception Processing " + request.getRequestURI(), var10); if(!response.isErrorReportRequired()) { request.setAttribute("javax.servlet.error.exception", var10); this.throwable(request, response, var10); } } response.setSuspended(false); Throwable t = (Throwable)request.getAttribute("javax.servlet.error.exception"); if(!context.getState().isAvailable()) { return; } if(response.isErrorReportRequired()) { if(t != null) { this.throwable(request, response, t); // 如果有異常, 就嘗試進行異常匯報 } else { this.status(request, response); // 如果不是, 那么就進行status 處理。 這里是關鍵 !!! } } if(!request.isAsync() && !asyncAtStart) { context.fireRequestDestroyEvent(request.getRequest()); } return; } } finally { if(ACCESS_SESSION) { request.getSession(false); } context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); } } } private void status(Request request, Response response) { int statusCode = response.getStatus(); Context context = request.getContext(); if(context != null) { if(response.isError()) {// 如果 statusCode 是 404 等, 那么 response 就是 isError ErrorPage errorPage = context.findErrorPage(statusCode); // 這里就是調用 StandardContext 的 findErrorPage ... if(errorPage == null) { errorPage = context.findErrorPage(0); } if(errorPage != null && response.isErrorReportRequired()) { response.setAppCommitted(false); request.setAttribute("javax.servlet.error.status_code", Integer.valueOf(statusCode)); String message = response.getMessage(); if(message == null) { message = ""; } request.setAttribute("javax.servlet.error.message", message); request.setAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH", errorPage.getLocation()); request.setAttribute("org.apache.catalina.core.DISPATCHER_TYPE", DispatcherType.ERROR); Wrapper wrapper = request.getWrapper(); if(wrapper != null) { request.setAttribute("javax.servlet.error.servlet_name", wrapper.getName()); } request.setAttribute("javax.servlet.error.request_uri", request.getRequestURI()); if(this.custom(request, response, errorPage)) { response.setErrorReported(); try { response.finishResponse(); } catch (ClientAbortException var9) { ; } catch (IOException var10) { this.container.getLogger().warn("Exception Processing " + errorPage, var10); } } } } } } protected void throwable(Request request, Response response, Throwable throwable) { // j2ee 內部的異常處理 Context context = request.getContext(); if(context != null) { Throwable realError = throwable; if(throwable instanceof ServletException) { realError = ((ServletException)throwable).getRootCause(); if(realError == null) { realError = throwable; } } if(realError instanceof ClientAbortException) { if(log.isDebugEnabled()) { log.debug(sm.getString("standardHost.clientAbort", new Object[]{realError.getCause().getMessage()})); } } else { ErrorPage errorPage = findErrorPage(context, throwable); if(errorPage == null && realError != throwable) { errorPage = findErrorPage(context, realError); } if(errorPage != null) { if(response.setErrorReported()) { response.setAppCommitted(false); request.setAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH", errorPage.getLocation()); request.setAttribute("org.apache.catalina.core.DISPATCHER_TYPE", DispatcherType.ERROR); request.setAttribute("javax.servlet.error.status_code", Integer.valueOf(500)); request.setAttribute("javax.servlet.error.message", throwable.getMessage()); request.setAttribute("javax.servlet.error.exception", realError); Wrapper wrapper = request.getWrapper(); if(wrapper != null) { request.setAttribute("javax.servlet.error.servlet_name", wrapper.getName()); } request.setAttribute("javax.servlet.error.request_uri", request.getRequestURI()); request.setAttribute("javax.servlet.error.exception_type", realError.getClass()); if(this.custom(request, response, errorPage)) { try { response.finishResponse(); } catch (IOException var9) { this.container.getLogger().warn("Exception Processing " + errorPage, var9); } } } } else { response.setStatus(500); response.setError(); this.status(request, response); } } } } 運行到 status 方法, 就會 轉發請求給 /error , 又回到了 boot 了! 此時的堆棧是: at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:869) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728) at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:469) at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:392) at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:311) at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:395) at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:254) // 注意這里有個 status 調用棧 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:177) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) - locked <0x195c> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) 那么, 你一定又會問,404 是如何設置給 response 的呢? 我看了下 StandardContextValve, 里面好像有相關內容哦, 但是呢, 答案不是 StandardContextValve, 而是spring web 框架的 ResourceHttpRequestHandler public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Resource resource = this.getResource(request); if(resource == null) { logger.trace("No matching resource found - returning 404"); response.sendError(404); // 這里找不到資源, 於是 404 } else if(HttpMethod.OPTIONS.matches(request.getMethod())) { response.setHeader("Allow", this.getAllowHeader()); } else { this.checkRequest(request); if((new ServletWebRequest(request, response)).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified - returning 304"); } else { this.prepareResponse(response); ... } } 此時的錯誤堆棧是: at org.apache.catalina.connector.Response.sendError(Response.java:1313) at org.apache.catalina.connector.Response.sendError(Response.java:1284) at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:478) at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:129) at com.alibaba.druid.support.http.WebStatFilter$StatHttpServletResponseWrapper.sendError(WebStatFilter.java:342) at org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(ResourceHttpRequestHandler.java:332) at org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle(HttpRequestHandlerAdapter.java:51) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:110) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:106) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) - locked <0x1917> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745)
那么 ResourceHttpRequestHandler ,是何時配置的,或者說何時注冊? 沒找到。 我感覺應該是 WebMvcAutoConfiguration 完成的
請參考 http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html
================================================ END =========================================================
這么大量的源碼, 看完你估計也累了吧,總的來說, boot也真是夠繞的了。