Springboot學習04-默認錯誤頁面加載機制源碼分析
前沿
希望通過本文的學習,對錯誤頁面的加載機制有這更神的理解
正文
1-Springboot錯誤頁面展示
2-Springboot默認錯誤處理邏輯
1-將請求轉發到BasicErrorController控制器來處理請求,
2-瀏覽器請求響應BasicErrorController的errorHtml()方法,APP等客戶端響應error()方法
3-以瀏覽器的404錯為例:最終返回一個modelAndView
3-1-調用BasicErrorController的errorHtml(HttpServletRequest request, HttpServletResponse response)方法,其中status=404;//詳見源碼L-134
3-2-調用AbstractErrorController的resolveErrorView方法,遍歷ErrorMvcAutoConfiguration.errorViewResolvers,尋找需要的modelAndView;//詳見源碼L-142;162
3-3-ErrorMvcAutoConfiguration.errorViewResolvers會有一個默認的DefaultErrorViewResolver,於是便執行DefaultErrorViewResolver.resolveErrorView()方法;//詳見源碼L-171;190
3-4-DefaultErrorViewResolver.resolveErrorView()的具體實現:調用當前的this.resolve(status, model),創建modelAndView;//即尋找error/404頁面 //詳見源碼L-191;199
3-5-如果創建error/404視圖失敗(即找不到error/404視圖),則創建error/4XX視圖;否則,繼續創建視圖;//詳見源碼L-192;193
3-6-如果創建error/4XX視圖失敗(即找不到error/4XX視圖),則創建默認名為error的視圖,而error視圖在靜態累WhitelabelErrorViewConfiguration中進行配置和加載(即Springboot默認的Whitelabel Error Page頁面);//詳見源碼L-144
3-7-根據實際獲取到的視圖,進行渲染
3-源碼分析 1//1-ErrorMvcAutoConfiguration配置類
1 //1-ErrorMvcAutoConfiguration配置類 2 package org.springframework.boot.autoconfigure.web.servlet.error; 3 @Configuration 4 @AutoConfigureBefore({WebMvcAutoConfiguration.class})//在WebMvcAutoConfiguration 配置之前完成peizhi 5 public class ErrorMvcAutoConfiguration { 6 7 //注冊了一個 專門收集 error 發生時錯誤信息的bean 8 //DefaultErrorAttributes 實現了HandlerExceptionResolver, 通過對異常的處理, 填充 錯誤屬性 ErrorAttributes 。 這個是boot 中的 controller 出現異常的時候會使用到的 9 @Bean 10 @ConditionalOnMissingBean( 11 value = {ErrorAttributes.class}, 12 search = SearchStrategy.CURRENT 13 ) 14 public DefaultErrorAttributes errorAttributes() { 15 return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); 16 } 17 18 //注冊 BasicErrorController。 BasicErrorController 完成對所有 controller 發生異常情況的處理, 包括 異常和 4xx, 5xx 子類的;處理默認/error請求 19 @Bean 20 @ConditionalOnMissingBean( 21 value = {ErrorController.class}, 22 search = SearchStrategy.CURRENT 23 ) 24 public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { 25 return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); 26 } 27 //注冊 錯誤頁面的 定制器 28 @Bean 29 public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer() { 30 return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath); 31 } 32 33 } 34 35 36 //1-1-ErrorMvcAutoConfigurationde配置類的內部類:WhitelabelErrorViewConfiguration 37 @Configuration 38 @ConditionalOnProperty( 39 prefix = "server.error.whitelabel", 40 name = {"enabled"}, 41 matchIfMissing = true 42 ) 43 @Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class}) 44 protected static class WhitelabelErrorViewConfiguration { 45 //StaticView就是ErrorMvcAutoConfigurationde配置類的內部類:StaticView 46 private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView(); 47 48 protected WhitelabelErrorViewConfiguration() { 49 } 50 51 @Bean(name = {"error"}) 52 @ConditionalOnMissingBean(name = {"error"}) 53 public View defaultErrorView() { 54 return this.defaultErrorView; 55 } 56 57 @Bean 58 @ConditionalOnMissingBean 59 public BeanNameViewResolver beanNameViewResolver() { 60 BeanNameViewResolver resolver = new BeanNameViewResolver(); 61 resolver.setOrder(2147483637); 62 return resolver; 63 } 64 } 65 66 67 //1-2-ErrorMvcAutoConfigurationde配置類的內部類:StaticView 68 //WhitelabelErrorViewConfiguration 邏輯 69 //1-WhitelabelErrorViewConfiguration 注冊了 一個View, 同時 注冊了BeanNameViewResolver,如果之前沒有注冊的話。 70 //2-BeanNameViewResolver 也是可以對View 進行處理的, 它的處理方式是根據 view 的name 查找對應的bean。 這里 defaultErrorView 也是一個bean, 其名字是 error。 即:如果發現請求是 /error, 那么如果其他 ViewResolver 處理不了, 就BeanNameViewResolver 來處理,BeanNameViewResolver 把defaultErrorView 渲染到瀏覽器 71 //3-可以看到, defaultErrorView 通常是異常處理的最后一個圍牆, 因為 BeanNameViewResolver的優先級比較低defaultErrorView(實際就是StaticView)實現了 View , 主要就是完成了對 頁面的渲染, 提供了一個 render 方法。 72 private static class StaticView implements View { 73 private static final Log logger = LogFactory.getLog(ErrorMvcAutoConfiguration.StaticView.class); 74 75 private StaticView() { 76 } 77 78 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 79 if (response.isCommitted()) { 80 String message = this.getMessage(model); 81 logger.error(message); 82 } else { 83 StringBuilder builder = new StringBuilder(); 84 Date timestamp = (Date)model.get("timestamp"); 85 Object message = model.get("message"); 86 Object trace = model.get("trace"); 87 if (response.getContentType() == null) { 88 response.setContentType(this.getContentType()); 89 } 90 91 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(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>"); 92 if (message != null) { 93 builder.append("<div>").append(this.htmlEscape(message)).append("</div>"); 94 } 95 96 if (trace != null) { 97 builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>"); 98 } 99 100 builder.append("</body></html>"); 101 response.getWriter().append(builder.toString()); 102 } 103 } 104 } 105 106 //1-3-ErrorMvcAutoConfigurationde配置類的內部類:ErrorPageCustomizer 107 private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { 108 private final ServerProperties properties; 109 private final DispatcherServletPath dispatcherServletPath; 110 111 protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) { 112 this.properties = properties; 113 this.dispatcherServletPath = dispatcherServletPath; 114 } 115 //把 /error 這樣的errorpage 注冊到了servlet容器,使得它異常的時候,會轉發到/error 116 public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { 117 ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); 118 errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage}); 119 } 120 121 public int getOrder() { 122 return 0; 123 } 124 } 125 126 //2-1-BasicErrorController類 127 package org.springframework.boot.autoconfigure.web.servlet.error; 128 @Controller 129 @RequestMapping({"${server.error.path:${error.path:/error}}"}) 130 public class BasicErrorController extends AbstractErrorController { 131 132 //當請求出現錯誤時,瀏覽器響應 ModelAndView errorHtml 133 @RequestMapping(produces = {"text/html"}) 134 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { 135 //示例:status = 404 NOT_FOUND 136 HttpStatus status = this.getStatus(request); 137 //這里的 model 是相關錯誤信息;示例:model={"timestamp":"Thu Dec 20 09:12:09 CST 2018","status" :"404","error": "Not Found","message": "No message available","path" :"/111"} 138 Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); 139 response.setStatus(status.value()); 140 //這個完成了具體的處理過程;獲取視圖 141 //這里的resolveErrorView 是AbstractErrorController.AbstractErrorController 142 ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); 143 //如果modelAndView=null;則返回 new ModelAndView("error", model); 144 return modelAndView != null ? modelAndView : new ModelAndView("error", model); 145 } 146 147 //這里相對上面的方法,簡單很多,它不會去使用 viewResolver 去處理, 因為它不需要任何的 view ,而是直接返回 text 格式數據,而不是 html 格式數據 148 @RequestMapping 149 public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { 150 Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL)); 151 HttpStatus status = this.getStatus(request); 152 return new ResponseEntity(body, status); 153 } 154 155 } 156 157 158 //2-2-AbstractErrorController抽象類 159 package org.springframework.boot.autoconfigure.web.servlet.error; 160 public abstract class AbstractErrorController implements ErrorController { 161 162 protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { 163 //這個errorViewResolvers 就是ErrorMvcAutoConfiguration.errorViewResolvers 成員變量,errorViewResolvers包含DefaultErrorViewResolver 164 Iterator var5 = this.errorViewResolvers.iterator(); 165 ModelAndView modelAndView; 166 do { 167 if (!var5.hasNext()) { 168 return null; 169 } 170 ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); 171 modelAndView = resolver.resolveErrorView(request, status, model); 172 } while(modelAndView == null); 173 174 return modelAndView; 175 } 176 } 177 178 179 //3-DefaultErrorViewResolver類 180 package org.springframework.boot.autoconfigure.web.servlet.error; 181 // DefaultErrorViewResolver 邏輯 182 //1-DefaultErrorViewResolver 作為 ErrorViewResolver 注入到 ErrorMvcAutoConfiguration 的構造中去;而 errorViewResolvers 其實就是直接 交給了 BasicErrorController。 也就是說, BasicErrorController 處理錯誤的時候,會使用 DefaultErrorViewResolver 提供的內容來進行頁面渲染。 183 //2-DefaultErrorViewResolver是一個純 boot 的內容,專門處理發生 error時候的 view 184 //3-當請求需要一個error view, 就先去 error/ 目錄下面去找 error/ + viewName + .html 的文件(這里的viewName通常是 404 ,500 之類的錯誤的response status code); 找到了 就直接展示(渲染)它。 否則就嘗試去匹配4xx, 5xx,然后去找error/4xx.html或者 error/5xx.html兩個頁面,找到了就展示它。 185 public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { 186 private static final Map<Series, String> SERIES_VIEWS; 187 private ApplicationContext applicationContext; 188 private final ResourceProperties resourceProperties; 189 190 public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { 191 ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); 192 if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { 193 modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model); 194 } 195 196 return modelAndView; 197 } 198 199 private ModelAndView resolve(String viewName, Map<String, Object> model) { 200 //示例:errorViewName = error/404 201 String errorViewName = "error/" + viewName; 202 //示例:從applicationContext中獲取viewName="error/404"的可用模版 203 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); 204 //如果provider=null,則返回this.resolveResource(errorViewName, model) 205 //this.resolveResource<--見下面代碼--> 206 return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); 207 } 208 209 //獲取靜態資源視圖 210 private ModelAndView resolveResource(String viewName, Map<String, Object> model) { 211 // 靜態的 location 包括 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" 212 String[] var3 = this.resourceProperties.getStaticLocations(); 213 int var4 = var3.length; 214 215 for(int var5 = 0; var5 < var4; ++var5) { 216 String location = var3[var5]; 217 218 try { 219 Resource resource = this.applicationContext.getResource(location); 220 resource = resource.createRelative(viewName + ".html"); 221 //資源必須要存在, 才會返回 222 if (resource.exists()) { 223 return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model); 224 } 225 } catch (Exception var8) { 226 ; 227 } 228 } 229 //如果各個靜態目錄下都沒有找到那個html文件,那么就還是 返回null, 交給白標吧 230 return null; 231 } 232 233 234 235 static { 236 Map<Series, String> views = new EnumMap(Series.class); 237 views.put(Series.CLIENT_ERROR, "4xx"); 238 views.put(Series.SERVER_ERROR, "5xx"); 239 SERIES_VIEWS = Collections.unmodifiableMap(views); 240 } 241 242 private static class HtmlResourceView implements View { 243 private Resource resource; 244 245 HtmlResourceView(Resource resource) { 246 this.resource = resource; 247 } 248 249 public String getContentType() { 250 return "text/html"; 251 } 252 253 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 254 response.setContentType(this.getContentType()); 255 FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream()); 256 } 257 } 258 }
參考資料:
1-https://www.cnblogs.com/FlyAway2013/p/7944568.html