故事发生在那一天:
刚想上床睡觉,室友突然问了一个问题:
“为什么我在资源目录下创建了error.html,使用重定向可以访问到,如果用其他名字就无法访问?”
这个问题把我问住了,既然遇到了疑问,那就上网搜一下,结果。。。没能搜到我想要的答案……
平时学习大部分看的是狂神的视频,可能是看多了,我竟然会想着学狂神自己研究一下底层原理(实在是想不开,咸鱼他不香吗?)
(此处安利一波狂神!B站搜遇见狂神说,wx公众号搜:狂神说)第一次看底层源码,可能讲的有点乱,可以直接拉到最后面看总结
正文开始 |
当我们发送一个错误的请求路径时,会出现下图情况
通过断点调试,发现错误是由 spring boot 的 BasicErrorControlle 的 errorHtml 方法进行处理
@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 类型的只读视图
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);
}
}
很快哦,它先获取请求状态 statusHttpStatus status = getStatus(request);
来都来了就看一下getStatus源码吧
protected HttpStatus getStatus(HttpServletRequest request) {
// 获取状态码
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode == null) {
// statusCode 为空时,返回的是 INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
// statusCode 不为空时,进行判断
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
可以看到,statusCode 非空时,来到枚举类 HttpStatus 的 valueOf() 方法,获取请求状态码
public enum HttpStatus {
/**
* Return the enum constant of this type with the specified numeric value.
* @param statusCode the numeric value of the enum to be returned
* @return the enum constant with the specified numeric value
* @throws IllegalArgumentException if this enum has no constant for the specified numeric value
*/
public static HttpStatus valueOf(int statusCode) {
HttpStatus status = resolve(statusCode);
if (status == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
}
return status;
}
/**
* Resolve the given status code to an {@code HttpStatus}, if possible.
* @param statusCode the HTTP status code (potentially non-standard)
* @return the corresponding {@code HttpStatus}, or {@code null} if not found
* @since 5.0
*/
@Nullable
public static HttpStatus resolve(int statusCode) {
// values() 在 stackoverflow 上查到该方法返回所有枚举常量的数组, 它是隐式定义的(由编译器生成)
for (HttpStatus status : values()) {
if (status.value == statusCode) {
return status;
}
}
return null;
}
}
上面已经获取到请求的状态码了,接下来来到这个
Map<String, Object> model =
// 返回一个 map的只读的视图
Collections.unmodifiableMap(
getErrorAttributes( request, getErrorAttributeOptions(request,MediaType.TEXT_HTML) )
);
我们看看 getErrorAttributes 是干啥的先
/**
* Returns a {@link Map} of the error attributes.
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @return the error attributes
* {@link #getErrorAttributes(HttpServletRequest, ErrorAttributeOptions)}
*/
protected Map<String,Object> getErrorAttributes(
HttpServletRequest request, ErrorAttributeOptions options){
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, options);
}
我们继续一步一步进去看源码,可以发现该方法返回了 错误属性的 map,可以用作错误页面的 model , 或者以 @ResponseBody 返回(源代码注释是这么写的or returned as a {@link ResponseBody @ResponseBody}.
)。
继续查看 Collections.unmodifiableMap 的源码
public class Collections {
/**
* Returns an unmodifiable view of the specified map. This method
* allows modules to provide users with "read-only" access to internal
* maps. Query operations on the returned map "read through"
* to the specified map, and attempts to modify the returned
* map, whether direct or via its collection views, result in an
* <tt>UnsupportedOperationException</tt>.<p>
*
*
* The returned map will be serializable if the specified map
* is serializable.
*
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map for which an unmodifiable view is to be returned.
* @return an unmodifiable view of the specified map.
*/
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
return new UnmodifiableMap<>(m);
}
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
private static final long serialVersionUID = -1034234728574286014L;
private final Map<? extends K, ? extends V> m;
UnmodifiableMap(Map<? extends K, ? extends V> m) {
if (m==null)
throw new NullPointerException();
this.m = m;
}
}
}
看注释我们可以知道拿到了一个map的只读视图。但是,这个map到底是装了啥?看注释也是一脸懵逼,不过利用 Debugger 可以看到
原来这个map装了时间戳(timestamp),请求的状态(status),错误信息(error),请求报文(message),请求的相对路径(path),看到这个是不是有点头绪了?没有的话继续往下看
接下来是设置响应的 status response.setStatus(status.value());
到这里,我们已经有了 请求(request),响应(response),状态码(status),map的只读视图(model),继续走~
/**
* Resolve any specific error views. By default this method delegates to
* {@link ErrorViewResolver ErrorViewResolvers}.
* @param request the request
* @param response the response
* @param status the HTTP status
* @param model the suggested model
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
* @since 1.4.0
*/
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response,
HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 解析指定详细信息的错误页面
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
进来 spring 写的默认的错误视图解析器 (DefaultErrorViewResolver)康康
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 视图解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
// 获取可用于渲染给定视图的 provider(这里不知道应该翻译为啥,翻译为提供程序感觉太奇怪了)。
TemplateAvailabilityProvider provider = this
.templateAvailabilityProviders
.getProvider(errorViewName,this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
看到上面这行代码了吗? return new ModelAndView(errorViewName, model);
再看看 ModelAndView 的构造方法
public ModelAndView(String viewName, @Nullable Map<String, ?> model) {
// 跳转的视图名称
this.view = viewName;
// 用 model 封装发送到前端的数据
if (model != null) {
getModelMap().addAllAttributes(model);
}
}
所以,前面的 map 类型的只读视图的作用懂了吧。还不明白我们来看这个图
咱们看一下 BasicErrorControlle 的 errorHtml 方法的返回
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
如果 modelAndView 为空时,spring 创建了一个新的modelAndView,可以看到 new ModelAndView(“error”, model);
视图名称是 error
发现问题了吗?这个新创建的错误视图的名称跟前面的不一样,前面的 errorViewName 是"error/" + viewName;
,
这后者的 errorViewName 是 error
,对比发现,后者少了一个目录,也就是我们在 Templates 下的 error.html (如果你有创建的话)
问题来了,什么时候它会是空的呢?咱们继续看DefaultErrorViewResolver
源码
// 这个方法中主要是解析错误请求路径时跳转的视图和请求内容
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 视图解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
// 这个方法主要是通过 errorViewName 来判断要跳转的视图是否存在,存在则返回一个对象,否则返回空
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
// 获取可用于渲染给定视图的 provider(这里不知道应该翻译为啥,机器翻译为提供程序,感觉太奇怪了)。
TemplateAvailabilityProvider provider = this
.templateAvailabilityProviders
.getProvider(errorViewName,this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
我们先来看一下 provider 的查找策略
// 获取渲染给定视图的 provider
/**
* Get the provider that can be used to render the given view.
* @param view the view to render
* @param environment the environment
* @param classLoader the class loader
* @param resourceLoader the resource loader
* @return a {@link TemplateAvailabilityProvider} or null
*/
public TemplateAvailabilityProvider getProvider(String view, Environment environment,
ClassLoader classLoader,ResourceLoader resourceLoader) {
Assert.notNull(view, "View must not be null");
Assert.notNull(environment, "Environment must not be null");
Assert.notNull(classLoader, "ClassLoader must not be null");
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
// 获取 spring.template.provider.cache 的值( 热部署使用 ,默认值是false)
// Map from view name resolve template view, synchronized when accessed.
Boolean useCache = environment.getProperty("spring.template.provider.cache", Boolean.class, true);
if (!useCache) {
// 使用热部署的查找策略
return findProvider(view, environment, classLoader, resourceLoader);
}
// 未开启热部署,通过视图名查找对应的 provider
TemplateAvailabilityProvider provider = this.resolved.get(view);
// 如果不存在,采用热部署的查找策略
if (provider == null) {
synchronized (this.cache) {
provider = findProvider(view, environment, classLoader, resourceLoader);
provider = (provider != null) ? provider : NONE;
this.resolved.put(view, provider);
this.cache.put(view, provider);
}
}
// provider 如果不是NoTemplateAvailabilityProvider(),则说明热部署查找策略成功找到
return (provider != NONE) ? provider : null;
}
// 热部署开启时的查找策略如下
private TemplateAvailabilityProvider findProvider(String view, Environment environment,
ClassLoader classLoader,ResourceLoader resourceLoader) {
for (TemplateAvailabilityProvider candidate : this.providers) {
// 查找是否存在
if (candidate.isTemplateAvailable(view, environment, classLoader, resourceLoader)) {
return candidate;
}
}
return null;
}
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader,ResourceLoader resourceLoader) {
// 是否存在 SpringTemplateEngine
if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine", classLoader)) {
String prefix = environment
.getProperty("spring.thymeleaf.prefix", ThymeleafProperties.DEFAULT_PREFIX);
String suffix = environment
.getProperty("spring.thymeleaf.suffix", ThymeleafProperties.DEFAULT_SUFFIX);
// 拼接视图名称并在资源目录下查找
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
由上述分析可知,
① 当我们开启热部署的时候,spring会优先使用 findProvider() 方法去查找要跳转的视图
② 如果我们没有开启热部署,spring会从 Map<String, TemplateAvailabilityProvider> 对象里面去查找是否存在 key 值为 view 的 provider,当查找不到时,使用热部署的查找策略,依旧查找不到时,将其赋为 new NoTemplateAvailabilityProvider();并在TemplateAvailabilityProviders 中的 List providers 和 Map<String, TemplateAvailabilityProvider> cache 中保存 key 值 为 view 的 provider
最后进行判断,provider 是否是 NoTemplateAvailabilityProvider() 类型,或者说是 TemplateAvailabilityProviders 的 NONE 属性的值,如果是,说明热部署查找策略成功找到,此时执行如下代码
if (provider != null) {
// 返回存有找到的视图和请求内容的 ModelAndView 对象
return new ModelAndView(errorViewName, model);
}
否则,执行
return resolveResource(errorViewName, model);
看 resolveResources 代码
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
// 调用 ResourceLoader 方法
Resource resource = this.applicationContext.getResource(location);
// 调用 ClassPathResource 方法
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
this.resourceProperties.getStaticLocations() 这里面存放的是静态资源目录,如下图
spring 在确定完路径后,会创建相对此资源的资源,resource = resource.createRelative(viewName + ".html");
@Override
public Resource createRelative(String relativePath) {
// 将相对路径应用到给定的Java资源路径,文件夹以分隔符“/”分离
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
// 生成资源
return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) :
new ClassPathResource(pathToUse, this.classLoader));
}
// ClassPathResource 的构造方法
// this.clazz : the class to load resources with, if any
// this.classLoader : the class loader to load the resource with, if any
// 该类主要是实现类路径资源的资源,使用指定的类加载器或给定的类去加载资源
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
生成的资源如下:
(其他静态资源目录类似,此处不再展示)
静态资源创建好后就进行判断该资源是否存在,resource.exists()
,实际上调用的是 ClassPathResource 的 resolveURL() 方法
// ClassPathResource
// 此方法实现检查资源URL的解析。
@Override
public boolean exists() {
return (resolveURL() != null);
}
// 解析底层类路径资源的URL。
@Nullable
protected URL resolveURL() {
if (this.clazz != null) {
// 通过当前类的getResource方法查找,该方法通过指定的name去查找
return this.clazz.getResource(this.path);
}
else if (this.classLoader != null) {
// 通过要加载的类加载器去查找资源
return this.classLoader.getResource(this.path);
}
else {
// 从用于装入类的搜索路径中查找指定名称的资源。
// 这个方法通过系统类装入器(参见{@link #getSystemClassLoader()})来定位资源。
return ClassLoader.getSystemResource(this.path);
}
}
// Class.java
/*
Finds a resource with a given name.
The rules for searching resources associated with a given class are implemented by the defining
{@linkplain ClassLoader class loader} of the class.
This method delegates to this object's class loader.
If this object was loaded by the bootstrap class loader, the method delegates to
{@link ClassLoader#getSystemResource}.
*/
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
// ClassLoader
// 通过给定的名字查找资源路径
// parent 是类加载器 用于委托的父类装入器
public URL getResource(String name) {
URL url;
if (parent != null) {
// 父类的类加载器存在
url = parent.getResource(name);
} else {
// Find resources from the VM's built-in classloader.
// 从 Java 虚拟机的内置类加载器查找资源
url = getBootstrapResource(name);
}
if (url == null) {
// 查找资源,方法返回的是null
url = findResource(name);
}
return url;
}
由上述代码可以知道,spring会通过类和类加载器的特定方法(如 getResource() )去查找静态资源,如果存在则返回url,否则返回空。
当以 viewName + ".html"
创建的资源存在时,则 return new ModelAndView(new HtmlResourceView(resource), model);
否则返回 return null;
回过头来看 resolveErrorView
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response,
HttpStatus status,Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 此处的返回结果就是上面分析的结果
// 当资源不存在时,返回空
// 存在时,返回对应目录下的资源和响应内容的 ModelAndView 对象
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
终于来到了最后这一步
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
如果查找的静态资源存在,则返回对应的 ModelAndView 对象,否则直接返回 new ModelAndView("error", model);
后者在前面讲过了,此处不再重复讲述。重点看前者,它的路径到底可以是啥?
还记得这个图吗?
还有这个
errorViewName = "error/" + viewName;
,viewName 也就是请求的状态码(如 404,500 ……)
所以呢,完整的路径应该是这个
classpath:/META-INF/resources/error/viewName
classpath:/resources/error/viewName (如果用了 thyme leaf模板引擎,resources 相当于 templates ,因为我们修改了静态资源的访问路径)
classpath:/static/error/viewName
classpath:/public/error/viewName
到这里,我们的分析就结束了~~ |
总结 |
简而言之,当请求路径出现错误时,spring 的 BasicErrorController 会进行处理,最后跳转的页面的路径有以下情况
classpath:/META-INF/resources/error/viewName
classpath:/resources/error/viewName
(如果用了 thymeleaf 模板引擎,resources 相当于 templates ,因为我们修改了静态资源的访问路径spring.thymeleaf.prefix=classpath:/templates/)
classpath:/static/error/viewName(.html)
classpath:/public/error/viewName(.html)
classpath:/error(.html)
viewName的命名需要按照官方给的命名标准(如果没有自定义错误解析的话)
viewName 需换为 4xx / 5xx
比如 404 500