Understanding OncePerRequestFilter in Spring Boot

Spring Boot OncePerRequestFilter

When building web applications with Spring Boot, you often need to perform certain actions for each HTTP request. However, in some cases, the same request might be processed multiple times due to internal forwarding or inclusion, leading to redundant executions of your logic. Spring provides the OncePerRequestFilter to handle such scenarios, which ensures your filter logic is executed only once per request. This blog will deep dive into the OncePerRequestFilter, understand its internal workings, explore use cases and see how to implement it effectively.

Introduction

OncePerRequestFilter is an abstract class provided by the Spring Framework that guarantees a filter is invoked only once per request, even if the request is forwarded or included multiple times. This is particularly useful for filters that should not be executed more than once in the same request processing lifecycle.

Features

  • Single Execution: Ensures that the filter logic is executed only once per request.
  • Simplicity: Simplifies filter implementation by handling the complexity of ensuring single execution internally.

Use Cases

  1. Logging Requests: Ensuring request details are logged only once per request, regardless of internal forwarding or inclusion.
  2. Security Filters: Checking authentication tokens or user permissions once per request to avoid redundant security checks.
  3. Performance Monitoring: Collecting performance metrics for each request, such as recording the time taken to process a request, ensuring accurate metrics.
  4. Cross-Site Request Forgery (CSRF) Protection: Checking and validating CSRF tokens once per request to ensure the request’s integrity.
  5. Custom Header Injection: Injecting custom headers into the response once per request to ensure headers are added consistently.

Implementation

To create a custom filter, extend the OncePerRequestFilter abstract class and override the doFilterInternal method, where you will implement your custom filter logic. 

@Component
public class LoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // Log request details
        System.out.println("Request URI: " + request.getRequestURI());
        
        // Continue the filter chain
        filterChain.doFilter(request, response);
    }
}

In this example, LoggingFilter ensures that request details are logged only once per request, even if the request is forwarded or included within the server. Here, Spring Boot automatically detects and registers the bean because the class is annotated with @Component.

Internal Working of OncePerRequestFilter

The OncePerRequestFilter works by using a request attribute to track whether the filter has already been executed for the current request. This request attribute a crucial part of the OncePerRequestFilter and by avoiding redundant executions, it improves the performance of the application. It acts as a flag to mark whether the filter has already been processed for the current request.

How it works

Lets look at the below source code of OncePerRequestFilter class.

public abstract class OncePerRequestFilter extends GenericFilterBean {
    public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";


    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request instanceof HttpServletRequest httpRequest) {
            if (response instanceof HttpServletResponse httpResponse) {
                String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
                boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
                if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
                    if (hasAlreadyFilteredAttribute) {
                        if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
                            this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
                            return;
                        }

                        filterChain.doFilter(request, response);
                    } else {
                        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

                        try {
                            this.doFilterInternal(httpRequest, httpResponse, filterChain);
                        } finally {
                            request.removeAttribute(alreadyFilteredAttributeName);
                        }
                    }
                } else {
                    filterChain.doFilter(request, response);
                }

                return;
            }
        }

        throw new ServletException("OncePerRequestFilter only supports HTTP requests");
    }

    private boolean skipDispatch(HttpServletRequest request) {
        if (this.isAsyncDispatch(request) && this.shouldNotFilterAsyncDispatch()) {
            return true;
        } else {
            return request.getAttribute("jakarta.servlet.error.request_uri") != null && this.shouldNotFilterErrorDispatch();
        }
    }

    protected boolean isAsyncDispatch(HttpServletRequest request) {
        return DispatcherType.ASYNC.equals(request.getDispatcherType());
    }

    protected boolean isAsyncStarted(HttpServletRequest request) {
        return WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted();
    }

    protected String getAlreadyFilteredAttributeName() {
        String name = this.getFilterName();
        if (name == null) {
            name = this.getClass().getName();
        }

        return name + ".FILTERED";
    }

    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return false;
    }

    protected boolean shouldNotFilterAsyncDispatch() {
        return true;
    }

    protected boolean shouldNotFilterErrorDispatch() {
        return true;
    }

    protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException;

    protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);
    }
}

Below are key points of the OncePerRequestFilter class.

  • Type Checking: Ensure the request and response are HTTP-specific.
  • Attribute Check and Dispatcher Type Handling:
  • getAlreadyFilteredAttributeName: Constructs the attribute name by combining the filter name or class name with the ALREADY_FILTERED_SUFFIX.
  • skipDispatch: Determines whether the current dispatch type should be skipped. It checks for async dispatch and error dispatch conditions.
  • isAsyncDispatch: Checks if the dispatcher type of the request is ASYNC.
  • Filter Execution:
  • If the attribute is found and the dispatcher type is not ERROR, continue with the filter chain.
  • If the attribute is not found, set the attribute, execute doFilterInternal.
  • After doFilterInternal completes, the request and response are passed down to the next filter in the chain.
  • Once all the remaining filters in the chain have been executed and the request processing is complete, the attribute is removed.

Supporting Methods of OncePerRequestFilter

  1. skipDispatch: To skip the filter for FORWARD and INCLUDE dispatcher types, you can override the skipDispatch method in your custom filter class.
  2. isAsyncDispatch: Checks if the request is an async dispatch.
  3. isAsyncStarted: Checks if async processing has started.
  4. getAlreadyFilteredAttributeName: Constructs a unique attribute name to mark the request as filtered.
  5. shouldNotFilter: Can be overridden to define custom skip conditions.
  6. shouldNotFilterAsyncDispatch: Defines whether to skip async dispatches.
  7. shouldNotFilterErrorDispatch: Defines whether to skip error dispatches.
  8. doFilterInternal: Abstract method to be implemented by subclasses for the actual filter logic.
  9. doFilterNestedErrorDispatch: Handles nested error dispatches.

Conclusion

The OncePerRequestFilter is a powerful feature in Spring Boot for ensuring a single execution of filter logic per request. It simplifies the development of filters that should not be executed multiple times in the same request lifecycle, enhancing efficiency and consistency. By understanding its internal workings and exploring various use cases, you can leverage OncePerRequestFilter to create robust and reliable filters for your Spring Boot applications.

Leave a Reply

Your email address will not be published. Required fields are marked *

*