Spring Boot 静态资源访问原理解析
一、前言
springboot 配置静态资源方式是多种多样,接下来我会介绍其中几种方式,并解析一下其中的原理。
二、使用 properties 属性进行配置
应该说 spring.mvc.static-path-pattern 和 spring.resources.static-locations 这两属性是成对使用的,如果不明白其中的原理,总会出现资源 404 的情况。首先收一下 spring.mvc.static-path-pattern 代表的是一个 Ant Path 路径,例如 resources/**,表示当你的路径中存在 resources/** 的时候才会处理请求。比如我们访问“http://localhost:8080/resources/xxx.js”时,很显然,springboot 逻辑中会根据模式匹配对 url 进行匹配,匹配命中后,是如何再定位到具体的资源的呢?这时候 spring.resources.static-locations 的配置就起作用了。
忘记说了,在 springboot 中 spring.mvc.static-path-pattern 的默认值是 /**,spring.resources.static-locations 的默认值是 classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources,servlet context:/,springboot 中相关的ResourceHttpRequestHandler 就会去 spring.resources.static-locations 配置的所有路径中寻找资源文件。
所以我之前才说 spring.mvc.static-path-pattern 和 spring.resources.static-locations 这两属性是成对使用的。
三、springboot 中默认对静态资源的处理
调试过程中,通过查看 org.springframework.web.servlet.DispatcherServlet 中的 handlerMappings 变量,我们发现有一个很显眼的 resourceHandlerMapping ,这个是 springboot 为我们提供的一个默认的静态资源 handler,通过全文搜索发现出现在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport 这个类中,也就是这个类包含了 @EnableWebMvc 注解中的大多数功能,更多的扩展功能请参考 org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration。
resourceHandlerMapping 的定义如下。
/** * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped * resource handlers. To configure resource handling, override * {@link #addResourceHandlers}. */ @Bean public HandlerMapping resourceHandlerMapping() { ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, this.servletContext, mvcContentNegotiationManager()); addResourceHandlers(registry);AbstractHandlerMapping handlerMapping </span>=<span style="color: rgba(0, 0, 0, 1)"> registry.getHandlerMapping(); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (handlerMapping != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { handlerMapping.setPathMatcher(mvcPathMatcher()); handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider())); handlerMapping.setCorsConfigurations(getCorsConfigurations()); } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { handlerMapping </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> EmptyHandlerMapping(); } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> handlerMapping;
}
请大家先记住 ResourceHandlerRegistry 这个类。
首先看一下 addResourceHandlers(registry); 这个方法,父类 DelegatingWebMvcConfiguration 做了实现,如下。
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); }
其中 WebMvcConfigurerComposite 是操作了 WebMvcConfigurer 类型的对象的集合。在 org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 这个 springmvc 的自动配置类中,有一个 WebMvcConfigurer 的实现类,如下。
// Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when not // on the classpath @Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { ...@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> addResourceHandlers(ResourceHandlerRegistry registry) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.resourceProperties.isAddMappings()) { logger.debug(</span>"Default resource handling disabled"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">; } Integer cachePeriod </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.resourceProperties.getCachePeriod(); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!registry.hasMappingForPattern("/webjars/**"<span style="color: rgba(0, 0, 0, 1)">)) { customizeResourceHandlerRegistration( registry.addResourceHandler(</span>"/webjars/**"<span style="color: rgba(0, 0, 0, 1)">) .addResourceLocations( </span>"classpath:/META-INF/resources/webjars/"<span style="color: rgba(0, 0, 0, 1)">) .setCachePeriod(cachePeriod)); } <strong> <span style="color: rgba(255, 0, 0, 1)">String staticPathPattern </span></strong></span><strong><span style="color: rgba(255, 0, 0, 1)">= this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration( registry.addResourceHandler(staticPathPattern) .addResourceLocations( this</span></strong><span style="color: rgba(0, 0, 0, 1)"><strong><span style="color: rgba(255, 0, 0, 1)">.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod));</span> }</strong> } ...
}
上面的 addResourceHandlers 方法中,增加了默认的 mapping pattern = /webjars/** ,默认的 resource location 是 classpath:/META-INF/resources/webjars/。正是这里的配置,我们在集成 swagger 的时候,就可以正常访问到 swagger webjars 中的 js 文件了。其中红色的代码部分就是用户可以自定义的默认静态资源访问方式,并通过 ResourceHandlerRegistry 对象进行注册。接着看一下 mvcProperties 和 resourceProperties 对应的类吧。
@ConfigurationProperties("spring.mvc") public class WebMvcProperties { ...</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Path pattern used for static resources. </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> String staticPathPattern = "/**"<span style="color: rgba(0, 0, 0, 1)">; ...
}
WebMvcProperties 类中的 staticPathPattern field 对应了 spring.mvc.static-path-pattern 这个属性,可以看到默认值是 "/**"。
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) public class ResourceProperties implements ResourceLoaderAware {..... </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String[] SERVLET_RESOURCE_LOCATIONS = { "/"<span style="color: rgba(0, 0, 0, 1)"> }; </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String[] CLASSPATH_RESOURCE_LOCATIONS =<span style="color: rgba(0, 0, 0, 1)"> { </span>"classpath:/META-INF/resources/", "classpath:/resources/"<span style="color: rgba(0, 0, 0, 1)">, </span>"classpath:/static/", "classpath:/public/"<span style="color: rgba(0, 0, 0, 1)"> }; </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span><span style="color: rgba(0, 0, 0, 1)"> String[] RESOURCE_LOCATIONS; </span><span style="color: rgba(0, 0, 255, 1)">static</span><span style="color: rgba(0, 0, 0, 1)"> { RESOURCE_LOCATIONS </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> String[CLASSPATH_RESOURCE_LOCATIONS.length </span>+<span style="color: rgba(0, 0, 0, 1)"> SERVLET_RESOURCE_LOCATIONS.length]; System.arraycopy(SERVLET_RESOURCE_LOCATIONS, </span>0, RESOURCE_LOCATIONS, 0<span style="color: rgba(0, 0, 0, 1)">, SERVLET_RESOURCE_LOCATIONS.length); System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, </span>0<span style="color: rgba(0, 0, 0, 1)">, RESOURCE_LOCATIONS, SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); } </span><span style="color: rgba(0, 0, 255, 1)">private</span> String[] staticLocations =<span style="color: rgba(0, 0, 0, 1)"> RESOURCE_LOCATIONS; ......
}
ResourceProperties 中 staticLocations field 对应了 spring.resources.static-locations 这个属性。可以看到默认值是 classpath:[/META-INF/resources/, /resources/, /static/, /public/], servlet context:/
四、静态资源的 Bean 配置
在了解了 springboot 默认资源的配置的原理(即 spring.mvc.static-path-pattern 和 spring.resources.static-locations),我们可以增加一个 WebMvcConfigurer 类型的 bean,来添加静态资源的访问方式,还记得上面说的“请记住 ResourceHandlerRegistry 这个类“,下面就用到了哦。
@Configuration public class ResourceWebMvcConfigurer extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") .addResourceLocations("classpath:/public-resources/") .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()); } }
那么当访问路径中包含 "resources/**" 的时候,resource handler 就会去 classpath:/public-resources 目录下寻找了。
五、静态资源的查找
参考 org.springframework.web.servlet.resource.ResourceHttpRequestHandler,ResourceHttpRequestHandler 中通过 org.springframework.web.servlet.resource.PathResourceResolver 进行查找。
举个例子,下图是 springboot 打包之后的目录结构,现在想要通过 url 访问 application.properties 文件,springboot 默认的静态文件配置可以吗?当然需要用事实来说话了。
我们已经知道,默认的 resource locations 中有个 servlet-context:/,访问你的 url 是http://localhost:8080/工程名 /application.properties,调试一下 PathResourceResolver,结果如下。
发现 servlet-context 的根路径如上图所示,查看一下这个路径对应的目录,发现什么都没有,所以很显然无法找到我们要找的文件了。毕竟一般使用 springboot 都是 jar 项目,servlet-context path 下没有用户自定义的资源。
六、其他方式
在 Servlet3 协议规范中,包含在 JAR 文件 /META-INFO/resources/ 路径下的资源可以直接访问了。如果将 springboot 项目打包成 war 包,可以配置一个默认的 servlet。在 WebMvcConfigurationSupport 中已经定义好了,不过默认是一个 EmptyHandlerMapping。
/** * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped * default servlet handler. To configure "default" Servlet handling, * override {@link #configureDefaultServletHandling}. */ @Bean public HandlerMapping defaultServletHandlerMapping() { DefaultServletHandlerConfigurer configurer = new DefaultServletHandlerConfigurer(servletContext); configureDefaultServletHandling(configurer); AbstractHandlerMapping handlerMapping = configurer.getHandlerMapping(); handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping(); return handlerMapping; }
可以通过自定义一个 WebMvcConfigurer 类型的 bean,改写configureDefaultServletHandling 方法,如下。
@Configuration public class MyWebConfigurer extends WebMvcConfigurerAdapter { @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable(); } }
这样就设置了一个默认的 servlet,在加载静态资源的时候就会按照 servelt 方式去加载了。
就先分享这么多了,更多分享请关注我们的技术公众号吧!!!