1. 程式人生 > >深入 Spring 系列之靜態資源處理

深入 Spring 系列之靜態資源處理

extend ada tst mar 找到 方法 rman 依賴 1-43

1. 背景

前一段時間,WebIDE 開源的過程中,無意間接觸到 webjars,覺得比較有趣,於是研究並整理了一下。

webjars 是將前端的庫(比如 jQuery)打包成 Jar 文件,然後使用基於 JVM 的包管理器(比如 Maven、Gradle 等)管理前端依賴的方案。

webjars 的效果非常神奇。對於其用法,我們可以在 maven 項目中添加下面的依賴:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.1.0</version>
</dependency>

然後通過請求 http://localhost:8080/webjars/jquery/3.1.0/jquery.js 即可正確訪問到 jquery 文件。

可以再舉一個應用場景的例子,比如項目要添加 Api 文檔,決定使用 Swagger,demo 參見。效果如圖:

技術分享圖片

該框架有兩部分,一部分是 springfox-swagger2 提供後端實現,另一部分是 springfox-swagger-ui 提供前端實現。引入後端實現很簡單,加入 maven 依賴即可,但是引入 springfox-swagger-ui 麻煩一些。

  • 一種方式是將該項目編譯後的 source 加入到項目。這種方式雖然能達到效果,但版本的升級就成了問題,需要手工維護。

  • 另一種方式就是我們提到的 webjars 了。去 webjars 官網、maven 倉庫、官方文檔 都可以查到 swagger-ui 依賴。將依賴加入 pom.xml 後,不需要對前端進行任何配置、修改即可引入前端代碼。代碼的更新也很方便,修改依賴版本號即可。

經過研究才發現,webjars 這並非新的技術,而是利用現有的框架對靜態資源的處理方案實現的。接下來我們一起看看 webjars 的實現以及靜態資源處理的設計方案。

2. 預備知識

2.1 Servlet 3

我們可以先來看一下 jquery webjar 的包結構:

jquery-3.1.0.jar
    └─ META-INF
        └─ resources
            └─ webjars
                └─ jquery
                    └─ 3.1.0
                        └─ jquery.js

拿 Servlet 3 舉例,應用打成 war 後,Jar(包括 WebJars)會被放在 WEB-INF/lib 目錄下,而 Servlet 3 允許直接訪問 WEB-INF/lib 下 jar 中的 /META-INF/resources 目錄下的資源。簡單來說就是 WEB-INF/lib/{\*.jar}/META-INF/resources下的資源可以被直接訪問。

技術分享圖片

所以對於 Servlet 3,直接使用 http://localhost:8080/webjars/jquery/3.1.0/jquery.js 即可訪問到 webjar 中的 jquery.js,而不用做其它的配置。

那麽如何在 Spring MVC 中訪問 webjars 呢?或者說,Spring MVC 如何處理靜態資源?

2.2 Spring MVC

Spring MVC 的入口是 DispatcherServlet,所有的請求都會匯集於該類,而後分發給不同的處理類。如果不做額外的配置,是無法訪問靜態資源的。

技術分享圖片

如果想讓 Dispatcher Servlet 直接可以訪問到靜態資源,最簡單的方法當然是交給默認的 Servlet。

技術分享圖片

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

這種情況下 Spring MVC 對資源的處理與 Servlet 方式相同。

3. 基礎

我們可以通過很簡單的配置使得 Spring MVC 有能力處理對靜態資源進行處理。

在 Spring MVC 中,資源的查找、處理使用的是責任鏈設計模式(Filter Chain):

技術分享圖片

其思路為如果當前 resolver 找不到資源,則轉交給下一個 resolver 處理。 當前 resolver 找到資源則立即返回給上級 resovler(如果存在),此時上級 resolver 又可以選擇對資源進一步處理或再次返回給它的上級(如果存在)。

