Close

Spring MVC - How to set 'Last-Modified' and 'If-Modified-Since' headers?

[Last Updated: Sep 20, 2026]

In this tutorial we will learn Spring's support for the 'Last-Modified' and 'If-Modified-Since' headers. Please check out the general step-by-step usage of these headers. We also recommend checking out the related servlet tutorial to get a good understanding of working with these headers at a low level.

Spring provides the following convenient ways to set these headers.

Using WebRequest#checkNotModified()

The following method of WebRequest transparently checks the value of the 'If-Modified-Since' request header and sets the 'Last-Modified' value in the response header as needed.

boolean checkNotModified(long lastModifiedTimestamp);

We can have WebRequest, or its implementation ServletWebRequest, as a parameter of the @RequestMapping method.

Returning ResponseEntity<T> after setting the Last-Modified value

Using the following method of ResponseEntity.BodyBuilder (inherited from its parent interface, ResponseEntity.HeadersBuilder), we can set the response's 'Last-Modified' header.

B lastModified(long lastModified);

When the corresponding ResponseEntity is returned from the handler method, the required headers will be populated, and the response will also be converted to an HTTP 304 (Not Modified) with an empty body if the conditional header 'If-Modified-Since' sent by the client is the same as the current modified date of the resource. Obviously this approach does not save on controller processing, since the full response must still be computed for each request, but it does save bandwidth by not sending the full response body to the client. If interested, check out the methods handleReturnValue() and isResourceNotModified() of HttpEntityMethodProcessor.



Example

In this example we are going to demonstrate the usage of WebRequest#checkNotModified and ResponseEntity.BodyBuilder.lastModified(..) for dynamic content, and also how static resources implicitly support the 'Last-Modified' and 'If-Modified-Since' headers.

Resource Service

package com.logicbig.example;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

@Service
public class ResourceService implements DisposableBean {

    private Path resourceFile;

    public ResourceService() throws Exception {
        File tmp = File.createTempFile("demo-resource-", ".txt");
        this.resourceFile = tmp.toPath();
        Files.write(resourceFile, "test data".getBytes(StandardCharsets.UTF_8));
    }

    public String read() throws IOException {
        String data = new String(Files.readAllBytes(resourceFile));
        return data + ", read at " +
                LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS);
    }

    public long lastModified() throws IOException {
        return Files.getLastModifiedTime(resourceFile).toMillis();
    }

    public void update(String content) throws IOException {
        Files.write(resourceFile, content.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public void destroy() throws Exception {
        Files.deleteIfExists(resourceFile);
    }
}

Controller

package com.logicbig.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import java.io.IOException;

@Controller
public class TheController {
    @Autowired
    private ResourceService resourceService;

    @ResponseBody
    @RequestMapping(value = "/test1")
    public String handle1(ServletWebRequest swr) throws IOException {

        //doesn't matter it returns false/true it will set the required headers automatically.
        //It doesn't include 'Cache-Control:no-cache' so have to do browser F5
        if (swr.checkNotModified(resourceService.lastModified())) {
            //it will return 304 with empty body
            return null;
        }

        //uncomment the following if last-modified checking is needed at every action

        return resourceService.read();
    }

    @ResponseBody
    @RequestMapping(value = "/test2")
    public ResponseEntity<String> handle2() throws IOException {

        //returning ResponseEntity with lastModified, HttpEntityMethodProcessor will
        //take care of populating/processing the required headers.
        //As the body can be replaced with empty one and 304 status can be send back,
        // this approach should be avoided if preparing the response body is very expensive.
        return ResponseEntity.ok()
                             .lastModified(resourceService.lastModified())
                             .body(resourceService.read());
    }

    @ResponseBody
    @RequestMapping(value = "/update", method = RequestMethod.POST)
    public String updateResource(@RequestBody String content) throws IOException {
        resourceService.update(content);
        return "Resource updated";
    }
}

A Filter to print information

We are going to add a filter to log the request/response headers. That will confirm the presence of the required headers. Instead of a Filter, we could have used Spring's HandlerInterceptor, but that would not intercept our static page.

@WebFilter(urlPatterns = "/*")
public class HeaderLogger implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException,
            ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rep = (HttpServletResponse) response;
        System.out.println("----- Request ---------");
        Collections
                .list(req.getHeaderNames())
                .forEach(n -> System.out.println(
                        n + ": " + req.getHeader(n)));

        chain.doFilter(request, response);

        System.out.println("----- response ---------");
        rep.getHeaderNames()
           .forEach(n -> {
               System.out.println(n + ": " + rep.getHeader(n));
           });

        System.out.println("response status: " + rep.getStatus());
    }

    @Override
    public void destroy() {
    }
}

The Config Class

@ComponentScan
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/")
                .setCachePeriod(30);
    }
}

src/main/webapp/static/static-test.html

