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

问题现象

在一个业务中引入了一个基于 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/");
    }
}

相关推荐
天空鸟_时光不老10 分钟前
13-亮点提炼:把技术说辞翻译成业务价值
java·spring boot·spring·spring cloud·mybatis
小蒜学长14 分钟前
基于SpringBoot+Vue的租房管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·租房管理
洋就在江州20 分钟前
gitlab-cicd 离线集成——springboot-cicd (非docker形式,shell形式)
java·spring boot·后端·ci/cd·gitlab·gitlab-runner
令狐少侠201132 分钟前
centos7 下,使用docker 部署IOT 调试平台,ThingsBoard 并完美启动
java·spring boot·iot
省钱兄--zs3 小时前
24小时自助健身房系统软件开发实战:从架构设计到部署全指南
java·数据仓库·spring boot·系统架构·需求分析
IT研究室3 小时前
最新计算机毕业设计选题推荐-基于spring boot的智慧教学资源管理与互动交流平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
程序猿_极客3 小时前
【免费】分享一套优质的基于Java的智慧养老院管理系统的设计与实现(带可视化),源码+文档+视频详解(讲解)
java·spring boot·后端·养老院管理系统
程序猿_极客3 小时前
【免费】分享一套优质的基于SpringBoot的旅游管理系统的设计与实现(带智能推荐算法功能),源码+文档+视频详解(讲解)
java·spring boot·课程设计·旅游管理系统·计算机专业项目
专业程序开发源3 小时前
django便利店外卖后台管理系统19650-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·django·课程设计
马剑威(威哥爱编程)3 小时前
【AI全栈后端12-09】Spring Boot 用流式输出做打字机对话:点下去不再干等
java·人工智能·spring boot·后端