深刻 Spring 系列之静态资源处理

1. 背景

前一段时间,WebIDE 开源的过程当中,无心间接触到 webjars,以为比较有趣,因而研究并整理了一下。css

webjars 是将前端的库(好比 jQuery)打包成 Jar 文件,而后使用基于 JVM 的包管理器(好比 Maven、Gradle 等)管理前端依赖的方案。html

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 文件。java

能够再举一个应用场景的例子,好比项目要添加 Api 文档,决定使用 Swagger,demo 参见。效果如图:jquery

图片

该框架有两部分,一部分是 springfox-swagger2 提供后端实现,另外一部分是 springfox-swagger-ui 提供前端实现。引入后端实现很简单,加入 maven 依赖便可,可是引入 springfox-swagger-ui 麻烦一些。git

  • 一种方式是将该项目编译后的 source 加入到项目。这种方式虽然能达到效果,但版本的升级就成了问题,须要手工维护。github

  • 另外一种方式就是咱们提到的 webjars 了。去 webjars 官网maven 仓库官方文档 均可以查到 swagger-ui 依赖。将依赖加入 pom.xml 后,不须要对前端进行任何配置、修改便可引入前端代码。代码的更新也很方便,修改依赖版本号便可。web

通过研究才发现,webjars 这并不是新的技术,而是利用现有的框架对静态资源的处理方案实现的。接下来咱们一块儿看看 webjars 的实现以及静态资源处理的设计方案。spring

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

相关文章
相关标签/搜索