<html>
<body>
This is a static page.
<br/>
<a href="">static-test.html</a>
</body>
</html>

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Example Outputs


/test1

$ curl -s -o page.html -z page.html "http://localhost:8080/test1" && type page.html
test data, read at 2026-09-20T00:47:20.371

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:13 GMT
Content-Length: 42
Date: Sun, 20 Sep 2026 05:47:20 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 200

In the above curl command, we used the -o flag to save the downloaded content to a specific filename and the -z flag to only download the file if the online version is newer than the local copy (a functionality that is already built into modern web browsers).


Accessing 'list1' again , a request will be sent to the server, but it will return 304 with an empty body (unless the resource has been modified on the server as the value returned the controller's getResourceLastModified() method):

$ curl -s -o page.html -z page.html "http://localhost:8080/test1" && type page.html
test data, read at 2026-09-20T00:47:20.371

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
If-Modified-Since: Sun, 20 Sep 2026 05:47:20 GMT
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:13 GMT
Date: Sun, 20 Sep 2026 05:47:38 GMT
response status: 304

Modify Resource

Let's call /update to modify the content which will modify last modified date/time as well.

$ curl -s -X POST "http://localhost:8080/update" -H "Content-Type: text/plain;charset=UTF-8" --data "updated test data"
Resource updated

Now access /test1 again

$ curl -s -o page.html -z page.html "http://localhost:8080/test1" && type page.html
updated test data, read at 2026-09-20T00:48:02.806

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
If-Modified-Since: Sun, 20 Sep 2026 05:47:20 GMT
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:51 GMT
Content-Length: 50
Date: Sun, 20 Sep 2026 05:48:02 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 200

This time it returned new content with status code 200

Access /test1 one more time

$ curl -s -o page.html -z page.html "http://localhost:8080/test1" && type page.html && del page.html
updated test data, read at 2026-09-20T00:48:02.806

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
If-Modified-Since: Sun, 20 Sep 2026 05:48:02 GMT
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:51 GMT
Date: Sun, 20 Sep 2026 05:48:10 GMT
response status: 304

Now we have the same content but with status 304, so that means the content were used from the local stored file.

/test2

This endpoint demonstrates the use of ResponseEntity with lastModified().

@Controller
public class TheController {
    @Autowired
    private ResourceService resourceService;
    .............
    @ResponseBody
    @RequestMapping(value = "/test2")
    public ResponseEntity<String> handle2() throws IOException {

        return ResponseEntity.ok()
                             .lastModified(resourceService.lastModified())
                             .body(resourceService.read());
    }
    .............
}
$ curl -s -o page2.html -z page2.html "http://localhost:8080/test2" && type page2.html
updated test data, read at 2026-09-20T00:49:12.128

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:51 GMT
Content-Length: 50
Date: Sun, 20 Sep 2026 05:49:12 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 200

Accessing /test2 one more time:

$ curl -s -o page2.html -z page2.html "http://localhost:8080/test2" && type page2.html && del page2.html
updated test data, read at 2026-09-20T00:49:12.128

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
If-Modified-Since: Sun, 20 Sep 2026 05:49:12 GMT
Host: localhost:8080
----- response ---------
Last-Modified: Sun, 20 Sep 2026 05:47:51 GMT
Date: Sun, 20 Sep 2026 05:49:17 GMT
response status: 304


/static/static-test.html

$ curl -s -o page3.html -z page3.html "http://localhost:8080/static/static-test.html" && type page3.html
<html>
<body>
This is a static page.
<br/>
<a href="">static-test.html</a>
</body>
</html>

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
Accept-Ranges: bytes
Cache-Control: max-age=30
Last-Modified: Thu, 02 Feb 2017 04:03:27 GMT
Content-Length: 96
Date: Sun, 20 Sep 2026 05:49:27 GMT
Content-Type: text/html
response status: 200

Accessing the static page again:

$ curl -s -o page3.html -z page3.html "http://localhost:8080/static/static-test.html" && type page3.html && del page3.html
<html>
<body>
This is a static page.
<br/>
<a href="">static-test.html</a>
</body>
</html>

Server Output


----- Request ---------
Accept: */*
User-Agent: curl/8.21.0
If-Modified-Since: Sun, 20 Sep 2026 05:49:27 GMT
Host: localhost:8080
----- response ---------
Last-Modified: Thu, 02 Feb 2017 04:03:27 GMT
Date: Sun, 20 Sep 2026 05:49:33 GMT
response status: 304

This is the same behavior we saw in the case of dynamic pages, which shows that the container provides implicit support for the 'Last-Modified' header for static pages. This support is based on the last-modified date of the File object.


Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 4.3.0.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • JDK 25
  • Maven 3.9.11

Spring MVC - 'Last-Modified' and 'If-Modified-Since' headers support Select All Download
  • last-modified-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TheController.java
          • webapp
            • static

    See Also

    Join