参考:https://docs.spring.io/spring/docs/4.3.20.RELEASE/spring-framework-reference/htmlsingle/#mvc-caching
良好的HTTP缓存策略可以显着提高Web应用程序的性能和客户端的体验。 'Cache-Control'HTTP响应头主要负责这一点,以及条件性响应头,如'Last-Modified'和'ETag'。
'Cache-Control'HTTP响应头建议私有缓存(例如浏览器)和公共缓存(例如代理)如何缓存HTTP响应以供进一步重用。
ETag(entity tag:实体标签)是由HTTP/1.1兼容的Web服务器返回的HTTP响应头,用于确定给定URL的内容更改。它可以被认为是Last-Modified头的更复杂的后继者。当服务器返回带有ETag标头的表示时,在If-None-Match头中,客户端可以在后续GET中使用此标头。如果内容未更改,则服务器返回 304:Not Modified。
1. Cache-Control HTTP头
Spring Web MVC支持许多用例和方法来为应用程序配置“Cache-Control”头。虽然RFC 7234第5.2.2节完整地描述了该头及其可能的指令(参考:https://tools.ietf.org/html/rfc7234#section-5.2.2),但有几种方法可以解决最常见的情况。
Spring Web MVC在其几个API中使用配置约定:setCachePeriod(int seconds):
- -1值:不会生成“Cache-Control”响应头。
- 0值:将使用“Cache-Control:no-store”指令阻止缓存。
- n > 0值:将使用'Cache-Control: max-age=n'指令将给定的响应缓存n秒。
CacheControl构建器类简单地描述了可用的“Cache-Control”指令,并使构建自己的HTTP缓存策略变得更容易。构建完成后,可以在几个Spring Web MVC API中接受CacheControl实例作为参数。
// 缓存一小时 - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// 阻止缓存 - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// 在公共和私有缓存中缓存十天,公共缓存不应该转换响应
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
2. 静态资源的HTTP缓存支持
应使用适当的“Cache-Control”和条件头提供静态资源,以获得最佳性能。 配置ResourceHttpRequestHandler以提供静态资源不仅可以通过读取文件的元数据本地写入“Last-Modified”头,还可以在正确配置的情况下写入“Cache-Control”头。
可以在ResourceHttpRequestHandler上设置cachePeriod属性,也可以使用CacheControl实例,该实例支持更具体的指令:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public-resources/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
}
}
在XML中:
<mvc:resources mapping="/resources/**" location="/public-resources/">
<mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>
3. 控制器中Cache-Control、ETag和Last-Modified响应头的支持
控制器可以支持'Cache-Control','ETag' 或 'If-Modified-Since'HTTP请求;如果要在响应上设置“Cache-Control”头,确实建议这样做。 这涉及计算给定请求的lastModified long 或 Etag值,将其与“If-Modified-Since”请求头值进行比较,并可能返回状态代码为304(未修改)的响应。
控制器可以使用HttpEntity类型与请求/响应进行交互。 返回ResponseEntity的控制器可以在响应中包含HTTP缓存信息,如下所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
执行此操作不仅会在响应中包含“ETag”和“Cache-Control”头,而且如果客户端发送的条件头与控制器设置的缓存信息匹配,它还会将响应转换为具有空响应体的 HTTP 304 Not Modified 响应。
@RequestMapping方法也可能希望支持相同的行为。 这可以通过以下方式实现:
@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {
long lastModified = // 1. application-specific calculation
if (request.checkNotModified(lastModified)) {
// 2. shortcut exit - no further processing necessary
return null;
}
// 3. or otherwise further request processing, actually preparing content
model.addAttribute(...);
return "myViewName";
}
这里有两个关键元素:调用request.checkNotModified(lastModified)并返回null。前者在返回true之前设置适当的响应状态和响应头。后者与前者相结合,导致Spring MVC不再对请求进行进一步处理。
请注意,有3种情况:
- request.checkNotModified(lastModified):将lastModified与'If-Modified-Since'或'If-Unmodified-Since'请求头进行比较
- request.checkNotModified(eTag):将eTag与'If-None-Match'请求头进行比较
- request.checkNotModified(eTag, lastModified):同时执行这两项操作,这意味着两个条件都应该有效
当接收到条件 'GET'/'HEAD' 请求时,checkNotModified 将检查资源是否未被修改,如果是,则将导致HTTP 304 Not Modified响应。在条件 'POST'/'PUT'/'DELETE' 请求的情况下,checkNotModified将检查资源是否已被修改,如果已经修改,则将导致HTTP 409 Precondition Failed响应以防止并发修改。
4. 简单ETag支持
Servlet过滤器ShallowEtagHeaderFilter提供对ETag的支持。它是一个普通的Servlet过滤器,因此可以与任何Web框架结合使用。 ShallowEtagHeaderFilter过滤器创建所谓的简单ETag(与深ETag相反,稍后将详细介绍)。过滤器缓存呈现的JSP(或其他内容)的内容,生成MD5哈希,并将其作为ETag返回响应中的头。客户端下次发送对同一资源的请求时,会将该哈希值用作If-None-Match值。过滤器检测到此情况,再次呈现视图,并比较两个哈希值。如果它们相等,则返回304。
请注意,此策略可以节省网络带宽,但不能节省CPU,因为必须为每个请求计算完整响应。控制器级别的其他策略(如上所述)可以节省网络带宽并避免计算。
此过滤器具有writeWeakETag参数,该参数将过滤器配置为写入弱ETag,如下所示:W/"02a2d595e6ed9a0b24f027f2b63b134d6",如RFC 7232第2.3节中所定义(https://tools.ietf.org/html/rfc7232#section-2.3)。
在web.xml中配置ShallowEtagHeaderFilter:
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
<!-- Optional parameter that configures the filter to write weak ETags
<init-param>
<param-name>writeWeakETag</param-name>
<param-value>true</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
或者在Servlet 3.0+环境中,
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new ShallowEtagHeaderFilter() };
}
}
有关更多详细信息,参考下文。
5. 基于代码的Servlet容器初始化
在Servlet 3.0+环境中,可以选择以编程方式配置Servlet容器作为替代方法,也可以与web.xml文件结合使用。 下面是注册DispatcherServlet的示例:
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
WebApplicationInitializer是Spring MVC提供的一个接口,可确保检测到自定义的实现并自动用于初始化任何Servlet 3容器。 名为AbstractDispatcherServletInitializer的WebApplicationInitializer的抽象基类实现通过简单地重写方法来指定servlet映射和DispatcherServlet配置的位置,从而更容易注册DispatcherServlet。
对于使用基于Java的Spring配置的应用程序,建议使用此方法:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
如果使用基于XML的Spring配置,则应直接从AbstractDispatcherServletInitializer扩展:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AbstractDispatcherServletInitializer还提供了一种方便的方法来添加Filter实例并使它们自动映射到DispatcherServlet:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每个过滤器都根据其具体类型添加默认名称,并自动映射到DispatcherServlet。
AbstractDispatcherServletInitializer的isAsyncSupported受保护方法提供了一个单独的位置来启用DispatcherServlet上的异步支持以及映射到它的所有过滤器。 默认情况下,此标志设置为true。
最后,如果需要进一步自定义DispatcherServlet本身,则可以覆盖createDispatcherServlet方法。