Skip to content

Commit 38f32e3

Browse files
committed
Improve HTTP caching flexiblity
This commit improves HTTP caching defaults and flexibility in Spring MVC. 1) Better default caching headers The `WebContentGenerator` abstract class has been updated with better HTTP defaults for HTTP caching, in line with current browsers and proxies implementation (wide support of HTTP1.1, etc); depending on the `setCacheSeconds` value: * sends "Cache-Control: max-age=xxx" for caching responses and do not send a "must-revalidate" value by default. * sends "Cache-Control: no-store" or "Cache-Control: no-cache" in order to prevent caching Other methods used to set specific header such as `setUseExpiresHeader` or `setAlwaysMustRevalidate` are now deprecated in favor of `setCacheControl` for better flexibility. Using one of the deprecated methods re-enables previous HTTP caching behavior. This change is applied in many Handlers, since `WebContentGenerator` is extended by `AbstractController`, `WebContentInterceptor`, `ResourceHttpRequestHandler` and others. 2) New CacheControl builder class This new class brings more flexibility and allows developers to set custom HTTP caching headers. Several strategies are provided: * `CacheControl.maxAge(int)` for caching responses with a "Cache-Control: max-age=xxx" header * `CacheControl.noStore()` prevents responses from being cached with a "Cache-Control: no-store" header * `CacheControl.noCache()` forces caches to revalidate the cached response before reusing it, with a "Cache-Control: no-store" header. From that point, it is possible to chain method calls to craft a custom CacheControl instance: ``` CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS) .cachePublic().noTransform(); ``` 3) Configuring HTTP caching in Resource Handlers On top of the existing ways of configuring caching mechanisms, it is now possible to use a custom `CacheControl` to serve resources: ``` @configuration public class MyWebConfig extends WebMvcConfigurerAdapter { @OverRide public void addResourceHandlers(ResourceHandlerRegistry registry) { CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS); registry.addResourceHandler("/resources/**) .addResourceLocations("classpath:/resources/") .setCacheControl(cc); } } ``` or ``` <mvc:resources mapping="/resources/**" location="classpath:/resources/"> <mvc:cachecontrol max-age="3600" cache-public="true"/> </mvc:resources> ``` Issue: SPR-2779, SPR-6834, SPR-7129, SPR-9543, SPR-10464
1 parent 953608e commit 38f32e3

File tree

21 files changed

+803
-177
lines changed

21 files changed

