JAX-RS Providers Part II

Let's continue exploring different JAX-RS providers and dive into

  • Filters: one of the foremost components of the JAX-RS request processing chain

  • Interceptors: work in tandem with (intercept) Message Body Readers and Writers

Filters

Filters provide AOP (Aspect Oriented Programming) like capabilities within JAX-RS applications and allow developers to implement cross cutting application specific concerns which ideally should not be sprinkled all over the business logic e.g. authentication, authorization, request/response validation etc. The AOP-based programming model involves interposing on methods of JAX-RS resource classes and dealing with (or mutating) components of HTTP request/response - headers, request URIs, the invoked HTTP method (GET, POST etc)

Server side Request filter

Server side request filters act on incoming HTTP requests from the clients and provide an opportunity to act on/make decisions based on certain characteristics of the HTTP request. In order to implement a server side request filter, one needs to implement the javax.ws.rs.container.ContainerRequestFilter interface (which is an extension provided by JAX-RS). An instance of the javax.rs.ws.ContainerRequestContext interface is seamlessly injected by the container into the filter method of ContainerRequestFilter. It is a mutable object (on purpose) and exposes methods to access and modify HTTP request components

A JAX-RS request processing pipeline involves dispatching a HTTP request to the appropriate Java method in the resource classes based on matching algorithm implemented by the JAX-RS provider. Filters take this into account and are divided into pre and post matching filters

Pre-matching filters

As the name indicates, Pre-matching filters are executed before the incoming HTTP request is mapped/dispatched to a Java method. Use the javax.ws.rs.container.PreMatching annotation on the filter implementation class

//Server side Pre Matching filter

@Provider
@PreMatching
public class PreMatchingAuthFilter{
 public void filter(ContainerRequestContext crc) 
 throws IOException{
  if(crc.getHeaderString("Authorization") == null){
   crc.abortWith(Response.status(403).build());
   }else{ //check credentials.... }
  }
}

Post-matching filters

A Post-matching filter is executed by the JAX-RS container only after the completion of method dispatch/matching process. Unlike, pre-matching filters, these filters do not need an explicit annotation i.e. filter classes without the @PreMatching annotation are assumed to be post-matching by default

//Server side Post Matching filter

@Provider
public class PostMatchingFilterExample{
 public void filter(ContainerRequestContext crc)
 throws IOException{
  System.out.println("Referrer: "+ 
  crc.getHeaderString("referrer"));
  System.out.println("Base URI: "+ 
  crc.getUriInfo().getBaseUri());
  System.out.println("HTTP Request method: "+ 
  crc.getMethod());
  }
}

A JAX-RS application can have multiple filters (in a chain like structure) which are executed as per user defined order (more on this later) or a default one (container driven). However, it possible to break the chain of processing by throwing an exception from the filter implementation logic or by calling the abortWith method. In either cases, the other request filters in the chain are not invoked and the control is passed on to the Response Filters (if any).

Server side Response filter

A server side Response filter is invoked by the runtime after a response (or an exception) is generated by the JAX-RS resource method (before dispatching the same to the caller/client). Response filters are similar to their counterparts (Request filters) in terms of their utility (read/mutate aspects of the response e.g. HTTP headers) and programming model (executed as a chain in a user defined or default order) Setting up a server side response filter is as simple as providing an implementation for the javax.ws.rs.container.ContainerResponseFilter interface. The injection of ContainerResponseContext into the filter method of the ContainerResponseFilter interface is taken care of by the JAX-RS runtime

Response filters also need to be annotated with the javax.ws.rs.ext.Provider annotation in order for the JAX-RS runtime to recognize it automatically.

//Server side Response filter

@Provider
public class CustomerHeaderResponseFilter{
 public void filter(ContainerRequestContext crc,
 ContainerResponseContext resCtx) 
 throws IOException{
  System.out.println("Request URI: "+ 
  crc.getUriInfo().getAbsolutePath().toString());
  //adding a custom header to the response
  resCtx.getHeaders().add("X-Search-ID",
  "qwer1234-tyuio5678-asdfg9876");
  }
}

Client side Request filter

Client side request filters are invoked after the HTTP request creation before it is dispatched to the server. They can be to mutate/make decisions based on the properties of the HTTP request (Headers, Cookies etc) In order to implement a client side request filter, one needs to implement the extension interface provided by JAX-RS - javax.ws.rs.client.ClientRequestFilter

//Client side Request filter

public class ClientRequestHTTPMethodFilter {
 public void filter(ClientRequestContext crc) 
 throws IOException{
  String method = crc.getMethod();
  if(method.equalsIgnoreCase("DELETE")){
   //Return HTTP 405 - Method Not Allowed
   crc.abortWith(Response.status(405).build());
   }
  }
}

Client side Response filters

Client side response filters are invoked after the HTTP response has been received from the server end but before it is dispatched to the caller/client. It provides an opportunity to mutate the properties of the HTTP response (Headers, Cookies etc) In order to implement a client side response filter, one needs to implement the extension interface provided by JAX-RS - javax.ws.rs.client.ClientResponseFilter

//Client side Response filter

public class ClientResponseLoggerFilter {
 public void filter(ClientRequestContext reqCtx, 
 ClientResponseContext resCtx) throws IOException{
  System.out.println("Response status: "+
  resCtx.getStatus());
 }
}

Sharing data between JAX-RS filters

JAX-RS API enables sharing of user-defined data amongst filters associated with a particular request