配置方法為重寫 WebMvcConfigurerAdapter 類的 addResourceHandlers。

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/webjars/**")
                .addResourceLocations(
                        "classpath:/META-INF/resources/webjars/");
}

通過這樣的配置,就成功添加了一個 PathResourceResolver

技術分享圖片

該 resolver 的作用是將 url 為 /webjars/** 的請求映射到 classpath:/META-INF/resources/webjars/

比如請求 http://localhost:8080/webjars/jquery/3.1.0/jquery.js 時, Spring MVC 會查找路徑為 classpath:/META-INF/resources/webjars/jquery/3.1.0/jquery.js 的資源文件。

4. 進階

4.1 為靜態資源添加版本號

為了簡單起見,我們假設靜態資源存放在 classpath:/static,且映射的 url 為 /static

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

    // 映射 /static 的請求到 classpath 下的 static 目錄

    registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static");
    }
}

比如,請求 /static/style.css, 則會直接查找 classpath:/static/style.css

我們剛才說到,這段代碼實際上是添加了一個 PathResourceResolver,來完成對資源的查找,那麽我們是不是可以繼續向 Resolver Chain 添加更多的 Resource Resolver,從而實現對靜態資源更多樣化的處理呢?

答案是肯定的,接下來,我們添加 VersionResourceResolver。

技術分享圖片

VersionResourceResolver 可以為資源添加版本號。其所作的工作如下:首先使用下一個 resolver 獲取資源,如果找到資源則返回,不做其它處理;如果 下一個 resolver 找不到資源,則嘗試去掉 url 中的 version 信息,重新調用下一個 resolver 處理,然後無論下一個 resolver 能否處理,都返回其結果。

版本號的策略有兩種,下面分別闡述。

4.1.1 指定版本號

指定固定值作為版本號,比如:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static")
           // resourceChain(false) 的作用後面會講解
           .resourceChain(false)
           // 添加 VersionResourceResolver,且指定版本號
           .addResolver(new VersionResourceResolver()
               .addFixedVersionStrategy("1.0.0", "/**"));
}

這樣,在請求資源時,加上 /1.0.0 前綴,即 http://localhost:8080/static/1.0.0/style.css 也可正確訪問。

VersionResourceResolver 在處理該請求時,首先使用 PathResourceResolver 按照配置的映射關系 "/static/**" => "classpath:/static" 處理,即查找文件 classpath:/static/1.0.0/style.css。由於該文件不存在,VersionResourceResolver 嘗試去掉版本號 1.0.0,然後再次查找 classpath:/static/style.css,找到文件,直接返回。

4.1.2 使用 MD5 作為版本號

除了指定版本號,也可以使用資源的 MD5 作為其版本號,配置方法為:

@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(false)
                .addResolver(new VersionResourceResolver()
                    .addContentVersionStrategy("/**"));
    }

這樣,請求資源時,加上資源的 md5,即 http://localhost:8080/static/style-dfbe630979d120fe54a50593f2621225.css 也可正確訪問。

由於使用資源的 MD5 作為版本號,是 VersionResourceResolver 的其中一種策略,因此與指定版本號的處理方式相同,不再闡述。

4.2 gzip 壓縮

很多時候,為了降低傳輸的數據量,可以對資源進行壓縮。比如可以將 style.css 壓縮成 style.css.gz,但是如何讓 Spring MVC 在處理對 style.css 的請求時能正確返回 style.css.gz 呢?

為了解決這個問題,我們可以繼續添加一個 Resource Resolver —— GzipResourceResolver。

技術分享圖片

GzipResourceResolver 用來查找資源的壓縮版本,它首先使用下一個 Resource Resolver 查找資源,如果可以找到,則再嘗試查找該資源的 gzip 版本。如果存在 gzip 版本則返回 gzip 版本的資源,否則返回非 gzip 版本的資源。

比如對於如下的資源:

static
    └─ style.css
    └─ style.css.gz (使用 gzip 壓縮)

在請求 /static/style.css 時,會先使用 PathResourceResolver 查找 style.css,找到後則再次查找 style.css.gz。這裏該文件是存在的,因此會返回 style.css.gz 的內容。

PS: 請求頭中的 Content-Encoding 要包含 gzip

配置 GzipResourceResolver 很簡單:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   registry.addResourceHandler("/static/**")
           .addResourceLocations("classpath:/static/")
           .resourceChain(false)
           .addResolver(new GzipResourceResolver())
           .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
           
}

4.3 chain cache

從上面的情況可以看出,Spring MVC 會對資源進行較多的處理。如果每一次請求都做這些處理,無疑會降低服務器的性能。為了避免這種情況,這時可以添加 CachingResourceResolver 來解決這種問題。

技術分享圖片

CachingResourceResolver 用於緩存其它 Resource Resolver 查找到的資源。因此 CachingResourceResolver 會被放在最外層。請求先到達 CachingResourceResolver,嘗試在緩存中查找,如果找到,則直接返回,如果找不到,則依次調用後面的 resolver,直到有一個 resolver 能夠找到資源,CachingResourceResolver 將找到的資源緩存起來,下次請求同樣的資源時,就可以從緩存中取了。

可能有人會擔心緩存資源會占用太多的內存。但實際上並沒有資源內容,僅僅是對資源的路徑(或者說資源的抽象)進行了緩存。

開啟緩存的方法很簡單:

.requestChain(true)

前面的例子中都選擇關閉 chain cache,原因是緩存的存在會增加調試的難度。因此開發時可以考慮關閉該功能。

4.4 省略 webjar 版本

AbstractResourceResolver 的子類一共有 5 個,我們已經提到了 4 個。最後一個是 WebJarsResourceResolver。

技術分享圖片

WebJarsResourceResolver 並不需要手動添加。WebJarsResourceResolver 依賴了 webjars-locator 包,因此當添加了 webjars-locator 依賴時,Spring MVC 會自動添加 WebJarsResourceResolver。

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator</artifactId>
    <version>0.32</version>
</dependency>

WebJarsResourceResolver 的作用是可以省略 webjar 的版本。比如對於請求 http://localhost:8080/webjars/jquery/3.1.0/jquery.js 省略版本號 3.1.10 直接使用 http://localhost:8080/webjars/jquery/jquery.js 也可訪問。

至此所有 Spring MVC 提供的 ResourceResolver 都講完了。Spring MVC 提供的這 4 個 ResourceResolver 基本夠用,如果不能滿足業務需求,也可以自定義 ResourceResolver 來滿足需求。

4.5 Transformer

實際上,除了 ResourceResolver,Spring MVC 還支持修改資源內容,即 Resource Transformer。

技術分享圖片

可用的 Resource Transformer 有以下幾個:

技術分享圖片

他們的功能依次為:

  • AppCacheManifestTransformer: 幫助處理 HTML5 離線應用的 AppCache 清單內的文件
  • CachingResourceTransformer: 緩存其它 transfomer 的結果,作用同 CachingResourceResolver
  • CssLinkResourceTransformer: 處理 css 文件中的鏈接,為其加上版本號
  • ResourceTransformerSupport: 抽象類,自定義 transfomer 時繼承

我們拿 CssLinkResourceTransformer 舉例。 它會將 css 文件中的 @import 或 url() 函數中的資源路徑自動轉換為包含版本號的路徑。

配置方法為:

registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(false)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
                .addTransformer(new CssLinkResourceTransformer());

當我們在 style.css 中通過 @import "style-other.css"; 導入了另一個 css 文件,則 transformer 會自動將該 style.css 內部的 css 文件路徑地址轉換為: @import "style-other-d41d8cd98f00b204e9800998ecf8427e.css"

4.6 Http 緩存

為了避免客戶端重復獲取資源,HTTP/1.1 規範中定義了 Cache-Control 頭。幾乎所有瀏覽器都實現了支持 Cache-Control

配置方法如下:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   registry.addResourceHandler("/static/**")
           .addResourceLocations("classpath:/static/")
           .setCacheControl(CacheControl
                   .maxAge(10, TimeUnit.MINUTES)
                   .cachePrivate());
}

當請求 /static/style.css 時,返回的頭信息中會多兩條信息:

Cache-Control:max-age=600, private
Last-Modified:Sun, 04 Oct 2016 15:08:22 GMT

瀏覽器會將該信息連同資源儲存起來,當再次請求該資源時,會取出 Last-Modified 並添加到在請求頭 If-Modified-Since 中:

If-Modified-Since:Sun, 04 Oct 2016 15:08:22 GMT

Spring MVC 在收到請求,發現存在 If-Modified-Since,會提取出來該值,並與資源的修改時間比較,如果發現沒有改變,則僅僅返回狀態碼 304,無需傳遞資源內容。瀏覽器收到狀態碼 304,明白資源從上次請求到現在未被改變,http 緩存依舊可用。

http 緩存的更多用法參見 這裏。

5. 使用 Spring Boot 配置

眾所周知,使用 Spring MVC 搭建 Web 服務,不僅要編寫不少的代碼或 XML 配置,如果開發人員使用不同的 IDE,還要配置這些 IDE 使其得以被正確運行。

為了解決這些問題,spring.io 平臺提供了 Spring Boot。Spring Boot 采用 約定優於配置 的理念,在整合已有的 Spring 組件的同時,提供了大量的默認配置。得益於這些默認配置,使用 Spring Boot,只需要編寫一個 pom.xml,再加上一個 java 類,就可以跑起來一個 web 服務,如果使用 groovy,一個類文件就能跑起來 web 服務。正是由於 spring boot 帶來的這種便捷的特性,被廣泛應用在微服務的場景中。

現在,Spring Boot 已經非常成熟了,最好的教程當然是官方文檔。

項目的創建可以為普通 maven 項目,當然還可以使用 spring.io 提供的 在線創建 Spring Boot 項目 的服務創建簡項目或者。當然,也可以查看本文的示例代碼。

強烈推薦 看下 WebMvcAutoConfiguration 這個類,它為 Spring Boot 提供了大量的 Web 服務的默認配置。這些配置包括但不局限於:設置了主頁、webjars配置、靜態資源位置等。這些配置對於我們使用配置 Web 服務很有借鑒意義。

ps: 想要使用默認配置,無需使用 @EnaleWebMvc 註解。使用了 @EnableWebMvc 註解後 WebMvcAutoConfiguration 提供的默認配置會失效,必須提供全部配置。

最後,我們使用 spring boot 提供的編寫配置文件的方式,實現上面使用代碼才能完成的功能。

# application.properties

# 設置靜態資源的存放地址
spring.resources.static-locations=classpath:/resources 

# 開啟 chain cache
spring.resources.chain.cache=true

# 開啟 gzip
spring.resources.chain.gzipped=true

# 指定版本號
spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/static  
spring.resources.chain.strategy.fixed.version=1.0.0

# 使用 MD5 作為版本號
spring.resources.chain.strategy.content.enable=true
spring.resources.chain.strategy.content.paths=/**

# http 緩存過期時間
spring.resources.cachePeriod=60 

最後介紹一下如何查看這些配置的技巧:

通過查看 ResourceProperties 這個類可以看到,該類頂部有一個註解 @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)

ConfigurationProperties 是用來註入值的,prefix = "spring.resources" 表示前綴。比如我們配置文件中的 spring.resources.static-locations=classpath:/resources 這個配置,去掉 spring.resources 這個前綴,剩下的為 static-locations ,則它的值 classpath:/resources 會被註入到 ConfigurationProperties 類的 staticLocations 成員變量中。通過這種方法,我們就能通過編寫配置文件改變類的狀態而無需編寫代碼。當然,如何使用這些配置的關鍵還是要知道這些成員變量的作用。

6. 總結

本文從一個新的技術點 webjars 出發,探討了 Spring MVC 對靜態資源的處理,緊接著又了解了 Spring Boot 的配置技巧。

示例代碼:下載

7. 參考

https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn#cache-control
http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-config-static-resources
http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-static-content

https://blog.coding.net/blog/spring-static-resource-process

https://blog.csdn.net/xichenguan/article/details/52794862

深入 Spring 系列之靜態資源處理