From 0175068cab7d294b6cce4369cbd7745dd03198ab Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 5 Jun 2015 14:30:09 +0200 Subject: [PATCH] Improve Last-Modified & ETag support Prior to this change, the `"Last-Modified"` and "`Etag`" support had been improved with SPR-11324: HTTP response headers are now automatically added for conditional requests and more. This commit fixes the format of the "`Last-Modified`" and "`ETag`" values, which were using an epoch timestamp rather than an HTTP-date format defined in RFC 7231 section 7.1.1.1. Also, Conditional responses are only applied when the given response applies, i.e. when it has an compatible HTTP status (2xx). Issue: SPR-13090 --- .../context/request/ServletWebRequest.java | 40 +++++++-- .../web/context/request/WebRequest.java | 2 +- .../ServletWebRequestHttpMethodsTests.java | 84 ++++++++++++------- .../servlet/support/WebContentGenerator.java | 13 ++- .../servlet/ComplexWebApplicationContext.java | 2 +- .../web/servlet/DispatcherServletTests.java | 6 +- .../servlet/SimpleWebApplicationContext.java | 2 +- .../HttpEntityMethodProcessorMockTests.java | 14 +++- .../ResourceHttpRequestHandlerTests.java | 15 +++- 9 files changed, 129 insertions(+), 49 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 5b8c5314ee0..4f787a6cdfa 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -17,16 +17,19 @@ package org.springframework.web.context.request; import java.security.Principal; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -55,6 +58,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final String METHOD_HEAD = "HEAD"; + private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); + private boolean notModified = false; @@ -174,19 +181,33 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ public boolean checkNotModified(long lastModifiedTimestamp) { HttpServletResponse response = getResponse(); if (lastModifiedTimestamp >= 0 && !this.notModified) { - if (response == null || !response.containsHeader(HEADER_LAST_MODIFIED)) { + if (response == null || isResponseCompatibleWithConditional(response, HEADER_LAST_MODIFIED)) { this.notModified = isTimeStampNotModified(lastModifiedTimestamp); if (response != null) { if (this.notModified && supportsNotModifiedStatus()) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + response.setHeader(HEADER_LAST_MODIFIED, formatDate(lastModifiedTimestamp)); } } } return this.notModified; } + private boolean isResponseCompatibleWithConditional(HttpServletResponse response, String... headers) { + if (response != null) { + if (HttpStatus.valueOf(response.getStatus()).is2xxSuccessful()) { + for (String header : headers) { + if (response.containsHeader(header)) { + return false; + } + } + return true; + } + } + return false; + } + @SuppressWarnings("deprecation") private boolean isTimeStampNotModified(long lastModifiedTimestamp) { long ifModifiedSince = -1; @@ -214,7 +235,7 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ public boolean checkNotModified(String etag) { HttpServletResponse response = getResponse(); if (StringUtils.hasLength(etag) && !this.notModified) { - if (response == null || !response.containsHeader(HEADER_ETAG)) { + if (response == null || isResponseCompatibleWithConditional(response, HEADER_ETAG)) { etag = addEtagPadding(etag); this.notModified = isETagNotModified(etag); if (response != null) { @@ -244,7 +265,7 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ // compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 if (StringUtils.hasLength(clientETag) && (clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")) - || clientETag.equals("*"))) { + || clientETag.equals("*"))) { return true; } } @@ -262,8 +283,7 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ public boolean checkNotModified(String etag, long lastModifiedTimestamp) { HttpServletResponse response = getResponse(); if (StringUtils.hasLength(etag) && !this.notModified) { - if (response == null || - (!response.containsHeader(HEADER_ETAG) && !response.containsHeader(HEADER_LAST_MODIFIED))) { + if (response == null || isResponseCompatibleWithConditional(response, HEADER_ETAG, HEADER_LAST_MODIFIED)) { etag = addEtagPadding(etag); this.notModified = isETagNotModified(etag) && isTimeStampNotModified(lastModifiedTimestamp); if (response != null) { @@ -271,13 +291,19 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } response.setHeader(HEADER_ETAG, etag); - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + response.setHeader(HEADER_LAST_MODIFIED, formatDate(lastModifiedTimestamp)); } } } return this.notModified; } + private String formatDate(long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); + dateFormat.setTimeZone(GMT); + return dateFormat.format(new Date(date)); + } + public boolean isNotModified() { return this.notModified; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java index 68f3debf6a1..bcaaffe168b 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java @@ -194,7 +194,7 @@ public interface WebRequest extends RequestAttributes { * Check whether the request qualifies as not modified given the * supplied {@code ETag} (entity tag) and last-modified timestamp, * as determined by the application. - *

This will also transparently set the appropriate response headers, + *

This will also transparently set the "Etag" and "Last-Modified" response headers, * for both the modified case and the not-modified case. *

Typical usage: *

diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
index b60aebb744f..68b700c91ec 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
@@ -20,6 +20,7 @@ import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Locale;
+import java.util.TimeZone;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -42,6 +43,8 @@ import static org.junit.Assert.*;
 @RunWith(Parameterized.class)
 public class ServletWebRequestHttpMethodsTests {
 
+	private static final String CURRENT_TIME = "Wed, 09 Apr 2014 09:57:42 GMT";
+
 	private SimpleDateFormat dateFormat;
 
 	private MockHttpServletRequest servletRequest;
@@ -50,6 +53,8 @@ public class ServletWebRequestHttpMethodsTests {
 
 	private ServletWebRequest request;
 
+	private Date currentDate;
+
 	@Parameter
 	public String method;
 
@@ -63,33 +68,57 @@ public class ServletWebRequestHttpMethodsTests {
 
 	@Before
 	public void setUp() {
+		currentDate = new Date();
 		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
 		servletRequest = new MockHttpServletRequest(method, "http://example.org");
 		servletResponse = new MockHttpServletResponse();
 		request = new ServletWebRequest(servletRequest, servletResponse);
 	}
 
 	@Test
-	public void checkNotModifiedTimestamp() {
-		long currentTime = new Date().getTime();
-		servletRequest.addHeader("If-Modified-Since", currentTime);
+	public void checkNotModifiedNon2xxStatus() {
+		long epochTime = currentDate.getTime();
+		servletRequest.addHeader("If-Modified-Since", epochTime);
+		servletResponse.setStatus(304);
+
+		assertFalse(request.checkNotModified(epochTime));
+		assertEquals(304, servletResponse.getStatus());
+		assertNull(servletResponse.getHeader("Last-Modified"));
+	}
+
+	@Test
+	public void checkNotModifiedHeaderAlreadySet() {
+		long epochTime = currentDate.getTime();
+		servletRequest.addHeader("If-Modified-Since", epochTime);
+		servletResponse.addHeader("Last-Modified", CURRENT_TIME);
+
+		assertFalse(request.checkNotModified(epochTime));
+		assertEquals(200, servletResponse.getStatus());
+		assertEquals(1, servletResponse.getHeaders("Last-Modified").size());
+		assertEquals(CURRENT_TIME, servletResponse.getHeader("Last-Modified"));
+	}
+
+	@Test
+	public void checkNotModifiedTimestamp() throws Exception {
+		long epochTime = currentDate.getTime();
+		servletRequest.addHeader("If-Modified-Since", epochTime);
 
-		assertTrue(request.checkNotModified(currentTime));
+		assertTrue(request.checkNotModified(epochTime));
 
 		assertEquals(304, servletResponse.getStatus());
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(dateFormat.format(currentDate), servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
 	public void checkModifiedTimestamp() {
-		long currentTime = new Date().getTime();
-		long oneMinuteAgo = currentTime - (1000 * 60);
+		long oneMinuteAgo = currentDate.getTime() - (1000 * 60);
 		servletRequest.addHeader("If-Modified-Since", oneMinuteAgo);
 
-		assertFalse(request.checkNotModified(currentTime));
+		assertFalse(request.checkNotModified(currentDate.getTime()));
 
 		assertEquals(200, servletResponse.getStatus());
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(dateFormat.format(currentDate), servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
@@ -154,44 +183,43 @@ public class ServletWebRequestHttpMethodsTests {
 	public void checkNotModifiedETagAndTimestamp() {
 		String eTag = "\"Foo\"";
 		servletRequest.addHeader("If-None-Match", eTag);
-		long currentTime = new Date().getTime();
-		servletRequest.addHeader("If-Modified-Since", currentTime);
+		servletRequest.addHeader("If-Modified-Since", currentDate.getTime());
 
-		assertTrue(request.checkNotModified(eTag, currentTime));
+		assertTrue(request.checkNotModified(eTag, currentDate.getTime()));
 
 		assertEquals(304, servletResponse.getStatus());
 		assertEquals(eTag, servletResponse.getHeader("ETag"));
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(dateFormat.format(currentDate), servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
 	public void checkNotModifiedETagAndModifiedTimestamp() {
 		String eTag = "\"Foo\"";
 		servletRequest.addHeader("If-None-Match", eTag);
-		long currentTime = new Date().getTime();
-		long oneMinuteAgo = currentTime - (1000 * 60);
+		long currentEpoch = currentDate.getTime();
+		long oneMinuteAgo = currentEpoch - (1000 * 60);
 		servletRequest.addHeader("If-Modified-Since", oneMinuteAgo);
 
-		assertFalse(request.checkNotModified(eTag, currentTime));
+		assertFalse(request.checkNotModified(eTag, currentEpoch));
 
 		assertEquals(200, servletResponse.getStatus());
 		assertEquals(eTag, servletResponse.getHeader("ETag"));
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(dateFormat.format(currentDate), servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
-	public void checkModifiedETagAndNotModifiedTimestamp() {
+	public void checkModifiedETagAndNotModifiedTimestamp() throws Exception {
 		String currentETag = "\"Foo\"";
 		String oldEtag = "\"Bar\"";
 		servletRequest.addHeader("If-None-Match", oldEtag);
-		long currentTime = new Date().getTime();
-		servletRequest.addHeader("If-Modified-Since", currentTime);
+		long epochTime = currentDate.getTime();
+		servletRequest.addHeader("If-Modified-Since", epochTime);
 
-		assertFalse(request.checkNotModified(currentETag, currentTime));
+		assertFalse(request.checkNotModified(currentETag, epochTime));
 
 		assertEquals(200, servletResponse.getStatus());
 		assertEquals(currentETag, servletResponse.getHeader("ETag"));
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(dateFormat.format(currentDate), servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
@@ -231,26 +259,26 @@ public class ServletWebRequestHttpMethodsTests {
 
 	@Test
 	public void checkNotModifiedTimestampWithLengthPart() throws Exception {
-		long currentTime = dateFormat.parse("Wed, 09 Apr 2014 09:57:42 GMT").getTime();
+		long epochTime = dateFormat.parse(CURRENT_TIME).getTime();
 		servletRequest.setMethod("GET");
 		servletRequest.addHeader("If-Modified-Since", "Wed, 09 Apr 2014 09:57:42 GMT; length=13774");
 
-		assertTrue(request.checkNotModified(currentTime));
+		assertTrue(request.checkNotModified(epochTime));
 
 		assertEquals(304, servletResponse.getStatus());
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(CURRENT_TIME, servletResponse.getHeader("Last-Modified"));
 	}
 
 	@Test
 	public void checkModifiedTimestampWithLengthPart() throws Exception {
-		long currentTime = dateFormat.parse("Wed, 09 Apr 2014 09:57:42 GMT").getTime();
+		long epochTime = dateFormat.parse(CURRENT_TIME).getTime();
 		servletRequest.setMethod("GET");
 		servletRequest.addHeader("If-Modified-Since", "Wed, 08 Apr 2014 09:57:42 GMT; length=13774");
 
-		assertFalse(request.checkNotModified(currentTime));
+		assertFalse(request.checkNotModified(epochTime));
 
 		assertEquals(200, servletResponse.getStatus());
-		assertEquals("" + currentTime, servletResponse.getHeader("Last-Modified"));
+		assertEquals(CURRENT_TIME, servletResponse.getHeader("Last-Modified"));
 	}
 
 }
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
index 2d3f4c83098..5db3572a827 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
@@ -16,9 +16,12 @@
 
 package org.springframework.web.servlet.support;
 
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
+import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletException;
@@ -91,6 +94,8 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
 
 	private boolean usePreviousHttpCachingBehavior = false;
 
+	private final SimpleDateFormat dateFormat;
+
 	private CacheControl cacheControl;
 
 
@@ -115,6 +120,8 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
 			this.supportedMethods.add(METHOD_HEAD);
 			this.supportedMethods.add(METHOD_POST);
 		}
+		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
 	}
 
 	/**
@@ -123,6 +130,8 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
 	 */
 	public WebContentGenerator(String... supportedMethods) {
 		this.supportedMethods = new HashSet(Arrays.asList(supportedMethods));
+		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
 	}
 
 
@@ -404,7 +413,7 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
 	protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
 		if (this.useExpiresHeader) {
 			// HTTP 1.0 header
-			response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
+			response.setHeader(HEADER_EXPIRES, dateFormat.format(System.currentTimeMillis() + seconds * 1000L));
 		}
 		if (this.useCacheControlHeader) {
 			// HTTP 1.1 header
@@ -424,7 +433,7 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
 		response.setHeader(HEADER_PRAGMA, "no-cache");
 		if (this.useExpiresHeader) {
 			// HTTP 1.0 header
-			response.setDateHeader(HEADER_EXPIRES, 1L);
+			response.setHeader(HEADER_EXPIRES, dateFormat.format(System.currentTimeMillis()));
 		}
 		if (this.useCacheControlHeader) {
 			// HTTP 1.1 header: "no-cache" is the standard value,
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java
index 9069c6b7b25..db442acd389 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java
@@ -460,7 +460,7 @@ public class ComplexWebApplicationContext extends StaticWebApplicationContext {
 
 		@Override
 		public long lastModified() {
-			return 99;
+			return 1427846401000L;
 		}
 	}
 
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java
index ca9b2894cb4..1ef593c8a57 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -177,7 +177,7 @@ public class DispatcherServletTests extends TestCase {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		simpleDispatcherServlet.service(request, response);
 		assertTrue("Not forwarded", response.getForwardedUrl() == null);
-		assertEquals("98", response.getHeader("Last-Modified"));
+		assertEquals("Wed, 01 Apr 2015 00:00:00 GMT", response.getHeader("Last-Modified"));
 	}
 
 	public void testUnknownRequest() throws Exception {
@@ -205,7 +205,7 @@ public class DispatcherServletTests extends TestCase {
 		assertTrue(request.getAttribute("test3") != null);
 		assertTrue(request.getAttribute("test3x") != null);
 		assertTrue(request.getAttribute("test3y") != null);
-		assertEquals("99", response.getHeader("Last-Modified"));
+		assertEquals("Wed, 01 Apr 2015 00:00:01 GMT", response.getHeader("Last-Modified"));
 	}
 
 	public void testExistingMultipartRequest() throws Exception {
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/SimpleWebApplicationContext.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/SimpleWebApplicationContext.java
index 337168beaa1..907f6f9358f 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/SimpleWebApplicationContext.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/SimpleWebApplicationContext.java
@@ -90,7 +90,7 @@ public class SimpleWebApplicationContext extends StaticWebApplicationContext {
 
 		@Override
 		public long getLastModified(HttpServletRequest request) {
-			return 98;
+			return 1427846400000L;
 		}
 	}
 
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
index 7103d734dc0..65eec1d786a 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
@@ -23,9 +23,12 @@ import static org.springframework.web.servlet.HandlerMapping.*;
 import java.lang.reflect.Method;
 import java.net.URI;
 import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -61,6 +64,8 @@ import org.springframework.web.method.support.ModelAndViewContainer;
  */
 public class HttpEntityMethodProcessorMockTests {
 
+	private SimpleDateFormat dateFormat;
+
 	private HttpEntityMethodProcessor processor;
 
 	private HttpMessageConverter messageConverter;
@@ -87,6 +92,9 @@ public class HttpEntityMethodProcessorMockTests {
 	@SuppressWarnings("unchecked")
 	@Before
 	public void setUp() throws Exception {
+		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+
 		messageConverter = mock(HttpMessageConverter.class);
 		given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
 
@@ -341,7 +349,7 @@ public class HttpEntityMethodProcessorMockTests {
 
 		assertTrue(mavContainer.isRequestHandled());
 		assertEquals(HttpStatus.NOT_MODIFIED.value(), servletResponse.getStatus());
-		assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
+		assertEquals(dateFormat.format(oneMinuteAgo), servletResponse.getHeader(HttpHeaders.LAST_MODIFIED));
 		assertEquals(0, servletResponse.getContentAsByteArray().length);
 	}
 
@@ -385,7 +393,7 @@ public class HttpEntityMethodProcessorMockTests {
 
 		assertTrue(mavContainer.isRequestHandled());
 		assertEquals(HttpStatus.NOT_MODIFIED.value(), servletResponse.getStatus());
-		assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
+		assertEquals(dateFormat.format(oneMinuteAgo), servletResponse.getHeader(HttpHeaders.LAST_MODIFIED));
 		assertEquals(etagValue, servletResponse.getHeader(HttpHeaders.ETAG));
 		assertEquals(0, servletResponse.getContentAsByteArray().length);
 	}
@@ -411,7 +419,7 @@ public class HttpEntityMethodProcessorMockTests {
 
 		assertTrue(mavContainer.isRequestHandled());
 		assertEquals(HttpStatus.OK.value(), servletResponse.getStatus());
-		assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
+		assertEquals(dateFormat.format(oneMinuteAgo), servletResponse.getHeader(HttpHeaders.LAST_MODIFIED));
 		assertEquals(changedEtagValue, servletResponse.getHeader(HttpHeaders.ETAG));
 		assertEquals(0, servletResponse.getContentAsByteArray().length);
 	}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java
index ed80ea0251e..50b805a53dd 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java
@@ -23,9 +23,13 @@ import static org.mockito.Mockito.*;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
 
 import javax.servlet.http.HttpServletResponse;
 
@@ -52,6 +56,8 @@ import org.springframework.web.servlet.HandlerMapping;
  */
 public class ResourceHttpRequestHandlerTests {
 
+	private SimpleDateFormat dateFormat;
+
 	private ResourceHttpRequestHandler handler;
 
 	private MockHttpServletRequest request;
@@ -61,6 +67,9 @@ public class ResourceHttpRequestHandlerTests {
 
 	@Before
 	public void setUp() throws Exception {
+		dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+		dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+
 		List paths = new ArrayList<>(2);
 		paths.add(new ClassPathResource("test/", getClass()));
 		paths.add(new ClassPathResource("testalternatepath/", getClass()));
@@ -127,7 +136,7 @@ public class ResourceHttpRequestHandlerTests {
 
 		assertEquals("no-cache", this.response.getHeader("Pragma"));
 		assertThat(this.response.getHeaderValues("Cache-Control"), Matchers.contains("no-cache", "no-store"));
-		assertTrue(headerAsLong("Expires") == 1);
+		assertEquals(this.response.getHeaderValue("Expires"), dateFormat.format(System.currentTimeMillis()));
 		assertTrue(this.response.containsHeader("Last-Modified"));
 		assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.css"));
 	}
@@ -467,8 +476,8 @@ public class ResourceHttpRequestHandlerTests {
 	}
 
 
-	private long headerAsLong(String responseHeaderName) {
-		return Long.valueOf(this.response.getHeader(responseHeaderName));
+	private long headerAsLong(String responseHeaderName) throws Exception {
+		return dateFormat.parse(this.response.getHeader(responseHeaderName)).getTime();
 	}
 
 	private long resourceLastModified(String resourceName) throws IOException {