從SpringBoot原始碼看資源對映原理

王子發表於2020-09-25

 

前言

很多的小夥伴剛剛接觸SpringBoot的時候,可能會遇到載入不到靜態資源的情況。

比如html沒有樣式,圖片無法載入等等。

今天王子就與大家一起看看SpringBoot中關於資源對映部分的主要原始碼實現。

建議環境允許的情況下,小夥伴們自己使用idea建立一個springBoot專案,跟著文章和王子一起看一看原始碼,更容易理解。

 

SSM中的資源對映

在談SpringBoot之前,我們先回顧一下SSM中關於資源配置是如何實現的。

在SSM環境下,一般我們可以通過<mvc:resources />節點來配置不攔截靜態資源,就像下邊這樣:

<mvc:resources mapping="/js/**" location="/js/" />
<mvc:resources mapping="/css/**" location="/css/" />
<mvc:resources mapping="/img/**" location="/img/" />

這裡的/**表示的是可以匹配任意層級的路徑,屬於ant風格表示式,感興趣的小夥伴自行百度瞭解即可,所以也可以寫成下面這樣

<mvc:resources mapping="/**" location="/" />

上邊的這種配置方式是屬於XML配置的方式,SpringMVC的配置方式除了XML配置,也是可以通過java程式碼配置的,只需要我們自己定義一個類,來繼WebMvcConfigurationSupport這個類即可,我們看一下WebMvcConfigurationSupport的原始碼部分:

    /**
     * Override this method to add resource handlers for serving static resources.
     * @see ResourceHandlerRegistry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

上邊的註釋寫的很清楚,重寫這個方法來增加一個靜態資源的對映。那麼具體怎麼寫呢?

我們再按照註釋看一下ResourceHandlerRegistry的原始碼,重點看一下對這個類的註釋,如下。

 /* <p>To create a resource handler, use {@link #addResourceHandler(String...)} providing the URL path patterns
 / * for which the handler should be invoked to serve static resources (e.g. {@code "/resources/**"}).

大概意思就是為了建立資源的處理器,要呼叫addResourceHandler方法來提供url的表示式,這個方法是為了服務靜態資源的(ps:王子的英語水平也一般,瞭解大意即可)

然後我們去看addResourceHandler方法:

    /**
     * Add a resource handler for serving static resources based on the specified URL path patterns.
     * The handler will be invoked for every incoming request that matches to one of the specified
     * path patterns.
     * <p>Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} are allowed.
     * See {@link org.springframework.util.AntPathMatcher} for more details on the syntax.
     * @return a {@link ResourceHandlerRegistration} to use to further configure the
     * registered resource handler
     */
    public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) {
        ResourceHandlerRegistration registration = new ResourceHandlerRegistration(pathPatterns);
        this.registrations.add(registration);
        return registration;
    }

我們看到這個方法執行後返回了一個新的類ResourceHandlerRegistration

那我們再來看一下這個類中的核心方法

    /**
     * Add one or more resource locations from which to serve static content.
     * Each location must point to a valid directory. Multiple locations may
     * be specified as a comma-separated list, and the locations will be checked
     * for a given resource in the order specified.
     * <p>For example, {{@code "/"}, {@code "classpath:/META-INF/public-web-resources/"}}
     * allows resources to be served both from the web application root and
     * from any JAR on the classpath that contains a
     * {@code /META-INF/public-web-resources/} directory, with resources in the
     * web application root taking precedence.
     * <p>For {@link org.springframework.core.io.UrlResource URL-based resources}
     * (e.g. files, HTTP URLs, etc) this method supports a special prefix to
     * indicate the charset associated with the URL so that relative paths
     * appended to it can be encoded correctly, e.g.
     * {@code [charset=Windows-31J]https://example.org/path}.
     * @return the same {@link ResourceHandlerRegistration} instance, for
     * chained method invocation
     */
    public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
        this.locationValues.addAll(Arrays.asList(resourceLocations));
        return this;
    }

