问题现象
在一个业务中引入了一个基于 Spring Quartz 二次开发的定时任务框架,这个任务框架自己提供了一个管理界面,但是微服务启动之后却访问不了它的管理界面,提示 404。按照 Spring Boot 的说法应该是会注册一个默认的静态资源处理器的,但是实际调试发现并没有注册任何的静态资源处理器,从而导致了无法访问静态资源。导致没有注册静态资源处理器的原因是项目中提供了一个继承了 WebMvcConfigurationSupport
的配置类。现象如下图所示:
问题复现
自己构造一个测试的 Demo 项目结构,在 resources 下面有 META-INF/resources 目录,在这个目录下有一个 index.html 文件。如下图所示:
启动应用之后,通过 http://localhost:8080/index.html 可以访问到这个界面。如下图所示: 通过调试
DispatcherServlet
的 getHanlder()
方法可以看到找到一个 ResourceHttpRequestHandler
,说明找到了一个默认的静态资源处理器。如下图所示:
当增加一个继承了 WebMvcConfigurationSupport
的配置类 TestConfig
时,再访问 http://localhost:8080/index.html 这个地址时,就提示 404 了。如下图所示:

通过调试 DispatcherServlet
的 getHanlder()
方法可以看到找不到对应的 Handler,说明确实没有注册静态资源的处理器。
那为什么增加一个继承了 WebMvcConfigurationSupport
的配置类 TestConfig
时,默认的静态资源处理器就不注册了呢?接下来将从源码的角度进行分析。
源码分析
在前面的文章Spring把「手动」的复杂裹成了「自动」的温柔中分析了 Spring Boot 自动配置 Spring MVC 的配置类 WebMvcAutoConfiguration
,它初始化的条件是 CLASSPATH 路径下存在存在 Servlet, DispatcherServlet, WebMvcConfigurer 这些类,同时没有配置 WebMvcConfigurationSupport
类型的 Bean。
而上面的案例中恰好是提供了一个继承了 WebMvcConfigurationSupport
的配置类 TestConfig
,所以 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
这个条件则不满足,则配置类 WebMvcAutoConfiguration
则不会起作用。静态资源处理器的注册恰恰就是在 WebMvcAutoConfiguration
配置类中注册的。代码如下:
java
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
}
那 WebMvcAutoConfiguration
配置类是如何注册静态资源处理器的呢?在 WebMvcAutoConfiguration
中有一个静态内部类 WebMvcAutoConfigurationAdapter
,在它的 addResourceHandlers()
方法里面就是负责默认静态资源的处理器。
而这静态资源处理器默认能够处理的静态资源路径是通过 this.resourceProperties.getStaticLocations()
方法返回的,而这个 this.resourceProperties
的赋值是通过 WebProperties
的 getResources()
方法赋值的。代码如下:
java
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
private final Resources resourceProperties;
private final WebMvcProperties mvcProperties;
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
// 在这里给resourceProperties赋值的
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
"classpath:/META-INF/resources/webjars/");
// 这里注册静态资源处理器
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations()); // 这个Lambda表达式注册了静态资源处理器默认处理的资源路径
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}
}
在 WebProperties
类中有一个静态内部类 Resources
,它里面的 CLASSPATH_RESOURCE_LOCATIONS
静态变量定义了静态资源处理器默认处理的路径。 前面的this.resourceProperties.getStaticLocations()
方法返回的值就是这个静态变量的值。代码如下:
java
ConfigurationProperties("spring.web")
public class WebProperties {
/**
* Locale to use. By default, this locale is overridden by the "Accept-Language"
* header.
*/
private Locale locale;
/**
* Define how the locale should be resolved.
*/
private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
private final Resources resources = new Resources();
public static class Resources {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
public String[] getStaticLocations() {
return this.staticLocations;
}
}
}
在 WebMvcAutoConfiguration
还有一个内部类 EnableWebMvcConfiguration
,它本身也是一个 Bean,在它的父类 DelegatingWebMvcConfiguration
的 setConfigurers()
方法会注入所有实现了 WebMvcConfigurer
接口的 Bean。Spring 中注入实现了一个接口的所有 Bean 的原理可以看前面的文章 别再逐个注入了!@Autowired 批量获取接口实现类的核心逻辑拆解。
而上面的 WebMvcAutoConfigurationAdapter
恰好就是一个实现了 WebMvcConfigurer
接口的 Bean。在 DelegatingWebMvcConfiguration
的 addResourceHandlers()
方法中会调用所有实现了 WebMvcConfigurer
接口的 Bean 的 addResourceHandlers()
方法。代码如下:
java
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
this.configurers.addResourceHandlers(registry);
}
}
而 DelegatingWebMvcConfiguration
的 addResourceHandlers()
是在它的父类 WebMvcConfigurationSupport
的 resourceHandlerMapping()
方法中调用的,这个方法本质上是在初始化一个 HandlerMapping
的 Bean。代码如下:
java
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
Assert.state(this.applicationContext != null, "No ApplicationContext set");
Assert.state(this.servletContext != null, "No ServletContext set");
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper());
// 这里调用了addResourceHandlers()方法
addResourceHandlers(registry);
AbstractHandlerMapping mapping = registry.getHandlerMapping();
initHandlerMapping(mapping, conversionService, resourceUrlProvider);
return mapping;
}
}
后续
既然提供了一个继承了 WebMvcConfigurationSupport
的配置类就会导致默认的静态资源处理器失效,那假如项目就是想自定义静态资源器处理的资源路径,这个应该怎么实现呢?
其实答案就藏在上面的 DelegatingWebMvcConfiguration
的 setConfigurers()
方法中。这个方法会注入所有实现了 WebMvcConfigurer
接口的 Bean,然后在它的 addResourceHandlers()
方法中会调用所有实现了 WebMvcConfigurer
接口的 Bean 的 addResourceHandlers()
方法。
那可以提供一个实现了 WebMvcConfigurer
接口的配置类,在它的 addResourceHandlers()
方法中注册一个自定义的静态资源处理器就可以了。比如下面的代码就将请求的 /assets
URL 映射到了 CLASSPATH 下的 my-assets
路径下。代码如下:
java
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将 /assets/** 的请求映射到 classpath:/my-assets/ 目录下
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/my-assets/");
}
}