JAX RS Filters and Interceptors

1. Overview

This post describes Filters, Interceptors, and their configuration. Filters and Interceptors can be used on both sides, on the client and the server side.

Filters can modify inbound and outbound requests and responses including modification of headers, entity, and other requests/response parameters.

Interceptors are used primarily for modification of entity input and output streams. You can use Interceptors for example to zip and unzip output and input entity streams.

2. Filters

Filters can be used when you want to modify any request or response parameters like headers. For example, you would like to add a response header "X-Powered-By" to each generated response. Instead of adding this header in each resource method you would use a response filter to add this header.
There are filters on the server side and the client side.

Server filters:

  • ContainerRequestFilter
  • ContainerResponseFilter
 Client filters:
  • ClientRequestFilter
  • ClientResponseFilter 

2.1 Server Filters

ContainerResponseFilter - The following example shows a simple container response filter adding a header to each response. 
Example: Container response filter
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Response;
 
public class PoweredByResponseFilter implements ContainerResponseFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
        throws IOException {
 
            responseContext.getHeaders().add("X-Powered-By", "Jersey :-)");
    }
}
  1. In the example above the PoweredByResponseFilter always adds a header "X-Powered-By" to the response. The filter must inherit from the ContainerResponseFilter and must be registered as a provider.
  2. The filter will be executed for every response which is in most cases after the resource method is executed. Response filters are executed even if the resource method is not run, for example when the resource method is not found and 404 "Not found" response code is returned by the Jersey runtime. In this case, the filter will be executed and will process the 404 response.
  3. The filter() method has two arguments, the container request, and container response. The ContainerRequestContext is accessible only for read-only purposes as the filter is executed already in the response phase. The modifications can be done in the ContainerResponseContext.
ContainerRequestFilter - The following example shows the usage of a request filter.
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
 
public class AuthorizationRequestFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext)
                    throws IOException {
 
        final SecurityContext securityContext =
                    requestContext.getSecurityContext();
        if (securityContext == null ||
                    !securityContext.isUserInRole("privileged")) {
 
                requestContext.abortWith(Response
                    .status(Response.Status.UNAUTHORIZED)
                    .entity("User cannot access the resource.")
                    .build());
        }
    }
}
The AuthorizationRequestFilter in the example checks whether the authenticated user is in the privileged role.

Pre-Matching and Post-Matching Filters

All the request filters shown above was implemented as post-matching filters. It means that the filters would be applied only after a suitable resource method has been selected to process the actual request i.e. after request matching happens. 
Request matching is the process of finding a resource method that should be executed based on the request path and other request parameters. Since post-matching request filters are invoked when a particular resource method has already been selected, such filters can not influence the resource method matching process.
To overcome the above-described limitation, there is a possibility to mark a server request filter as a pre-matching filter, i.e. to annotate the filter class with the @PreMatching annotation. Pre-matching filters are request filters that are executed before the request matching is started. Thanks to this, pre-matching request filters have the possibility to influence which method will be matched. Such a pre-matching request filter example is shown here:
Example: Pre-matching request filter
...
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
...
 
@PreMatching
public class PreMatchingFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext)
                        throws IOException {
        // change all PUT methods to POST
        if (requestContext.getMethod().equals("PUT")) {
            requestContext.setMethod("POST");
        }
    }
}

2.2 Client Filters

Client filters are similar to container filters. The response can also be aborted in the ClientRequestFilter which would cause that no request will actually be sent to the server at all. A new response is passed to the abort method. This response will be used and delivered as a result of the request invocation. Such a response goes through the client response filters. This is similar to what happens on the server side. 
The process is shown in the following example:
public class CheckRequestFilter implements ClientRequestFilter {
 
    @Override
    public void filter(ClientRequestContext requestContext)
                        throws IOException {
        if (requestContext.getHeaders(
                        ).get("Client-Name") == null) {
            requestContext.abortWith(
                        Response.status(Response.Status.BAD_REQUEST)
                .entity("Client-Name header must be defined.")
                        .build());
         }
    }
}
The CheckRequestFilter validates the outgoing request. It is checked for presence of a Client-Name header. If the header is not present the request will be aborted with a made up response with an appropriate code and message in the entity body. This will cause that the original request will not be effectively sent to the server but the actual invocation will still end up with a response as if it would be generated by the server side. If there would be any client response filter it would be executed on this response.