上邊註釋的大意就是增加一個或者多個靜態資源路徑,並舉了一些例子。原始碼我們就看到這裡。

所以我們可以像這樣實現資源的對映:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @author liumeng
 * @Date: 2020/9/25 09:20
 * @Description:
 */
@Configuration
public class SpringMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("/");
    }
}

關於SpringMVC中的資源對映部分就介紹到這,那麼我們繼續來看SpringBoot的資源對映吧。

 

SpringBoot的資源對映

其實SpringBoot的資源對映也是一脈相承的,當我們初始化一個SpringBoot專案後,靜態資源會預設存在resource/static目錄中,那麼SpringBoot的底層是怎麼實現的呢,接下來我們就去原始碼裡探索一下。

SpringBoot的原始碼在WebMvcAutoConfiguration這個類中,我們發現了熟悉的程式碼:

    @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
            CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
            if (!registry.hasMappingForPattern("/webjars/**")) {
                customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                        .addResourceLocations("classpath:/META-INF/resources/webjars/")
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
            String staticPathPattern = this.mvcProperties.getStaticPathPattern();
            if (!registry.hasMappingForPattern(staticPathPattern)) {
                customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
        }

這裡面我們重點看下邊這部分

       String staticPathPattern = this.mvcProperties.getStaticPathPattern();
            if (!registry.hasMappingForPattern(staticPathPattern)) {
                customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }

我們看一下getStaticPathPattern()方法的實現,發現獲得的就是

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

這個屬性的值,預設是/**。

然後我們再看this.resourceProperties.getStaticLocations()方法,發現獲得的是

    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

而CLASSPATH_RESOURCE_LOCATIONS是一個常量,值如下:

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

在這裡我們看到了四個值,static就是其中一個,到這裡我們就明白了SpringBoot的靜態資源為什麼會存在resource/static這個目錄下,而且放在以上4個目錄中是都可以讀取到的。

實際上SpringBoot預設的靜態資源是5個,我們再來看getResourceLocations方法,如下:

    static String[] getResourceLocations(String[] staticLocations) {
        String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
        System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
        System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
        return locations;
    }

可以看到,除了我們之前看到的4個路徑,這個方法裡還新增了一個SERVLET_LOCATIONS的路徑,點進去看一下

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

發現就是一個"/"。

所以實際上SpringBoot的預設靜態資源路徑有5個:

"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/",“/”

 

自定義配置

好了,到現在我們已經知道了SpringBoot的預設資源對映來源,那麼我們如何配置自定義的資源對映路徑呢?

其實我們可以直接通過application.properties配置,如下:

spring.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**

那麼為什麼這樣配置就可以了呢,是因為相應的類上使用了下面的註解:

@ConfigurationProperties(prefix = "spring.mvc")

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)

本文就不對這個註解再做深入研究了。感興趣的小夥伴可以持續關注我們的後續文章。

 

除了通過配置檔案自定義,還可以通過java程式碼進行配置,這種方式和我們上文說到的SpringMvc方式比較類似,只不過我們這次是實現的WebMvcConfigurer這個介面

實現方式如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        registry.addResourceHandler( "/**").addResourceLocations("classpath:/aaa");
    }
}

有了上邊文章的鋪墊,相信大家對於這段程式碼應該秒懂了吧。

到這裡,小夥伴們是否會有個疑問,WebMvcConfigurer和WebMvcConfigurationSupport有什麼關係呢?

這個問題本文就不再探索了,留給大家自行探索。

 

總結

好了,今天王子和大家一起從SpringMVC的原始碼開始探索,引出了SpringBoot的資源對映配置原理。

又介紹了SpringBoot自定義資源對映路徑的兩種方式,相信小夥伴們會有一個比較深刻的印象了。

本文到這裡就結束了,如果覺得內容對你有所幫助,那麼歡迎持續關注後續文章。

 

往期文章推薦:

Windows下使用Nginx+Tomcat做負載均衡

聊聊分散式下的WebSocket解決方案

 

相關文章