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

相关推荐
拽着尾巴的鱼儿4 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
小江的记录本8 小时前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
一 乐9 小时前
高校实习信息发布网站|基于Spring Boot的高校实习信息发布网站的设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·高校实习信息发布网站
han_hanker10 小时前
BeanUtils.copyProperties 和序列化的问题
java·开发语言·spring boot
西凉的悲伤11 小时前
Spring Boot 中 @Async(value = “alertThreadPool“) 是什么?为什么企业项目喜欢自定义线程池?
spring boot·多线程·async·异步
闪电悠米12 小时前
黑马点评-优惠券秒杀-05_local_lock_cluster_problem
java·spring boot·redis·缓存
小江的记录本13 小时前
【JVM虚拟机】类加载机制:类加载全流程:加载→验证→准备→解析→初始化(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·算法·安全·spring·面试
搬石头的马农13 小时前
Claude Code SpringBoot开发:从0到1搭建企业级项目的6个核心Skill
java·人工智能·spring boot·后端·ai编程
yurenpai(27届找实习中)14 小时前
redis_点评(26.附近店铺——实现附近商家功能)
数据库·spring boot·redis
愤怒的苹果ext15 小时前
Spring Boot Redis Stream队列
spring boot·redis·消息队列·stream