Spring Boot 静态资源访问原理解析

一、前言

  springboot 配置静态资源方式是多种多样,接下来我会介绍其中几种方式,并解析一下其中的原理。

二、使用 properties 属性进行配置

  应该说 spring.mvc.static-path-pattern 和 spring.resources.static-locations 这两属性是成对使用的,如果不明白其中的原理,总会出现资源 404 的情况。首先收一下 spring.mvc.static-path-pattern 代表的是一个 Ant Path 路径,例如 resources/**,表示当你的路径中存在 resources/** 的时候才会处理请求。比如我们访问“http://localhost:8080/resources/xxx.js”时,很显然,springboot 逻辑中会根据模式匹配对 url 进行匹配,匹配命中后,是如何再定位到具体的资源的呢?这时候 spring.resources.static-locations 的配置就起作用了。

  忘记说了,在 springboot 中 spring.mvc.static-path-pattern 的默认值是 /**,spring.resources.static-locations 的默认值是 classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources,servlet context:/,springboot 中相关的ResourceHttpRequestHandler 就会去 spring.resources.static-locations 配置的所有路径中寻找资源文件。

  所以我之前才说 spring.mvc.static-path-pattern 和 spring.resources.static-locations 这两属性是成对使用的。

三、springboot 中默认对静态资源的处理

  调试过程中,通过查看 org.springframework.web.servlet.DispatcherServlet 中的 handlerMappings 变量,我们发现有一个很显眼的 resourceHandlerMapping ,这个是 springboot 为我们提供的一个默认的静态资源 handler,通过全文搜索发现出现在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport 这个类中,也就是这个类包含了 @EnableWebMvc 注解中的大多数功能,更多的扩展功能请参考 org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration。

  resourceHandlerMapping 的定义如下。

/**
 * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped
 * resource handlers. To configure resource handling, override
 * {@link #addResourceHandlers}.
 */
@Bean
public HandlerMapping resourceHandlerMapping() {
    ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
            this.servletContext, mvcContentNegotiationManager());
    addResourceHandlers(registry);
AbstractHandlerMapping handlerMapping </span>=<span style="color: rgba(0, 0, 0, 1)"> registry.getHandlerMapping();
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (handlerMapping != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
    handlerMapping.setPathMatcher(mvcPathMatcher());
    handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
    handlerMapping.setInterceptors(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
    handlerMapping.setCorsConfigurations(getCorsConfigurations());
}
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
    handlerMapping </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> EmptyHandlerMapping();
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> handlerMapping;

}

  请大家先记住 ResourceHandlerRegistry 这个类。

    首先看一下 addResourceHandlers(registry); 这个方法,父类 DelegatingWebMvcConfiguration 做了实现,如下。


private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    this.configurers.addResourceHandlers(registry);
}

  其中 WebMvcConfigurerComposite 是操作了 WebMvcConfigurer 类型的对象的集合。在 org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 这个 springmvc 的自动配置类中,有一个 WebMvcConfigurer 的实现类,如下。

// Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when not
// on the classpath
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
    ...
@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> addResourceHandlers(ResourceHandlerRegistry registry) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.resourceProperties.isAddMappings()) {
        logger.debug(</span>"Default resource handling disabled"<span style="color: rgba(0, 0, 0, 1)">);
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }
    Integer cachePeriod </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.resourceProperties.getCachePeriod();
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!registry.hasMappingForPattern("/webjars/**"<span style="color: rgba(0, 0, 0, 1)">)) {
        customizeResourceHandlerRegistration(
                registry.addResourceHandler(</span>"/webjars/**"<span style="color: rgba(0, 0, 0, 1)">)
                        .addResourceLocations(
                                </span>"classpath:/META-INF/resources/webjars/"<span style="color: rgba(0, 0, 0, 1)">)
                .setCachePeriod(cachePeriod));
    }
   <strong> <span style="color: rgba(255, 0, 0, 1)">String staticPathPattern </span></strong></span><strong><span style="color: rgba(255, 0, 0, 1)">= this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(
                registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(
                                this</span></strong><span style="color: rgba(0, 0, 0, 1)"><strong><span style="color: rgba(255, 0, 0, 1)">.resourceProperties.getStaticLocations())
                .setCachePeriod(cachePeriod));</span>
    }</strong>
}

