Servlet - Working with 'Last-Modified' and 'If-Modified-Since' headers

[Updated: Feb 2, 2017, Created: Jan 31, 2017]

The response header 'Last-Modified' and request header 'If-Modified-Since' are used to avoid unnecessary transferring of resources if they are not changed. Please check out the tutorial on the basics of these headers if not already familiar.

HttpServlet provides following method to be overridden by the sub-classes to make use of the headers under discussion.

protected long getLastModified(HttpServletRequest req) {
    return -1;
}

The subclasses should return the time at which requested resource was last modified in milliseconds since 1970-01-01 00:00:00 GMT. If the time is unknown, this method returns a negative number (the default). This method is based on template method pattern. HttpServlet#service method applies the logic based on the two header specifications and to know the last modified millis of the resource it defers to the subclass by calling the getLastModified(..) method.

In this tutorial we will first show how to use the two headers without overriding above method (that's because we want to have a better understanding of low level usage of the two headers) and then by overriding it.



Example without overriding getLastModified()

The servlet

@WebServlet(name = "testServlet1",
        urlPatterns = {"/test1"},
        loadOnStartup = 1)
public class MyServlet1 extends HttpServlet {

  @Override
  protected void doGet (HttpServletRequest req,
                        HttpServletResponse resp)
            throws ServletException, IOException {

      long lastModifiedFromBrowser = req.getDateHeader("If-Modified-Since");
      long lastModifiedFromServer = getLastModifiedMillis();

      if (lastModifiedFromBrowser != -1 &&
                lastModifiedFromServer <= lastModifiedFromBrowser) {
          //setting 304 and returning with empty body
          resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
          return;
      }

      resp.addDateHeader("Last-Modified", lastModifiedFromServer);
      //uncomment to force revalidate on all actions
      //resp.addHeader("Cache-Control", "no-cache");
      resp.setContentType("text/html");
      PrintWriter writer = resp.getWriter();
      writer.write("<h4>My Servlet 1</h4>");
      writer.write(LocalDateTime.now().toString());
      writer.write("<br/><a href='test1'>test1</a>");
  }

  private static long getLastModifiedMillis () {
      //Using hard coded value, in real scenario there should be for example
      // last modified date of this servlet or of the underlying resource
      ZonedDateTime zdt = ZonedDateTime.of(LocalDateTime.of(2017, 1, 8,
                                                            10, 30, 20),
                                           ZoneId.of("GMT"));
      return zdt.toInstant().toEpochMilli();
  }
}

The above logic should be clear after reading the basics of the 'Last-Modified' and 'If-Modified-Since' headers.

Logging request and response headers

We are going to create a filter to log request and response headers, and response status code to see what's going on in each step.

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

  @Override
  public void init (FilterConfig filterConfig) throws ServletException {
  }

  @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 () {
  }
}

Running the web application

Run the embedded Jetty (We have included the jetty plugin in pom.xml).

mvn jetty:run

Output at /test1

For the very first time when we access (when there's no cache in the browser exists. We can also use Ctrl + F5 in the browser to ignore the page cache, if it already exists):

----- Request ---------
Cookie: JSESSIONID=0417EB7939AC0696D807329DFE69AAFD
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36
Host: localhost:8080
Pragma: no-cache
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Date: Wed, 01 Feb 2017 03:56:22 GMT
Last-Modified: Sat, 07 Jan 2017 10:30:20 GMT
Content-Type: text/html;charset=utf-8
response status: 200

On refreshing the page again:

----- Request ---------
Cookie: JSESSIONID=0417EB7939AC0696D807329DFE69AAFD
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36
If-Modified-Since: Sat, 07 Jan 2017 10:30:20 GMT
Host: localhost:8080
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Date: Wed, 01 Feb 2017 03:57:14 GMT
response status: 304

The content of the browser remains same.

Clicking on the link 'test1' doesn't make any server call, it just uses local cache without revalidation. If we want to enable validation at this action as well, then we have to include 'Cache-Control:no-cache', just uncomment the related line in MyServlet1.java.

Now increase the date in MyServlet1#getLastModifiedMillis(..) and restart. Refresh the page, we will notice the date displayed on the page will change:

 ----- Request ---------
Cookie: JSESSIONID=0417EB7939AC0696D807329DFE69AAFD
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36
If-Modified-Since: Sat, 07 Jan 2017 10:30:20 GMT
Host: localhost:8080
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Date: Wed, 01 Feb 2017 04:05:59 GMT
Last-Modified: Sun, 08 Jan 2017 10:30:20 GMT
Content-Type: text/html;charset=utf-8
response status: 200

Example with overriding getLastModified()

@WebServlet(name = "testServlet2",
        urlPatterns = {"/test2"},
        loadOnStartup = 1)
public class MyServlet2 extends HttpServlet {

  @Override
  protected void doGet (HttpServletRequest req,
                        HttpServletResponse resp)
            throws ServletException, IOException {

      resp.setContentType("text/html");
      PrintWriter writer = resp.getWriter();
      writer.write("<h4>My Servlet 2</h4>");
      writer.write(LocalDateTime.now().toString());
      writer.write("<br/><a href='test2'>test2</a>");
  }

  @Override
  protected long getLastModified (HttpServletRequest req) {
      return getLastModifiedMillis();
  }

  private static long getLastModifiedMillis () {
      ZonedDateTime zdt = ZonedDateTime.of(LocalDateTime.of(2017, 1, 1,
                                                            10, 30, 20),
                                           ZoneId.of("GMT"));
      return zdt.toInstant().toEpochMilli();
  }
}

Note that all header logic is transparent now, we just have to return the resource last modified date from getLastModified method.

The output will be the same as above example. Also the default logic of populating headers in HttpServlet doesn't include 'Cache-Control:no-cache', so actions like accessing the resource through a link or by entering the link in the address bar will use the browser cache directly and will no send a request to the server to check the validity of the cache.

Static pages

By default servlet containers implicitly set 'Last-Modified' and 'If-Modified-Since' for the static resources, by making use of the last modified date time of the resource on the file system, so we don't have to provide an application specified last modified date. Let's check that out with an example.

src/main/webapp/static-resources/staticPage.html

<html>
<h4>A static page</h4>
Version 1.
<br/><a href='staticPage.html'>staticPage.html</a>
</html>

Output at very first access:

----- Request ---------
Cookie: JSESSIONID=0417EB7939AC0696D807329DFE69AAFD
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36
Host: localhost:8080
Pragma: no-cache
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Accept-Ranges: bytes
Last-Modified: Wed, 01 Feb 2017 05:00:51 GMT
Content-Length: 130
Date: Wed, 01 Feb 2017 05:02:39 GMT
Content-Type: text/html
response status: 200

Notice, Last-Modified header was sent implicitly by the container. Subsequent request will just return 304 with empty body (browser will use the cached copy) until we modify the static page.

----- Request ---------
Cookie: JSESSIONID=0417EB7939AC0696D807329DFE69AAFD
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36
Referer: http://localhost:8080/static-resources/staticPage.html
If-Modified-Since: Wed, 01 Feb 2017 05:06:15 GMT
Host: localhost:8080
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Date: Wed, 01 Feb 2017 05:06:45 GMT
response status: 304

Example Project

Dependencies and Technologies Used :

  • javax.servlet-api 3.1.0 Java Servlet API
  • jetty-maven-plugin 9.4.1.v20170120: Jetty maven plugins.
  • JDK 1.8
  • Maven 3.3.9

Last Modified And If Modified Since Example Select All Download
  • servlet-last-modified-header
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • webapp
          • static-resources

See Also