首页 > 其他分享 >深入 Spring 系列之静态资源处理

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

时间:2023-01-02 21:07:04浏览次数:55  
标签:webjars 系列 静态 Spring resources static 资源 css

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​

 


标签:webjars,系列,静态,Spring,resources,static,资源,css
From: https://blog.51cto.com/u_15147537/5984056

相关文章

  • Spring Security 源码分析(四):Spring Social实现微信社交登录
    前言在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用​​SpringSocial​​​+​​Security​​的QQ社交登录。本章我们将实现微信的社......
  • Springboot 整合 Dubbo/ZooKeeper 详解 SOA 案例
    一、为啥整合Dubbo实现SOADubbo不单单只是高性能的RPC调用框架,更是SOA服务治理的一种方案。核心:1.远程通信,向本地调用一样调用远程方法。2.集群容错3.服务自动发......
  • Spring 事务源码(三):事务相关对象的创建
    事务源码(二)中,已经分析了beanDefinition的加载,下面来创建对应beanDefinition的bean。1、PropertySourcesPlaceholderConfigurer创建占位符处理的beanPropertyS......
  • Spring注解综合应用
    需求通过注解方式,实现下面xml的配置(实现“控制层(controller)--业务逻辑层(service)--dao层--数据源”的关系)<?xmlversion="1.0"encoding="UTF-8"?><beansxmlns="ht......
  • Spring AOP源码(四):创建被代理类的代理对象
    在AOP源码(三):创建AOP相关的Bean中,介绍了Spring创建AOP的Advisor、AnnotationAwareAspectJAutoProxyCreator的创建,其中被代理类的代理对象是如何创建的未做说明,下面来......
  • spring cache
    SpringCache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。SpringCache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过Cache......
  • Spring注解:使用注解的方式完成IOC
    补充:xml配置最开始(Spring1.x),Spring都是通过xml配置控制层(controller)--业务逻辑层(service)--dao层--数据源的关系,但是比较复杂Spring2.x的时候,随着JDK1.5支持注解的方式,......
  • 华为云双十一、双十二系列直播圆满收官,助力企业获数智化发展商机
    ​截至12月22日最后一场直播结束,华为云“快成长”直播间联合华为云云商店打造的双11、双12营销季系列直播圆满结束。13场产品专场、8场伙伴专场,不同领域的专家带着满满干货......
  • SpringCloud+Bus动态刷新
    1.设计思想:利用消息机制来进行来进行动态刷新(1)利用消息总线触发一公客户端/bus/refresh,而刷新所有客户端的配置(2)利用消息总线触发一个服务端ConfigServer的/bus/refresh端......
  • 回收站管理器 - 开源研究系列文章
    这些天弄了一个回收站管理器,主要是判断回收站里是否有文件,以及清空等操作。关键是把回收站放到任务栏里,方便操作,桌面的回收站也能够关闭显示了。1、      ......