  • It is abstracted in the form of a Map (pretty natural choice) via the ContainerRequestContext interface

  • Get all the custom properties using the getPropertyNames() method

  • The value of a specific property can be fetched (from the Map) using getProperty(String name)

  • Overwrite an existing property or a add a new one using setProperty(String name, Object val)

The same capability is available in the Client side JAX-RS filters as well. The only difference is that you would be interacting with an instance of the ClientRequestContext

{title="",lang=java}

public class ReqFilter_1 implements ContainerRequestFilter {

  @Override
  public void filter(ContainerRequestContext cReqCtx) throws IOException {
    cReqCtx.setProperty("prop1", "value1");
  }
}

public class ReqFilter_2 implements ContainerRequestFilter {

  @Override
  public void filter(ContainerRequestContext cReqCtx) throws IOException {
    String val1 = (String) cReqCtx.getProperty("prop1");
    cReqCtx.setProperty("prop1", "value1");
    cReqCtx.setProperty("prop2", "value2");
  }
}

public class ReqFilter_3 implements ContainerRequestFilter {

  @Override
  public void filter(ContainerRequestContext cReqCtx) throws IOException {
    String val1 = (String) cReqCtx.getProperty("prop1");
    String val2 = (String) cReqCtx.getProperty("prop2");
    Collection<String> customProperties = cReqCtx.getPropertyNames();
  }
}
//Sharing contextual data b/w Request and Response filters

@Priority(Priorities.AUTHENTICATION)
public class ReqFilter_1 implements ContainerRequestFilter {

  @Override
  public void filter(ContainerRequestContext cReqCtx) throws IOException {
    //generated and used internally
    cReqCtx.setProperty("random-token", "token-007"); 
  }
}

public class ResponseFilter implements ContainerResponseFilter {

  @Override
  public void filter(ContainerRequestContext cReqCtx, ContainerResponseContext cRespCtx) throws IOException {
       //get the property
    String responseToken = (String) cReqCtx.getProperty("random-token"); 
    if(responseToken!=null){
      //set it to HTTP response 
      cRespCtx.getHeaders.put("random-token-header" , responseToken); header
    }
  }
}

Interceptors

Interceptors are similar to filters in the sense that they are also used to mutate HTTP requests and responses, but the major difference lies in the fact that Interceptors are primarily used to manipulate HTTP message payloads. They are divided into two categories - javax.ws.rs.ext.ReaderInterceptor and javax.ws.rs.ext.WriterInterceptor for HTTP requests and responses respectively.

The same set of interceptors are applicable on the client side as well (unlike filters)

Reader Interceptor

A ReaderInterceptor is a contract (extension interface) provided by the JAX-RS API. On the server side, a Reader Interceptor acts on HTTP payloads sent by the client while the client side reader interceptors are supposed to act on (read/mutate) the request payload prior to it being sent to the server

Writer Interceptor

On the server side, a WriterInterceptor act on HTTP payloads produced by the resource methods while the client side writer interceptors are supposed to act on (read/mutate) the payload sent by the server prior to it being dispatched to the caller

Interceptors are invoked in a chain like fashion (similar to filters). They are only triggered when entity providers (MessageBodyReaderand MessageBodyWriter) are required to convert HTTP message to and from their Java object representations. Both ReaderInterceptor and WriterInteceptor wrap around MessageBodyReader and MessageBodyWriter respectively and hence executed in the same call stack.

Binding strategies for JAX-RS filters and interceptors

JAX-RS 2.0 defines multiple ways using which server side filters and interceptors can be bound to their target components.

  • Global Binding

  • Named Binding

  • Dynamic Binding

Global Binding

By default, JAX-RS filters and interceptors are bound to all the methods of resource classes in an application. That is, both request (pre and post) and response filters will be invoked whenever any resource method is invoked in response to a HTTP request by the client. This convention can be overridden using named binding or dynamic binding.

Named Binding

Filters and interceptors scoping can be handled in a fine-grained manner (based on per resource class/method)

Configuring Named Binding

//Step 1: Define a custom annotation with the @NamedBinding annotation

@NameBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Audited { }

//Step 2: Apply the custom annotation on the filter or interceptor

@Provider
@Audited
public class AuditFilter implements ContainerRequestFilter {
    //filter implementation....
}

//Step 3: Apply the same annotation to the required resource class or method

@GET
@Path("{id}")
@Produces("application/json")
@Audited
public Response find(@PathParam("id") String custId){
//search and return customer info
}

If it is applied to a class, the filter/interceptor will be bound to all its resource methods

Dynamic Binding

JAX-RS provides the DynamicFeature interface to help bind filters and interceptors dynamically at runtime. They can be used in tandem with the more static way of binding made possible using @NamedBinding. The injected instance of the ResourceInfo interface helps you choose the resource method in dynamic fashion by exposing various methods and the FeatureContext interface allows us to register the filter or interceptor once the resource method has been selected.

//Dynamic Binding sample

@Provider
public class AuthFilterDynamic implements DynamicFeature {
    @Override
    public void configure(ResourceInfo resInfo, FeatureContext ctx) {
        if (UserResource.class.equals(resInfo.getResourceClass()) && 
           resInfo.getResourceMethod().getName().contains("PUT")) {
              ctx.register(AuthenticationFilter.class);
           }
      }
}

Last updated