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

相关推荐
WX-bisheyuange2 小时前
基于Spring Boot的民宿预定系统的设计与实现
java·spring boot·后端·毕业设计
q***9443 小时前
springboot接入deepseek深度求索 java
java·spring boot·后端
百***06013 小时前
SpringBoot的@Scheduled和@Schedules有什么区别
java·spring boot·spring
q***48316 小时前
【监控】Spring Boot+Prometheus+Grafana实现可视化监控
spring boot·grafana·prometheus
披着羊皮不是狼6 小时前
多用户跨学科交流系统(4)参数校验+分页搜索全流程的实现
java·spring boot
q***23927 小时前
基于SpringBoot和PostGIS的云南与缅甸的千里边境线实战
java·spring boot·spring
q***78787 小时前
Spring Boot的项目结构
java·spring boot·后端
百***17078 小时前
Spring Boot spring.factories文件详细说明
spring boot·后端·spring
Q_Q5110082858 小时前
python+django/flask的宠物用品系统vue
spring boot·python·django·flask·node.js·php
whltaoin8 小时前
【Java 微服务中间件】RabbitMQ 全方位解析:同步异步对比、SpringAMQT基础入门、实战、交换机类型及消息处理详解
spring boot·微服务·中间件·rabbitmq·spring amqt