Close

Spring MVC - Shallow ETag support

[Last Updated: Sep 20, 2026]

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 ETag value will be generated. ETag generation uses the MD5 algorithm, a hash function that produces a 128-bit hash value.

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



Example

In this example, we are going to register ShallowEtagHeaderFilter as a servlet filter (by using Spring Boot's FilterRegistrationBean)

Registering ShallowETagHeaderFilter

package com.logicbig.example;

import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import jakarta.servlet.Filter;

public class WebAppInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected Filter[] getServletFilters() {

        return new Filter[]{
                new HeaderLogger(), new ShallowEtagHeaderFilter()};
    }
}

Data Store

package com.logicbig.example;

import org.springframework.stereotype.Service;

@Service
public class ResourceStore {
    private String data;

    public ResourceStore() {
        data = "initial Value";
    }

    public void update(String newData) {
        data = newData;
    }

    public String getData() {
        return data;
    }
}

The 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 org.springframework.web.context.request.WebRequest;
import java.io.IOException;

@Controller
public class TheController {
    @Autowired
    private ResourceStore resourceStore;

    @ResponseBody
    @RequestMapping(value = "/test1")
    public String handle1(ServletWebRequest swr) {
        return resourceStore.getData();
    }

    @ResponseBody
    @RequestMapping(value = "/test2")
    public ResponseEntity<String> handle3(WebRequest swr) {
        return ResponseEntity
                .ok()
                .body(resourceStore.getData());
    }

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

Note that our controller is free from all ETag logic.

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

package com.logicbig.example;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

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

Running application

mvn spring-boot:run

Output

/test1

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html
initial Value

Server Output


----- Request ---------
If-None-Match: ""
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
ETag: "0260a4b029153e49e1f820ca3d1abfdb1"
Content-Length: 13
Date: Mon, 21 Sep 2026 00:21:51 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 200

Please check out the last tutorialto understand to curl options -o, --etag and --etag-compare.


On a subsequent access to /test1 without modifying the target resource:

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html
initial Value

Server Output


----- Request ---------
If-None-Match: "0260a4b029153e49e1f820ca3d1abfdb1"
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
ETag: "0260a4b029153e49e1f820ca3d1abfdb1"
Content-Length: 13
Date: Mon, 21 Sep 2026 00:25:42 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 304

Notice the ETag value did not change, but it is still evaluated and validated by ShallowEtagHeaderFilter on every request.

Modify the page content

Now modify the response content by accessing /update POST endpoint:

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

Now access /test1 again

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html
updated test data

Server Output


----- Request ---------
If-None-Match: "0260a4b029153e49e1f820ca3d1abfdb1"
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
ETag: "00cb70c511ff289a8c25e9ab250570aee"
Content-Length: 17
Date: Mon, 21 Sep 2026 00:30:53 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 200

The new content has been sent by the server. The ETag value has also changed this time.

Access /test1 again to confirm server send 304 and same ETag value

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html && del page.html && del etag.txt
updated test data

Server Output


----- Request ---------
If-None-Match: "00cb70c511ff289a8c25e9ab250570aee"
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
ETag: "00cb70c511ff289a8c25e9ab250570aee"
Content-Length: 17
Date: Mon, 21 Sep 2026 00:30:54 GMT
Content-Type: text/plain;charset=iso-8859-1
response status: 304

/test2

The outcome will be similar to what we observed for /test1, using the same actions.

/static/myStaticPage.html

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/static/myStaticPage.html" && type page.html
<html>
<body>
This is a static page.
<br/>
<a href="myStaticPage.html">myStaticPage.html</a>
</body>
</html>

Server Output


----- Request ---------
If-None-Match: ""
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
Accept-Ranges: bytes
Cache-Control: max-age=30
ETag: "08f5b2a3112e3bab25277c9506ec69460"
Last-Modified: Sat, 04 Feb 2017 03:47:02 GMT
Content-Length: 114
Date: Mon, 21 Sep 2026 00:49:08 GMT
Content-Type: text/html
response status: 200

Accessing the static page again:

$ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/static/myStaticPage.html" && type page.html && del page.html && del etag.txt
<html>
<body>
This is a static page.
<br/>
<a href="myStaticPage.html">myStaticPage.html</a>
</body>
</html>

Server Output


----- Request ---------
If-None-Match: "08f5b2a3112e3bab25277c9506ec69460"
Accept: */*
User-Agent: curl/8.21.0
Host: localhost:8080
----- response ---------
Accept-Ranges: bytes
Cache-Control: max-age=30
ETag: "08f5b2a3112e3bab25277c9506ec69460"
Last-Modified: Sat, 04 Feb 2017 03:47:02 GMT
Content-Length: 114
Date: Mon, 21 Sep 2026 00:52:05 GMT
Content-Type: text/html
response status: 304

The static page request will continue to return 304 until the content of myStaticPage.html changes.


Weak ETag support

ShallowEtagHeaderFilter supports weak ETag values. To enable this, use the following method during ShallowEtagHeaderFilter registration:

public void setWriteWeakETag(boolean writeWeakETag)

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 - ShallowEtagHeaderFilter Example Select All Download
  • spring-shallow-etag
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • WebAppInitializer.java
          • webapp
            • public

    See Also

    Join