使用 Spring MVC 的两步资源版本控制

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

提供静态资源时,通常的做法是将某种版本信息附加到资源 URL。这允许浏览器无限期地缓存资源。每当资源的内容发生变化时,URL 中的版本信息也会发生变化。更新后的 URL 强制客户端浏览器丢弃缓存的资源并从服务器重新加载最新的资源版本。

使用 Spring,只需两个简单的步骤即可配置版本化资源 URL。在这篇文章中,我们将看到它是如何工作的。

服务版本化 URL

首先,我们需要告诉 Spring 资源应该可以通过版本控制的 URL 访问。这是在资源处理程序 MVC 配置中完成的:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

在这里,我们为位于类路径中名为 static 的文件夹中的 JavaScript 文件创建一个资源处理程序。这些 JavaScript 文件的缓存期设置为一年。重要的部分是 VersionResourceResolver ,它支持带有版本信息的资源 URL。 VersionStrategy 用于获取资源的实际版本。

在此示例中,我们使用 ContentVersionStrategy 。此 VersionStrategy 实现根据资源的内容计算 MD5 哈希并将其附加到文件名。

例如:假设我们在 classpath:/static/ 目录中有一个 JavaScript 文件 test.js。 test.js 的 MD5 哈希值是 69ea0cf3b5941340f06ea65583193168。

我们现在可以发送请求到


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

它将解析为 classpath:/static/test.js。

请注意,仍然可以在没有 MD5 哈希的情况下请求资源。所以这个请求也有效:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

另一种 VersionStrategy 实现是 FixedVersionStrategy 。 FixedVersionStrategy 使用固定版本字符串作为前缀添加到资源路径。

例如:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

生成版本化 URL

现在我们需要确保应用程序生成包含 MD5 哈希的资源 URL。

一种方法是使用 ResourceUrlProvider 。使用 ResourceUrlProvider,资源 URL(例如 /javascript/test.js)可以转换为版本化 URL(例如 /javascript/test-69ea0cf3b5941340f06ea65583193168.js)。带有 id mvcResourceUrlProvider 的 ResourceUrlProvider bean 是用 MVC 配置自动声明的。

如果您使用 Thymeleaf 作为模板引擎,您可以使用 @bean 语法直接从模板访问 ResourceUrlProvider bean。

例如:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

如果您使用的模板引擎不允许您直接访问 Spring bean,则可以将 ResourceUrlProvider bean 添加到模型属性中。使用 ControllerAdvice ,这可能看起来像这样:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

在视图中,我们可以使用 urls 模型属性访问 ResourceUrlProvider:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

这种方法应该适用于所有支持方法调用的模板引擎。

生成版本化 URL 的另一种方法是使用 ResourceUrlEncodingFilter 。这是一个 Servlet 过滤器,它覆盖 HttpServletResponse.encodeURL() 方法以生成版本化资源 URL。

要使用 ResourceUrlEncodingFilter,我们只需在配置类中添加一个额外的 bean:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

如果您使用的模板引擎调用 response encodeURL() 方法,版本信息将自动添加到 URL。这将适用于 JSP、Thymeleaf、FreeMarker 和 Velocity。

例如:使用 Thymeleaf,我们可以使用标准的 @{..} 语法来创建 URL:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

这将导致:


 @Configuration
public class MvcApplication extends WebMvcConfigurerAdapter {

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { VersionResourceResolver versionResourceResolver = new VersionResourceResolver() .addVersionStrategy(new ContentVersionStrategy(), "/**");

registry.addResourceHandler("/javascript/*.js")
    .addResourceLocations("classpath:/static/")
    .setCachePeriod(60 * 60 * 24 * 365) /* one year */
    .resourceChain(true)
    .addResolver(versionResourceResolver);

}

... }

概括

将版本信息添加到资源 URL 是最大化浏览器缓存的常见做法。使用 Spring,我们只需定义一个 VersionResourceResolver 和一个 VersionStrategy 来为版本化的 URL 提供服务。在模板引擎中生成版本化 URL 的最简单方法是使用 ResourceUrlEncodingFilter。

如果标准 VersionStrategy 实现不符合您的要求,您可以创建我们自己的 VersionStrategy 实现。

您可以在 GitHub 上找到完整的示例源代码。