Spring MVC - Shallow ETag support

[Updated: Feb 4, 2017, Created: Feb 4, 2017]

ShallowEtagHeaderFilter automatically generates an ETag value based on the content of the response. That means if we change the content of the target resource, a new value for ETag will be generated . The ETag generation is done using MD5 algorithm which is a hash function to produce 128 bit hash value.

ShallowEtagHeaderFilter also takes care of populating response ETag header and validating request header If-None-Match value. That means client code is entirely unaware of any ETag logic.



Example

In this example we are going to register ShallowEtagHeaderFilter as servlet filter (by using Spring boot FilterRegistrationBean)

Registering ShallowETagHeaderFilter

@SpringBootApplication
public class Main {
    .............
  @Bean
  FilterRegistrationBean shallowEtagBean () {
      FilterRegistrationBean frb = new FilterRegistrationBean();
      frb.setFilter(new ShallowEtagHeaderFilter());
      frb.addUrlPatterns("/test1", "/test2", "/public/myStaticPage.html");
      frb.setOrder(2);
      return frb;
  }
    .............
}

The Controller

@Controller
public class TheController {

  @RequestMapping(value = "/test1")
  public String handle1 (ServletWebRequest swr) {
      return "myView";
  }

  @ResponseBody
  @RequestMapping(value = "/test2")
  public String handle2 (WebRequest swr) {

      String testBody = "<p>Response content: content 2 " +
                "</p><a href='test2'>test2</a>";
      return testBody;
  }

  @ResponseBody
  @RequestMapping(value = "/test3")
  public ResponseEntity<String> handle3 (WebRequest swr) {

      String testBody = "<p>Response content: content 2 " +
                "</p><a href='test2'>test2</a>";
      return ResponseEntity
                .ok()
                .body(testBody);
  }
}

Note that our controller is free from all ETag logic.

src/main/webapp/WEB-INF/pages/myView.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ page import="java.time.LocalDateTime"%>
<html>
  <body style="margin:20px;">
  JSP page
  <p>
  The page content:<br/> content 1
  </p>
<a href='test1'>test1</a>
  </body>
</html>

src/main/resources/application.properties

spring.mvc.view.prefix= /WEB-INF/pages/
spring.mvc.view.suffix= .jsp

src/main/webapp/public/myStaticPage.html

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

A Servlet Filter to log request and response headers

public class HeaderLogger 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 () {
  }
}

The boot main class

@SpringBootApplication
public class Main {

  public static void main (String[] args) {
      SpringApplication sa = new SpringApplication(Main.class);
      sa.setLogStartupInfo(false);
      sa.setBannerMode(Banner.Mode.OFF);
      sa.run(args);
  }

  @Bean
  FilterRegistrationBean shallowEtagBean () {
      FilterRegistrationBean frb = new FilterRegistrationBean();
      frb.setFilter(new ShallowEtagHeaderFilter());
      frb.addUrlPatterns("/test1", "/test2", "/public/myStaticPage.html");
      frb.setOrder(2);
      return frb;
  }

  @Bean
  FilterRegistrationBean headerLogger () {
      FilterRegistrationBean frb = new FilterRegistrationBean();
      frb.setFilter(new HeaderLogger());
      frb.addUrlPatterns("/test1", "/test2", "/public/myStaticPage.html");
      frb.setOrder(1);
      return frb;
  }
}

Running application

mvn spring-boot:run

Output

/test1

Console output on the first access:

----- Request ---------
host: localhost:8080
connection: keep-alive
pragma: no-cache
cache-control: no-cache
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
referer: http://localhost:8080/test1
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=B59E06D9342F9A77D86A0778C99746A2
----- response ---------
Set-Cookie: JSESSIONID=310C25B65A18F57E7D5BDCBB9D82D704;path=/;HttpOnly
ETag: "08f096bad1758c6a20093dc83165c6513"
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 151
Date: Sat, 04 Feb 2017 23:17:12 GMT
response status: 200

Notice ETag value "08f096bad1758c6a20093dc83165c6513" is generated by ShallowEtagHeaderFilter.


On subsequent reloads-F5 (without modifying the target resource):

----- Request ---------
host: localhost:8080
connection: keep-alive
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
referer: http://localhost:8080/test1
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=310C25B65A18F57E7D5BDCBB9D82D704
if-none-match: "08f096bad1758c6a20093dc83165c6513"
----- response ---------
ETag: "08f096bad1758c6a20093dc83165c6513"
response status: 304

Notice Etag value did not change but it is evaluated and validated by ShallowEtagHeaderFilter on every request.

Now modify the content of myView.jsp and refresh (in boot, if running in exploded form, we don't have to restart for JSP changes):

----- Request ---------
host: localhost:8080
connection: keep-alive
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
referer: http://localhost:8080/test1
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=310C25B65A18F57E7D5BDCBB9D82D704
if-none-match: "003ab62975bb1fb6136f25a63ba8e9349"
----- response ---------
ETag: "076184173c89ee3a02f9ddbd7c8e735fc"
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 160
Date: Sat, 04 Feb 2017 23:25:50 GMT
response status: 200

The new content has sent by the server (Content-Length: 160). Also the ETag value has changed this time.

/test2

The outcome will be the similar on the same actions we used for /test1.

/test3

It will also produce the same outcome.

/public/myStaticPage.html

It will also produce the same outcome.


Weak Etag support

ShallowEtagHeaderFilter supports for weak ETag value, we have to use following method during ShallowEtagHeaderFilter registration:

public void setWriteWeakETag(boolean writeWeakETag)

Example Project

Dependencies and Technologies Used :

  • spring-boot-starter-web 1.4.4.RELEASE: Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container.
    Corresponding Spring version: 4.3.6.RELEASE
  • tomcat-embed-jasper 8.5.11: Core Tomcat implementation.
  • JDK 1.8
  • Maven 3.3.9

Spring Mvc Shallow Etag Header Filter Select All Download
  • spring-shallow-etag
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • resources
        • webapp
          • WEB-INF
            • pages
          • public

See Also