别乱继承 WebMvcConfigurationSupport!Spring Boot 静态资源 404 的血泪坑

问题现象

在一个业务中引入了一个基于 Spring Quartz 二次开发的定时任务框架,这个任务框架自己提供了一个管理界面,但是微服务启动之后却访问不了它的管理界面,提示 404。按照 Spring Boot 的说法应该是会注册一个默认的静态资源处理器的,但是实际调试发现并没有注册任何的静态资源处理器,从而导致了无法访问静态资源。导致没有注册静态资源处理器的原因是项目中提供了一个继承了 WebMvcConfigurationSupport 的配置类。现象如下图所示:

问题复现

自己构造一个测试的 Demo 项目结构,在 resources 下面有 META-INF/resources 目录,在这个目录下有一个 index.html 文件。如下图所示:

启动应用之后,通过 http://localhost:8080/index.html 可以访问到这个界面。如下图所示: 通过调试 DispatcherServletgetHanlder() 方法可以看到找到一个 ResourceHttpRequestHandler,说明找到了一个默认的静态资源处理器。如下图所示:

当增加一个继承了 WebMvcConfigurationSupport 的配置类 TestConfig 时,再访问 http://localhost:8080/index.html 这个地址时,就提示 404 了。如下图所示:

通过调试 DispatcherServletgetHanlder() 方法可以看到找不到对应的 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 的赋值是通过 WebPropertiesgetResources() 方法赋值的。代码如下:

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,在它的父类 DelegatingWebMvcConfigurationsetConfigurers() 方法会注入所有实现了 WebMvcConfigurer 接口的 Bean。Spring 中注入实现了一个接口的所有 Bean 的原理可以看前面的文章 别再逐个注入了!@Autowired 批量获取接口实现类的核心逻辑拆解

而上面的 WebMvcAutoConfigurationAdapter 恰好就是一个实现了 WebMvcConfigurer 接口的 Bean。在 DelegatingWebMvcConfigurationaddResourceHandlers() 方法中会调用所有实现了 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);
    }
}

DelegatingWebMvcConfigurationaddResourceHandlers() 是在它的父类 WebMvcConfigurationSupportresourceHandlerMapping() 方法中调用的,这个方法本质上是在初始化一个 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 的配置类就会导致默认的静态资源处理器失效,那假如项目就是想自定义静态资源器处理的资源路径,这个应该怎么实现呢?

其实答案就藏在上面的 DelegatingWebMvcConfigurationsetConfigurers() 方法中。这个方法会注入所有实现了 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/");
    }
}

相关推荐
hello 早上好3 小时前
Spring Boot 核心启动机制与配置原理剖析
java·spring boot·后端
SEO-狼术5 小时前
Telerik UI for ASP.NET MVC 2025 Q3
ui·asp.net·mvc
Terio_my5 小时前
Spring Boot 缓存技术
spring boot·后端·缓存
速易达网络5 小时前
.NET MVC中实现后台商品列表功能
asp.net·mvc
梦飞翔23814 小时前
Spring Boot
spring boot
青柠编程14 小时前
基于Spring Boot的选课管理系统架构设计
java·spring boot·后端
前端橙一陈14 小时前
LocalStorage Token vs HttpOnly Cookie 认证方案
前端·spring boot
每次的天空16 小时前
Android -Glide实战技术总结
java·spring boot·spring
Code blocks18 小时前
SpringBoot快速生成二维码
java·spring boot·后端