...

}

  上面的 addResourceHandlers 方法中,增加了默认的 mapping pattern = /webjars/** ,默认的 resource location 是 classpath:/META-INF/resources/webjars/。正是这里的配置,我们在集成 swagger 的时候,就可以正常访问到 swagger webjars 中的 js 文件了。其中红色的代码部分就是用户可以自定义的默认静态资源访问方式,并通过 ResourceHandlerRegistry 对象进行注册。接着看一下 mvcProperties 和 resourceProperties 对应的类吧。

@ConfigurationProperties("spring.mvc")
public class WebMvcProperties {
    ...
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * Path pattern used for static resources.
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span> String staticPathPattern = "/**"<span style="color: rgba(0, 0, 0, 1)">;

...

}

  WebMvcProperties 类中的 staticPathPattern field 对应了 spring.mvc.static-path-pattern 这个属性,可以看到默认值是 "/**"。

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
.....

</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String[] SERVLET_RESOURCE_LOCATIONS = { "/"<span style="color: rgba(0, 0, 0, 1)"> };

</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String[] CLASSPATH_RESOURCE_LOCATIONS =<span style="color: rgba(0, 0, 0, 1)"> {
        </span>"classpath:/META-INF/resources/", "classpath:/resources/"<span style="color: rgba(0, 0, 0, 1)">,
        </span>"classpath:/static/", "classpath:/public/"<span style="color: rgba(0, 0, 0, 1)"> };

</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span><span style="color: rgba(0, 0, 0, 1)"> String[] RESOURCE_LOCATIONS;

</span><span style="color: rgba(0, 0, 255, 1)">static</span><span style="color: rgba(0, 0, 0, 1)"> {
    RESOURCE_LOCATIONS </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> String[CLASSPATH_RESOURCE_LOCATIONS.length
            </span>+<span style="color: rgba(0, 0, 0, 1)"> SERVLET_RESOURCE_LOCATIONS.length];
    System.arraycopy(SERVLET_RESOURCE_LOCATIONS, </span>0, RESOURCE_LOCATIONS, 0<span style="color: rgba(0, 0, 0, 1)">,
            SERVLET_RESOURCE_LOCATIONS.length);
    System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, </span>0<span style="color: rgba(0, 0, 0, 1)">, RESOURCE_LOCATIONS,
            SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
}

</span><span style="color: rgba(0, 0, 255, 1)">private</span> String[] staticLocations =<span style="color: rgba(0, 0, 0, 1)"> RESOURCE_LOCATIONS;

......

}

  ResourceProperties 中 staticLocations field 对应了 spring.resources.static-locations 这个属性。可以看到默认值是 classpath:[/META-INF/resources/, /resources/, /static/, /public/], servlet context:/

四、静态资源的 Bean 配置

  在了解了 springboot 默认资源的配置的原理(即 spring.mvc.static-path-pattern 和 spring.resources.static-locations),我们可以增加一个 WebMvcConfigurer 类型的 bean,来添加静态资源的访问方式,还记得上面说的“请记住 ResourceHandlerRegistry 这个类“,下面就用到了哦。

@Configuration
public class ResourceWebMvcConfigurer extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("classpath:/public-resources/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
    }
}

  那么当访问路径中包含 "resources/**" 的时候,resource handler 就会去 classpath:/public-resources 目录下寻找了。

五、静态资源的查找

  参考 org.springframework.web.servlet.resource.ResourceHttpRequestHandler,ResourceHttpRequestHandler 中通过 org.springframework.web.servlet.resource.PathResourceResolver 进行查找。

  举个例子,下图是 springboot 打包之后的目录结构,现在想要通过 url 访问 application.properties 文件,springboot 默认的静态文件配置可以吗?当然需要用事实来说话了。

   

   我们已经知道,默认的 resource locations 中有个 servlet-context:/,访问你的 url 是http://localhost:8080/工程名 /application.properties,调试一下 PathResourceResolver,结果如下。

  

  

  发现 servlet-context 的根路径如上图所示,查看一下这个路径对应的目录,发现什么都没有,所以很显然无法找到我们要找的文件了。毕竟一般使用 springboot 都是 jar 项目,servlet-context path 下没有用户自定义的资源。

 六、其他方式

  在 Servlet3 协议规范中,包含在 JAR 文件 /META-INFO/resources/ 路径下的资源可以直接访问了。如果将 springboot 项目打包成 war 包,可以配置一个默认的 servlet。在 WebMvcConfigurationSupport 中已经定义好了,不过默认是一个 EmptyHandlerMapping。

/**
 * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped
 * default servlet handler. To configure "default" Servlet handling,
 * override {@link #configureDefaultServletHandling}.
 */
@Bean
public HandlerMapping defaultServletHandlerMapping() {
    DefaultServletHandlerConfigurer configurer = new DefaultServletHandlerConfigurer(servletContext);
    configureDefaultServletHandling(configurer);
    AbstractHandlerMapping handlerMapping = configurer.getHandlerMapping();
    handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping();
    return handlerMapping;
}

  可以通过自定义一个 WebMvcConfigurer 类型的 bean,改写configureDefaultServletHandling 方法,如下。

@Configuration
public class MyWebConfigurer extends WebMvcConfigurerAdapter {
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable();
    }
}

  这样就设置了一个默认的 servlet,在加载静态资源的时候就会按照 servelt 方式去加载了。

 

  就先分享这么多了,更多分享请关注我们的技术公众号吧!!!