1. 程式人生 > >Spring Boot 靜態資源訪問原理解析

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,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 = registry.getHandlerMapping();
    if (handlerMapping != null) {
        handlerMapping.setPathMatcher(mvcPathMatcher());
        handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
        handlerMapping.setInterceptors(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
        handlerMapping.setCorsConfigurations(getCorsConfigurations());
    }
    else {
        handlerMapping = new EmptyHandlerMapping();
    }
    return 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
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!this.resourceProperties.isAddMappings()) {
            logger.debug("Default resource handling disabled");
            return;
        }
        Integer cachePeriod = this.resourceProperties.getCachePeriod();
        if (!registry.hasMappingForPattern("/webjars/**")) {
            customizeResourceHandlerRegistration(
                    registry.addResourceHandler("/webjars/**")
                            .addResourceLocations(
                                    "classpath:/META-INF/resources/webjars/")
                    .setCachePeriod(cachePeriod));
        }
        String staticPathPattern = this.mvcProperties.getStaticPathPattern();
        if (!registry.hasMappingForPattern(staticPathPattern)) {
            customizeResourceHandlerRegistration(
                    registry.addResourceHandler(staticPathPattern)
                            .addResourceLocations(
                                    this.resourceProperties.getStaticLocations())
                    .setCachePeriod(cachePeriod));
        }
    }
    
    ...
}

  上面的addResourceHandlers方法中,增加了預設的mapping pattern = /webjars/** ,預設的resource location是classpath:/META-INF/resources/webjars/。正是這裡的配置,我們在整合swagger的時候,就可以正常訪問到swagger webjars中的js檔案了。其中紅色的程式碼部分就是使用者可以自定義的預設靜態資源訪問方式,並通過ResourceHandlerRegistry物件進行註冊。接著看一下mvcProperties和resourceProperties對應的類吧。

@ConfigurationProperties("spring.mvc")
public class WebMvcProperties {
    ...

    /**
     * Path pattern used for static resources.
     */
    private String staticPathPattern = "/**";

    ...
}

  WebMvcProperties類中的staticPathPattern field 對應了spring.mvc.static-path-pattern這個屬性,可以看到預設值是 "/**"。

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

    .....

    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };

    private static final String[] RESOURCE_LOCATIONS;

    static {
        RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
                + SERVLET_RESOURCE_LOCATIONS.length];
        System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
                SERVLET_RESOURCE_LOCATIONS.length);
        System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
                SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
    }

    private String[] staticLocations = 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方式去載入了。