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