别乱继承 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/");
    }
}

相关推荐
直奔標竿8 小时前
Java开发者AI转型第二十五课!Spring AI 个人知识库实战(四)——RAG来源追溯落地,拒绝AI幻觉
java·开发语言·人工智能·spring boot·后端·spring
敖正炀10 小时前
WebFlux 深度:Reactor 线程模型、背压与错误处理
spring boot
BING_Algorithm10 小时前
一文搞定 AOP 所有核心知识点
spring boot·后端·spring
Cyan_RA910 小时前
SpringMVC 请求数据绑定与参数映射 详解
java·后端·spring·mvc·springmvc·映射请求数据
勿忘初心122111 小时前
【Java实战】SpringBoot 集成 freemarker 导出 Word 模板
java·spring boot·freemarker·模板引擎·word导出·后端实战
绿草在线11 小时前
SpringBoot项目实战:从零搭建高效开发环境
java·spring boot·后端
空中海12 小时前
Spring Boot Kafka 项目 Demo:订单事件系统 专家知识、源码阅读路线与面试题
spring boot·kafka·linq
默 语1 天前
基于 Spring Boot 3 + LangChain4j 快速构建企业级 AI 应用实战
人工智能·spring boot·后端
薪火铺子1 天前
SpringBoot WebServer启动与监听器原理深度解析
spring boot·后端·tomcat
KmSH8umpK1 天前
SpringBoot 分布式锁实战:从单机锁到Redis分布式锁全覆盖,解决超卖、重复下单、幂等并发问题
spring boot·redis·分布式