Spring Boot 静态资源处理,默认配置路径为 classpath 下的/static (或 /public/resources/META-INF/resources),即将/**映射到这几个路径下。Spring Boot 启动的时候,我们会看到如下日志:

1
2
3
2016-12-16 22:27:56 INFO 9812 - [restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-16 22:27:56 INFO 9812 - [restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-16 22:27:56 INFO 9812 - [restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]

可以看到,Spring Boot 静态资源处理使用 Spring MVC 的ResourceHttpRequestHandler来实现。这里除了将/**映射到默认资源路径下,还为我们配置一个favicon.ico以及后面会讲到的webjars。Spring Boot中可以通过添加一个配置类,继承WebMvcConfigurerAdapter来自定义Spring MVC的配置,其中addResourceHandlers方法可以用来自定义静态资源处理,比如修改静态资源的缓存时间。这里我们想要替换默认的静态资源列表:

1
2
3
4
5
6
7
8
9
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

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

此时,/**被映射到了"classpath:/mydir1/","classpath:/mydir2/","classpath:/mydir3/"下,要注意,如果替换了默认静态资源地址,那么默认欢迎页面index.html也将会被替换到我们自定义配置的任何路径下,Spring Boot相关的实现代码在ResourceProperties,这个类使用了ConfigurationProperties注解,前缀(prefix)为spring.resources,静态资源字段为staticLocations,那么我们也可以通过配置spring.resources.staticLocations这个properties来替换静态资源地址。


1.使用webjars

我们在Web开发中,前端页面中用了越来越多的JS或CSS,如jQuery等等,平时我们是将这些Web资源拷贝到Java的目录下,这种通过人工方式拷贝可能会产生版本误差,拷贝版本错误,前端页面就无法正确展示。 WebJars 就是为了解决这种问题衍生的,将这些Web前端资源打包成Java的Jar包,然后借助Maven这些依赖库的管理,保证这些Web资源版本唯一性,升级也比较容易。

webjars中的资源所在路径为:/META-INF/resources/webjars/,其中/META-INF/resources是默认资源路径,从刚才的日志当中也可以看到,Spirng Boot为其配置一个/webjars/**的Url映射。我们可以去http://www.webjars.org/ 找到我们想要的资源,将其依赖加入我们项目当中,即可使用这个资源了。例如:

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.4</version>
</dependency>

这里dependency的version即为静态资源的版本号,模板中(thymeleaf)使用:

1
2
3
<head>
<script th:src="@{/webjars/jquery/2.1.4/jquery.js}"></script>
</head>

2.静态资源处理

Spring Boot 为我们提供了 Spring MVC 中比较高级的静态资源处理方法,比如为webjars添加动态版本号、清除缓存的静态资源等。

2.1.为Webjars添加动态版本号

实际开发当中,我们不可避免的需要升级第三方js、css库的版本,如果我们有100个页面,那么一个一个页面的去替换版本号显然不是个好方法,Spring Boot项目中我们只需要简单的添加webjars-locator依赖,即可为Webjars添加动态版本号管理。

1
2
3
4
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
</dependency>

此依赖使用spring-boot-dependencies中统一管理的版本号,然后模板中就不需要写版本号了:

1
2
3
<head>
<script th:src="@{/webjars/jquery/jquery.js}"></script>
</head>

生成的结果为:

1
2
3
<head>
<script th:src="/webjars/jquery/2.1.4/jquery.js"></script>
</head>

原理很简单,运行时使用ResourceUrlEncodingFilter这个过滤器,将response通过HttpServletResponseWrapper进行静态包装并重写encodeURL方法,然后模板中使用responseencodeURL将链接地址重写。并且一定要注意ThymeleafFreeMarker这两个模板自动配置了ResourceUrlEncodingFilter,JSP则需要手动声明这个过滤器,而其它模板则需要自已去实现这个功能。下面说到的其它版本管理中,也是同理。

此时,如果想要更换jquery版本号,只需修改jquery的webjars的dependency版本即可。

2.2.缓存清除

当静态资源发生变化时,为了清除用户客户端的缓存,我们一般会将静态资源加上版本号或者固定时间戮,每当资源变化时手动去更新版本或时间戳。比如:

1
<link rel="stylesheet" type="text/css" href="/css/style.css?v=1.0.0"/>

这种方式也是需要我们一个页面一个页面的去修改,显然不合适。我们有更好的方式,Spring Boot中我们可以去配置VersionResourceResolver,它有两种策略:

  • FixedVersionStrategy可以使用某项属性,或者日期,或者其它来作为版本.
  • ContentVersionStrategy是使用资源内容计算出来的MD5哈希作为版本

与Webjars添加动态版本号一样,这两种方式都借助了ResourceUrlEncodingFilter来重写模板中的URL。

更详细配置可参考:Spring Framework’s reference documentation.

2.2.1.ContentVersionStrategy

properties文件配置方法如下:

1
2
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

同样也可以在继承WebMvcConfigurerAdapter的配置类中配置

1
2
3
4
5
6
7
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}

配置成功后,页面中的:

1
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}"/>

会被替换为:

1
<link rel="stylesheet" type="text/css" th:href="/css/style-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

文件的hash会被缓存,使用的是Spring的ConcurrentMapCache,一个简单的缓存实现,文件改动后,只有重启服务再次访问才会重新生成hash

2.2.2.FixedVersionStrategy

在动态的加载静态资源时,比如javascript模块加截器,这时更改文件名不是个好选择,可以使用FixedVersionStrategy,用某项属性,或者日期,或者其它来作为版本。

比如,在properties文件中这样配置

1
2
3
4
5
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/lib/
spring.resources.chain.strategy.fixed.version=v12

这样配置后, 位于 "/js/lib/"下的 JavaScript 将会使用一个固定的版本号"/v12/js/lib/mymodule.js" ,而其它的静态资源仍然使用文件hash的方式。

同样可以在类中配置:

1
2
3
4
5
6
7
8
9
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addFixedVersionStrategy("v12", "/js/lib/")
.addContentVersionStrategy("/**"));
}

2.3.URL中含有”#”字符的问题

查看ResourceUrlEncodingFilter这个过滤器,发现当URL中含有”?”,比如

1
<script th:src="@{/jquery.js?v=1}"></script>

responseencodeURL执行时,会首先将”?”后面的部分?v=1去掉,然后再根据/jquery.js去资源目录去查找文件

1
2
3
4
private int getQueryParamsIndex(String url) {
int index = url.indexOf("?");
return (index > 0 ? index : url.length());
}

如果URL中有其它字符,比如/jquery.js#1encodeURL执行时会拿着/jquery.js#1去查找文件,这样当然找不到,所以导致没法配置资源缓存清除这个功能了。

至于为什么我会遇到资源文件的URL中有”#”字符的情况呢?

1
2
3
<svg>
<use xlink:href="/svg-symbols.svg#grbs"></use>
</svg>

我们公司前端的svg引用就是这样写的,将很多svg放在一个文件中,通过”#”选择调用。

于是我重写了ResourceUrlEncodingFilter过滤器,添加”#”的判断,并优化了文件URL缓存。