【“没有什么是一个断点不能解决的”系列】
一、Spring Boot静态资源配置原理
-
Spring Boot 启动默认加载 XxxxxAutoConfiguration 类(自动配置类)
-
SpringMVC 功能的自动配置类 主要是 WebMvcAutoConfiguration,检查 WebMvcAutoConfiguration 配置类是否生效
// WebMvcAutoConfiguration.class @Configuration( proxyBeanMethods = false ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) @AutoConfigureOrder(-2147483638) @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}) public class WebMvcAutoConfiguration {}
@Configuration 说明这是一个配置类;
@ConditionalOnWebApplication(type = Type.SERVLET) 判断是不是一个 Servlet 应用,我们的 Spring Boot 应用就是一个典型的 Servlet 应用,因此生效;
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) 判断应用里面是否有 Servlet、DispatcherServlet、WebMvcConfigurer 这三个类型场景,Servlet类就是原生的Web开发类,DispatcherServlet、WebMvcConfigurer 都是导了 spring-webmvc-5.3.7.jar 包自然会有的类,因此生效;
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) 判断容器中没有 WebMvcConfigurationSupport 类型的组件时就会生效
-
WebMvcAutoConfiguration 配置类给容器中配了什么?
主要来看 WebMvcAutoConfigurationAdapter 这个内部配置类
// WebMvcAutoConfiguration.class @Configuration(proxyBeanMethods = false) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
注意留意@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
配置文件的相关属性和xxx进行了绑定,分别是 WebMvcProperties (点进去发现与 spring.mvc 进行了绑定)、 ResourceProperties(点进去发现与 spring.resources 进行了绑定)、WebProperties(点进去发现与 spring.web 进行了绑定),@EnableConfigurationProperties 注解帮我们按制定规则绑定好配置文件并且将它们导入容器中。
1、额外注意配置类中只有一个有参构造器的情况
另外注意,WebMvcAutoConfigurationAdapter 这个配置类中只有一个有参构造器,那么这个有参构造器中所有参数的值默认都会从容器中去找,
- ResourceProperties resourceProperties:这个就是上面绑定了配置文件并且@EnableConfigurationProperties开启注入容器中的组件,所以这是获取和 spring.resources 绑定的所有的值的对象;
- WebProperties webProperties:获取和 spring.web绑定的所有的值的对象;
- WebMvcProperties mvcProperties:获取和 spring.mvc绑定的所有的值的对象;
- ListableBeanFactory beanFactory:Spring 的 beanFactory,相当于找 Spring 容器;
- ObjectProvider messageConvertersProvider:HttpMessageConverters,找到系统中所有的 HttpMessageConverters;
- ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider:ResourceHandlerRegistrationCustomizer,相当于找到资源处理器的自定义器;
- ObjectProvider dispatcherServletPath:DispatcherServletPath,相当于找 DispatchServlet 处理的路径;
- ObjectProvider<ServletRegistrationBean<?>> servletRegistrations:ServletRegistrationBean,给应用注册原生的 Servlet、Filter 时用到…
// WebMvcAutoConfiguration.class // public static class WebMvcAutoConfigurationAdapter 配置类中的构造器 public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources()); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; this.mvcProperties.checkConfiguration(); }
2、资源处理的默认规则
然后再来主要关注 addResourceHandlers(ResourceHandlerRegistry registry) 方法,添加资源处理器
// WebMvcAutoConfiguration.class // public static class WebMvcAutoConfigurationAdapter public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { // webjars 的规则 this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); // this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, "/"); registration.addResourceLocations(new Resource[]{resource}); } }); } }
-
首先对 resourceProperties.isAddMappings() 进行判断,也就是配置文件绑定的 spring.resources.add-mappings 属性进行判断,如果该属性为 false 的话,那就进入报异常就不会进入 else 部分执行了,配置也就不再生效。因此我们可以在 application.properties 中对 spring.resources.add-mappings 属性设置为 false,相当于禁用掉所有静态资源的配置规则,就无法访问到静态资源。
-
在 spring.resources.add-mappings 属性值为 true 的默认情况下,进入 else 部分;
-
首先是 “/webjars/**” 请求路径的配置处理规则,写死了默认到 “/META-INF/resources/webjars/” 路径查找;
// WebMvcAutoConfiguration.class // public static class WebMvcAutoConfigurationAdapter // public void addResourceHandlers(ResourceHandlerRegistry registry) this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
访问 /webjars/ 下所有的请求,都会进入到 /META-INF/resources/webjars/ 路径下去查找。而且这个资源还能设置 spring.resources.cache.period 缓存时间,并且会缓存设置的相应时间,即访问过该路径一次之后,会在浏览器中缓存生效。缓存策略封装在 addResourceHandler 了,进入可以查看。
-
再是 静态资源请求路径 的配置规则,访问 spring.mvc.static-path-pattern 属性值的请求路径,默认是 “/**”,到 spring.web.resources.static-locations 属性值所在的路径下查找,默认是"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”;
// WebMvcAutoConfiguration.class // public static class WebMvcAutoConfigurationAdapter // public void addResourceHandlers(ResourceHandlerRegistry registry) this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, "/"); registration.addResourceLocations(new Resource[]{resource}); } });
this.mvcProperties.getStaticPathPattern() 获取到静态资源路径,可以发现 staticPathPattern 是在 WebMvcProperties 中的,而 WebMvcProperties 又是与 配置文件 spring.mvc 绑定的,相当于如果我们配了 spring.mvc.static-path-pattern,那么我们就会获取到对应的值,如果没有配,就是默认值 “/**”,任何访问 / 下的路径请求,就会去到this.resourceProperties.getStaticLocations() 路径下去查找,跟踪进入到 WebProperties 类中的 Resources 类,可以发现底层是配有默认静态路径的,分别是"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”,因此我们访问 / 路径请求静态资源,就会去到上面四个默认静态路径位置下查找。并且也有缓存策略,都封装在了 addResourceHandler 中了。
(相较于Spring Boot 2.3.4版本不同的地方是,去掉了 spring.resources 的 Application Properties 配置属性,替换成了 spring.web.resources)
// WebProperties.class public static class Resources { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"}; private String[] staticLocations; private boolean addMappings; private boolean customized; private final WebProperties.Resources.Chain chain; private final WebProperties.Resources.Cache cache; public Resources() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.customized = false; this.chain = new WebProperties.Resources.Chain(); this.cache = new WebProperties.Resources.Cache(); } public String[] getStaticLocations() { return this.staticLocations; } }
-
以及 欢迎页 的配置规则,
HandlerMapping:是 Spring MVC 中的一个核心组件,翻译为 处理器映射,保存了每一个 Handler 能处理哪些请求。找到各自的哪个请求由各自对应的哪个处理器处理,找到之后再利用反射机制调用能够处理的那个方法。
此处 WelcomePageHandlerMapping 表示处理欢迎页请求的映射规则
// WebMvcAutoConfiguration.class // public static class EnableWebMvcConfiguration @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations()); return welcomePageHandlerMapping; }
同样这里 @Bean 给容器中注入 welcomePageHandlerMapping,这个方法中的传参也一定都会从容器中去获取。
-
此处 this.mvcProperties.getStaticPathPattern() 拿的仍然是配置文件中 静态路径配置属性(spring.mvc.static-path-pattern)的值或者是其默认值"/**";
-
进入到 WelcomePageHandlerMapping 构造器,具体检查其构造过程,
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { if (welcomePage != null && "/**".equals(staticPathPattern)) { // 要使用欢迎页功能,必须是"/**"路径请求 logger.info("Adding welcome page: " + welcomePage); this.setRootViewName("forward:index.html"); } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { // 否则调用 Controller,看谁能处理这个 /index 请求 logger.info("Adding welcome page template: index"); this.setRootViewName("index"); } }
从第一个 if 判断中我们可以看到 欢迎页需要存在 并且 静态路径配置属性(spring.mvc.static-path-pattern) 的值必须是 “/**”,我们才能使用欢迎页功能。
-
额外注意
spring: mvc: static-path-pattern: /res/** #修改默认静态资源请求访问路径,会导致 欢迎页功能、favivon浏览器页面标签小图标功能 失效!原因是与上面第5步欢迎页配置同理,底层写死了,只有 "/**" 请求路径才能判断成功,才能访问到我们项目里来,但是改了路径请求之后就相当于无法访问进项目里来了,因此会失效。
-