+803
-177
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.springframework.util.StringUtils;
21+
22+
/**
23+
* A builder for creating "Cache-Control" HTTP response headers.
24+
*
25+
* <p>Adding Cache-Control directives to HTTP responses can significantly improve the client experience when interacting
26+
* with a web application. This builder creates opinionated "Cache-Control" headers with response directives only, with
27+
* several use cases in mind.
28+
*
29+
* <ul>
30+
* <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
31+
* will result in {@code Cache-Control: "max-age=3600"}</li>
32+
* <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
33+
* will result in {@code Cache-Control: "no-store"}</li>
34+
* <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
35+
* will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
36+
* </ul>
37+
*
38+
* <p>Note that to be efficient, Cache-Control headers should be written along HTTP validators such as
39+
* "Last-Modifed" or "ETag" headers.
40+
*
41+
* @author Brian Clozel
42+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
43+
* @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
44+
* HTTP caching - Google developers reference</a>
45+
* @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
46+
* @since 4.2
47+
*/
48+
public class CacheControl {
49+
50+
private boolean mustRevalidate;
51+
52+
private boolean noCache;
53+
54+
private boolean noStore;
55+
56+
private boolean noTransform;
57+
58+
private boolean cachePublic;
59+
60+
private boolean cachePrivate;
61+
62+
private boolean proxyRevalidate;
63+
64+
private long maxAge;
65+
66+
private long sMaxAge;
67+
68+
/**
69+
* Create a CacheControl instance with default values,
70+
* i.e. that will produce an empty "Cache-Control" header value.
71+
*/
72+
protected CacheControl() {
73+
this.mustRevalidate = false;
74+
this.noCache = false;
75+
this.noStore = false;
76+
this.noTransform = false;
77+
this.cachePublic = false;
78+
this.cachePrivate = false;
79+
this.proxyRevalidate = false;
80+
this.maxAge = -1;
81+
this.sMaxAge = -1;
82+
}
83+
84+
/**
85+
* Add a "max-age=" directive.
86+
*
87+
* <p>This directive is well suited for publicly caching resources, knowing that they won't change within
88+
* the configured amount of time. Additional directives can be also used, in case resources shouldn't be
89+
* cached ({@link #cachePrivate()}) or transformed ({@link #noTransform()}) by shared caches.
90+
*
91+
* <p>In order to prevent caches to reuse the cached response even when it has become stale
92+
* (i.e. the "max-age" delay is passed), the "must-revalidate" directive should be set ({@link #mustRevalidate()}
93+
*
94+
* @param maxAge the maximum time the response should be cached
95+
* @param unit the time unit of the {@code maxAge} argument
96+
* @return {@code this}, to facilitate method chaining
97+
*
98+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
99+
*/
100+
public static CacheControl maxAge(long maxAge, TimeUnit unit) {
101+
CacheControl cc = new CacheControl();
102+
cc.maxAge = unit.toSeconds(maxAge);
103+
return cc;
104+
}
105+
106+
/**
107+
* Add a "no-store" directive
108+
*
109+
* <p>This directive is well suited for preventing caches (browsers and proxies) to cache the content of responses.
110+
*
111+
* @return {@code this}, to facilitate method chaining
112+
*
113+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
114+
*/
115+
public static CacheControl noStore() {
116+
CacheControl cc = new CacheControl();
117+
cc.noStore = true;
118+
return cc;
119+
}
120+
121+
/**
122+
* Add a "no-cache" directive.
123+
*
124+
* <p>This directive is well suited for telling caches that the response can be reused only if the client
125+
* revalidates it with the server. This directive won't disable cache altogether and may result with
126+
* clients sending conditional requests (with "ETag", "If-Modified-Since" headers) and the server responding
127+
* with "304 - Not Modified" status.
128+
*
129+
* <p>In order to disable caching and minimize requests/responses exchanges, the {@link #noStore()} directive
130+
* should be used.
131+
*
132+
* @return {@code this}, to facilitate method chaining
133+
*
134+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
135+
*/
136+
public static CacheControl noCache() {
137+
CacheControl cc = new CacheControl();
138+
cc.noCache = true;
139+
return cc;
140+
}
141+
142+
/**
143+
* Return an empty directive.
144+
*
145+
* <p>This is well suited for using other optional directives without "no-cache", "no-store" or "max-age".
146+
*
147+
* @return {@code this}, to facilitate method chaining
148+
*/
149+
public static CacheControl empty() {
150+
CacheControl cc = new CacheControl();
151+
return cc;
152+
}
153+
154+
/**
155+
* Add a "must-revalidate" directive
156+
*
157+
* <p>This directive indicates that once it has become stale, a cache MUST NOT use the response
158+
* to satisfy subsequent requests without successful validation on the origin server.
159+
*
160+
* @return {@code this}, to facilitate method chaining
161+
*
162+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
163+
*/
164+
public CacheControl mustRevalidate() {
165+
this.mustRevalidate = true;
166+
return this;
167+
}
168+
169+
/**
170+
* Add a "no-transform" directive
171+
*
172+
* <p>This directive indicates that intermediaries (caches and others) should not transform the response content.
173+
* This can be useful to force caches and CDNs not to automatically gzip or optimize the response content.
174+
*
175+
* @return {@code this}, to facilitate method chaining
176+
*
177+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
178+
*/
179+
public CacheControl noTransform() {
180+
this.noTransform = true;
181+
return this;
182+
}
183+
184+
/**
185+
* Add a "public" directive
186+
*
187+
* <p>This directive indicates that any cache MAY store the response, even if the response
188+
* would normally be non-cacheable or cacheable only within a private cache.
189+
*
190+
* @return {@code this}, to facilitate method chaining
191+
*
192+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
193+
*/
194+
public CacheControl cachePublic() {
195+
this.cachePublic = true;
196+
return this;
197+
}
198+
199+
/**
200+
* Add a "private" directive
201+
*
202+
* <p>This directive indicates that the response message is intended for a single user
203+
* and MUST NOT be stored by a shared cache.
204+
*
205+
* @return {@code this}, to facilitate method chaining
206+
*
207+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
208+
*/
209+
public CacheControl cachePrivate() {
210+
this.cachePrivate = true;
211+
return this;
212+
}
213+
214+
/**
215+
* Add a "proxy-revalidate" directive
216+
*
217+
* <p>This directive has the same meaning as the "must-revalidate" directive,
218+
* except that it does not apply to private caches (i.e. browsers, HTTP clients)
219+
*
220+
* @return {@code this}, to facilitate method chaining
221+
*
222+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
223+
*/
224+
public CacheControl proxyRevalidate() {
225+
this.proxyRevalidate = true;
226+
return this;
227+
}
228+
229+
/**
230+
* Add a "s-maxage" directive
231+
*
232+
* <p>This directive indicates that, in shared caches, the maximum age specified by this directive
233+
* overrides the maximum age specified by other directives.
234+
*
235+
* @param sMaxAge the maximum time the response should be cached
236+
* @param unit the time unit of the {@code sMaxAge} argument
237+
* @return {@code this}, to facilitate method chaining
238+
*
239+
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
240+
*/
241+
public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) {
242+
this.sMaxAge = unit.toSeconds(sMaxAge);
243+
return this;
244+
}
245+
246+
/**
247+
* Return the "Cache-Control" header value
248+
*
249+
* @return null if no directive was added, the header value otherwise
250+
*/
251+
public String getHeaderValue() {
252+
StringBuilder ccValue = new StringBuilder();
253+
if (this.maxAge != -1) {
254+
appendDirective(ccValue, "max-age=" + Long.toString(maxAge));
255+
}
256+
if (this.noCache) {
257+
appendDirective(ccValue, "no-cache");
258+
}
259+
if (this.noStore) {
260+
appendDirective(ccValue, "no-store");
261+
}
262+
if (this.mustRevalidate) {
263+
appendDirective(ccValue, "must-revalidate");
264+
}
265+
if (this.noTransform) {
266+
appendDirective(ccValue, "no-transform");
267+
}
268+
if (this.cachePublic) {
269+
appendDirective(ccValue, "public");
270+
}
271+
if (this.cachePrivate) {
272+
appendDirective(ccValue, "private");
273+
}
274+
if (this.proxyRevalidate) {
275+
appendDirective(ccValue, "proxy-revalidate");
276+
}
277+
if (this.sMaxAge != -1) {
278+
appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge));
279+
}
280+
String ccHeaderValue = ccValue.toString();
281+
if (StringUtils.hasText(ccHeaderValue)) {
282+
return ccHeaderValue;
283+
}
284+
return null;
285+
}
286+
287+
private void appendDirective(StringBuilder b, String value) {
288+
if (b.length() > 0) {
289+
b.append(", ");
290+
}
291+
b.append(value);
292+
}
293+
294+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import org.hamcrest.Matchers;
20+
import org.junit.Test;
21+
22+
import static org.junit.Assert.*;
23+
24+
import java.util.concurrent.TimeUnit;
25+
26+
/**
27+
* @author Brian Clozel
28+
*/
29+
public class CacheControlTests {
30+
31+
private static final String CACHE_CONTROL_HEADER = "Cache-Control";
32+
33+
@Test
34+
public void emptyCacheControl() throws Exception {
35+
CacheControl cc = CacheControl.empty();
36+
assertThat(cc.getHeaderValue(), Matchers.nullValue());
37+
}
38+
39+
@Test
40+
public void maxAge() throws Exception {
41+
CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS);
42+
assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600"));
43+
}
44+
45+
@Test
46+
public void maxAgeAndDirectives() throws Exception {
47+
CacheControl cc = CacheControl.maxAge(3600, TimeUnit.SECONDS).cachePublic().noTransform();
48+
assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, no-transform, public"));
49+
}
50+
51+
@Test
52+
public void maxAgeAndSMaxAge() throws Exception {
53+
CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).sMaxAge(30, TimeUnit.MINUTES);
54+
assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, s-maxage=1800"));
55+
}
56+
57+
@Test
58+
public void noCachePrivate() throws Exception {
59+
CacheControl cc = CacheControl.noCache().cachePrivate();
60+
assertThat(cc.getHeaderValue(), Matchers.equalTo("no-cache, private"));
61+
}
62+
63+
@Test
64+
public void noStore() throws Exception {
65+
CacheControl cc = CacheControl.noStore();
66+
assertThat(cc.getHeaderValue(), Matchers.equalTo("no-store"));
67+
}
68+
}

0 commit comments

Comments
 (0)