3. Interceptors

Interceptors share a common API for the server and the client side. Whereas filters are primarily intended to manipulate request and response parameters like HTTP headers, URIs and/or HTTP methods, interceptors are intended to manipulate entities, via manipulating entity input/output streams. If you, for example, need to encode entity body of a client request then you could implement an interceptor to do the work for you.
There are two kinds of interceptors,
  • ReaderInterceptor
  • WriterInterceptor
Reader interceptors are used to manipulate inbound entity streams. These are the streams coming from the "wire". So, using a reader interceptor you can manipulate request entity stream on the server side (where an entity is read from the client request) and response entity stream on the client side (where an entity is read from the server response).
Writer interceptors are used for cases where the entity is written to the "wire" which on the server means when writing out a response entity and on the client side when writing request entity for a request to be sent out to the server.
Writer and reader interceptors are executed before message body readers or writers are executed and their primary intention is to wrap the entity streams that will be used in message body reader and writers.
The following example shows a writer interceptor that enables GZIP compression of the whole entity body.
Example: GZIP writer interceptor
public class GZIPWriterInterceptor implements WriterInterceptor {
 
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
                    throws IOException, WebApplicationException {
        final OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new GZIPOutputStream(outputStream));
        context.proceed();
    }
}
The interceptor gets an output stream from the WriterInterceptorContext and sets a new one which is a GZIP wrapper of the original output stream. After all, interceptors are executed the output stream lastly set to the WriterInterceptorContext will be used for serialization of the entity.

Let's now look at an example of a ReaderInterceptor
Example: GZIP reader interceptor

public class GZIPReaderInterceptor implements ReaderInterceptor {
 
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
                    throws IOException, WebApplicationException {
        final InputStream originalInputStream = context.getInputStream();
        context.setInputStream(new GZIPInputStream(originalInputStream));
        return context.proceed();
    }
}
The GZIPReaderInterceptor wraps the original input stream with the GZIPInputStream. All further reads from the entity stream will cause that data will be decompressed by this stream. The interceptor method aroundReadFrom() must return an entity. The entity is returned from the proceed method of the ReaderInterceptorContext
The proceed method internally calls the wrapped interceptor which must also return an entity. The proceed method invoked from the last interceptor in the chain calls message body reader which deserializes the entity-end returns it. Every interceptor can change this entity if there is a need but in the most cases interceptors will just return the entity as returned from the proceed method.

4. Filter and Interceptor Execution Order

Let's look closer at the context of execution of filters and interceptors. The following steps describe a scenario where a JAX-RS client makes a POST request to the server. The server receives an entity and sends a response back with the same entity. GZIP reader and writer interceptors are registered on the client and the server. Also, filters are registered on client and server which change the headers of request and response.
  • Client request invoked: The POST request with an attached entity is built on the client and invoked.
  • ClientRequestFilters: client request filters are executed on the client and they manipulate the request headers.
  • Client WriterInterceptor: As the request contains an entity, writer interceptor registered on the client is executed before a MessageBodyWriter is executed. It wraps the entity output stream with the GZipOutputStream.
  • Client MessageBodyWriter: message body writer is executed on the client which serializes the entity into the new GZipOutput stream. This stream zips the data and sends it to the "wire".
  • Server: server receives a request. Data of entity is compressed which means that pure read from the entity input stream would return compressed data.
  • Server pre-matching ContainerRequestFilters: ContainerRequestFilters are executed that can manipulate resource method matching process.
  • Server: matching: resource method matching is done.
  • Server: post-matching ContainerRequestFilters: ContainerRequestFilters post matching filters are executed. This includes execution of all global filters (without name binding) and filters name-bound to the matched method.
  • Server ReaderInterceptor: reader interceptors are executed on the server. The GZIPReaderInterceptor wraps the input stream (the stream from the "wire") into the GZipInputStream and set it to context.
  • Server MessageBodyReader: server message body reader is executed and it deserializes the entity from new GZipInputStream (get from the context). This means the reader will read unzipped data and not the compressed data from the "wire".
  • Server resource method is executed: the deserialized entity object is passed to the matched resource method as a parameter. The method returns this entity as a response entity.
  • Server ContainerResponseFilters are executed: response filters are executed on the server and they manipulate the response headers. This include all global bound filters (without name binding) and all filters name-bound to the resource method.
  • Server WriterInterceptor: is executed on the server. It wraps the original output stream with a new GZIPOuptutStream. The original stream is the stream that "goes to the wire" (output stream for response from the underlying server container).
  • Server MessageBodyWriter: message body writer is executed on the server which serializes the entity into the GZIPOutputStream. This stream compresses the data and writes it to the original stream which sends this compressed data back to the client.
  • Client receives the response: the response contains compressed entity data.
  • Client ClientResponseFilters: client response filters are executed and they manipulate the response headers.
  • Client response is returned: the javax.ws.rs.core.Response is returned from the request invocation.
  • Client code calls response.readEntity(): read entity is executed on the client to extract the entity from the response.
  • Client ReaderInterceptor: the client reader interceptor is executed when readEntity(Class) is called. The interceptor wraps the entity input stream with GZIPInputStream. This will decompress the data from the original input stream.
  • Client MessageBodyReaders: client message body reader is invoked which reads decompressed data from GZIPInputStream and deserializes the entity.
  • Client: The entity is returned from the readEntity().
Filter or interceptor can be assigned to a resource method using the @NameBinding annotation.
Example:
...
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.zip.GZIPInputStream;
 
import javax.ws.rs.GET;
import javax.ws.rs.NameBinding;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
...
 
 
// @Compress annotation is the name binding annotation
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface Compress {}
 
 
@Path("helloworld")
public class HelloWorldResource {
 
    @GET
    @Produces("text/plain")
    public String getHello() {
        return "Hello World!";
    }
 
    @GET
    @Path("too-much-data")
    @Compress
    public String getVeryLongString() {
        String str = ... // very long string
        return str;
    }
}
 
// interceptor will be executed only when resource methods
// annotated with @Compress annotation will be executed
@Compress
public class GZIPWriterInterceptor implements WriterInterceptor {
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
                    throws IOException, WebApplicationException {
        final OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new GZIPOutputStream(outputStream));
        context.proceed();
    }
}
The example above defines a new @Compress annotation which is a name binding annotation as it is annotated with @NameBinding. The comments in above example are self-describe.

6. Dynamic binding

Dynamic binding is a way how to assign filters and interceptors to the resource methods in a dynamic manner.
...
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.container.DynamicFeature;
...
 
@Path("helloworld")
public class HelloWorldResource {
 
    @GET
    @Produces("text/plain")
    public String getHello() {
        return "Hello World!";
    }
 
    @GET
    @Path("too-much-data")
    public String getVeryLongString() {
        String str = ... // very long string
        return str;
    }
}
 
// This dynamic binding provider registers GZIPWriterInterceptor
// only for HelloWorldResource and methods that contain
// "VeryLongString" in their name. It will be executed during
// application initialization phase.
public class CompressionDynamicBinding implements DynamicFeature {
 
    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (HelloWorldResource.class.equals(resourceInfo.getResourceClass())
                && resourceInfo.getResourceMethod()
                    .getName().contains("VeryLongString")) {
            context.register(GZIPWriterInterceptor.class);
        }
    }
}
The binding is done using the provider which implements DynamicFeature interface. The interface defines one configure method with two arguments, ResourceInfo and FeatureContext. ResourceInfo contains information about the resource and method to which the binding can be done.

7. Priorities

In case you register more filters and interceptors you might want to define an exact order in which they should be invoked. The order can be controlled by the @Priority annotation defined by the javax.annotation.Priority class. It's a good practice to assign a priority to filters and interceptors.
...
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
...
 
@Priority(Priorities.HEADER_DECORATOR)
public class ResponseFilter implements ContainerResponseFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext,
                    ContainerResponseContext responseContext)
                    throws IOException {
 
        responseContext.getHeaders().add("X-Powered-By", "Jersey :-)");
    }
}

Comments