Filters and Interceptors - REST and the JAX-RS Standard - RESTful Java with JAX-RS 2.0 (2013)

RESTful Java with JAX-RS 2.0 (2013)

Part I. REST and the JAX-RS Standard

Chapter 12. Filters and Interceptors

Filters and interceptors are objects that are able to interpose themselves on client or server request processing. They allow you to encapsulate common behavior that cuts across large parts of your application. This behavior is usually infrastructure- or protocol-related code that you don’t want to pollute your business logic with. While most JAX-RS features are applied by application developers, filters and interceptors are targeted more toward middleware and systems developers. They are also often used to write portable extensions to the JAX-RS API. This chapter teaches you how to write filters and interceptors using real-world examples.

Server-Side Filters

On the server side there are two different types of filters: request filters and response filters. Request filters execute before a JAX-RS method is invoked. Response filters execute after the JAX-RS method is finished. By default they are executed for all HTTP requests, but can be bound to a specific JAX-RS method too. Internally, the algorithm for executing an HTTP on the server side looks something like this:

for (filter : preMatchFilters) {

filter.filter(request);

}

jaxrs_method = match(request);

for (filter : postMatchFilters) {

filter.filter(request);

}

response = jaxrs_method.invoke();

for (filter : responseFilters) {

filter.filter(request, response);

}

For those of you familiar with the Servlet API, JAX-RS filters are quite different. JAX-RS breaks up its filters into separate request and response interfaces, while servlet filters wrap around servlet processing and are run in the same Java call stack. Because JAX-RS has an asynchronous API, JAX-RS filters cannot run in the same Java call stack. Each request filter runs to completion before the JAX-RS method is invoked. Each response filter runs to completion only after a response becomes available to send back to the client. In the asynchronous case, response filters run afterresume(), cancel(), or a timeout happens. See Chapter 13 for more details on the asynchronous API.

Server Request Filters

Request filters are implementations of the ContainerRequestFilter interface:

package javax.ws.rs.container;

public interface ContainerRequestFilter {

public void filter(ContainerRequestContext requestContext)

throws IOException;

}

ContainerRequestFilters come in two flavors: prematching and postmatching. Prematching ContainerRequestFilters are designated with the @PreMatching annotation and will execute before the JAX-RS resource method is matched with the incoming HTTP request. Prematching filters often are used to modify request attributes to change how they match to a specific resource. For example, some firewalls do not allow PUT and/or DELETE invocations. To circumvent this limitation, many applications tunnel the HTTP method through the HTTP headerX-Http-Method-Override:

import javax.ws.rs.container.ContainerRequestFilter;

import javax.ws.rs.container.ContainerRequestContext;

@Provider

@PreMatching

public class HttpMethodOverride implements ContainerRequestFilter {

public void filter(ContainerRequestContext ctx) throws IOException {

String methodOverride = ctx.getHeaderString("X-Http-Method-Override");

if (methodOverride != null) ctx.setMethod(methodOverride);

}

}

This HttpMethodOverride filter will run before the HTTP request is matched to a specific JAX-RS method. The ContainerRequestContext parameter passed to the filter() method provides information about the request like headers, the URI, and so on. The filter() method uses the ContainerRequestContext parameter to check the value of the X-Http-Method-Override header. If the header is set in the request, the filter overrides the request’s HTTP method by calling ContainerRequestFilter.setMethod(). Filters can modify pretty much anything about the incoming request through methods on ContainerRequestContext, but once the request is matched to a JAX-RS method, a filter cannot modify the request URI or HTTP method.

Another great use case for request filters is implementing custom authentication protocols. For example, OAuth 2.0 has a token protocol that is transmitted through the Authorization HTTP header. Here’s what an implementation of that might look like:

import javax.ws.rs.container.ContainerRequestFilter;

import javax.ws.rs.container.ContainerRequestContext;

import javax.ws.rs.NotAuthorizedException;

@Provider

@PreMatching

public class BearerTokenFilter implements ContainerRequestFilter {

public void filter(ContainerRequestContext ctx) throws IOException {

String authHeader = request.getHeaderString(HttpHeaders.AUTHORIZATION);

if (authHeader == null) throw new NotAuthorizedException("Bearer");

String token = parseToken(authHeader);

if (verifyToken(token) == false) {

throw new NotAuthorizedException("Bearer error=\"invalid_token\"");

}

}

private String parseToken(String header) {...}

private boolean verifyToken(String token) {...}

}

In this example, if there is no Authorization header or it is invalid, the request is aborted with a NotAuthorizedException. The client receives a 401 response with a WWW-Authenticate header set to the value passed into the constructor of NotAuthorizedException. If you want to avoid exception mapping, then you can use the ContainerRequestContext.abortWith() method instead. Generally, however, I prefer to throw exceptions.

Server Response Filters

Response filters are implementations of the ContainerResponseFilter interface:

package javax.ws.rs.container;

public interface ContainerResponseFilter {

public void filter(ContainerRequestContext requestContext,

ContainerResponseContext responseContext)

throws IOException;

}

Generally, you use these types of filters to decorate the response by adding or modifying response headers. One example is if you wanted to set a default Cache-Control header for each response to a GET request. Here’s what it might look like:

import javax.ws.rs.container.ContainerResponseFilter;

import javax.ws.rs.container.ContainerRequestContext;

import javax.ws.rs.container.ContainerResponseContext;

import javax.ws.rs.core.CacheControl;

@Provider

public class CacheControlFilter implements ContainerResponseFilter {

public void filter(ContainerRequestContext req, ContainerResponseContext res)

throws IOException

{

if (req.getMethod().equals("GET")) {

CacheControl cc = new CacheControl();

cc.setMaxAge(100);

req.getHeaders().add("Cache-Control", cc);

}

}

}

The ContainerResponseFilter.filter() method has two parameters. The ContainerRequestContext parameter gives you access to information about the request. Here we’re checking to see if the request was a GET. The ContainerResponseContext parameter allows you to view, add, and modify the response before it is marshalled and sent back to the client. In the example, we use the ContainerResponseContext to set a Cache-Control response header.

Reader and Writer Interceptors

While filters modify request or response headers, reader and writer interceptors deal with message bodies. They work in conjunction with a MessageBodyReader or MessageBodyWriter and are usable on both the client and server. Reader interceptors implement theReaderInterceptor interface. Writer interceptors implement the WriterInterceptor interface.

package javax.ws.rs.ext;

public interface ReaderInterceptor {

public Object aroundReadFrom(ReaderInterceptorContext context)

throws java.io.IOException, javax.ws.rs.WebApplicationException;

}

public interface WriterInterceptor {

void aroundWriteTo(WriterInterceptorContext context)

throws java.io.IOException, javax.ws.rs.WebApplicationException;

}

These interceptors are only triggered when a MessageBodyReader or MessageBodyWriter is needed to unmarshal or marshal a Java object to and from the HTTP message body. They also are invoked in the same Java call stack. In other words, a ReaderInterceptor wraps around the invocation of MessageBodyReader.readFrom() and a WriterInterceptor wraps around the invocation of MessageBodyWWriter.writeTo().

A simple example that illustrates these interfaces in action is adding compression to your input and output streams through content encoding. While most JAX-RS implementations support GZIP encoding, let’s look at how you might add support for it using a ReaderInterceptor andWriterInterceptor:

@Provider

public class GZIPEncoder implements WriterInterceptor {

public void aroundWriteTo(WriterInterceptorContext ctx)

throws IOException, WebApplicationException {

GZIPOutputStream os = new GZIPOutputStream(ctx.getOutputStream());

ctx.getHeaders().putSingle("Content-Encoding", "gzip");

ctx.setOutputStream(os);

ctx.proceed();

return;

}

}

The WriterInterceptorContext parameter allows you to view and modify the HTTP headers associated with this invocation. Since interceptors can be used on both the client and server side, these headers represent either a client request or a server response. In the example, ouraroundWriteTo() method uses the WriterInterceptorContext to get and replace the OutputStream of the HTTP message body with a GZipOutputStream. We also use it to add a Content-Encoding header. The call to WriterInterceptorContext.proceed() will either invoke the next registered WriterInterceptor, or if there aren’t any, invoke the underlying MessageBodyWriter.writeTo() method.

Let’s now implement the ReaderInterceptor counterpart to this encoding example:

@Provider

public class GZIPDecoder implements ReaderInterceptor {

public Object aroundReadFrom(ReaderInterceptorContext ctx)

throws IOException {

String encoding = ctx.getHeaders().getFirst("Content-Encoding");

if (!"gzip".equalsIgnoreCase(encoding)) {

return ctx.proceed();

}

GZipInputStream is = new GZipInputStream(ctx.getInputStream());

ctx.setInputStream(is);

return ctx.proceed(is);

}

}

The ReaderInterceptorContext parameter allows you to view and modify the HTTP headers associated with this invocation. Since interceptors can be used on both the client and server side, these headers represent either a client response or a server request. In the example, ouraroundReadFrom() method uses the ReaderInterceptorContext to first check to see if the message body is GZIP encoded. If not, it returns with a call to ReaderInterceptorContext.proceed(). The ReaderInterceptorContext is also used to get and replace theInputStream of the HTTP message body with a GZipInputStream. The call to ReaderInterceptorContext.proceed() will either invoke the next registered ReaderInterceptor, or if there aren’t any, invoke the underlying MessageBodyReader.readFrom() method. The value returned by proceed() is whatever was returned by MessageBodyReader.readFrom(). You can change this value if you want, by returning a different value from your aroundReadFrom() method.

There’s a lot of other use cases for interceptors that I’m not going to go into detail with. For example, the RESTEasy project uses interceptors to digitally sign and/or encrypt message bodies into a variety of Internet formats. You could also use a WriterInterceptor to add a JSONP wrapper to your JSON content. A ReaderInterceptor could augment the unmarshalled Java object with additional data pulled from the request or response. The rest is up to your imagination.

Client-Side Filters

The JAX-RS Client API also has its own set of request and response filter interfaces:

package javax.ws.rs.client;

public interface ClientRequestFilter {

public void filter(ClientRequestContext requestContext) throws IOException;

}

public interface ClientResponseFilter {

public void filter(ClientRequestContext requestContext,

ClientResponseContext responseContext)

throws IOException;

}

Let’s use these two interfaces to implement a client-side cache. We want this cache to behave like a browser’s cache. This means we want it to honor the Cache-Control semantics discussed in Chapter 11. We want cache entries to expire based on the metadata within Cache-Controlresponse headers. We want to perform conditional GETs if the client is requesting an expired cache entry. Let’s implement our ClientRequestFilter first:

import javax.ws.rs.client.ClientRequestFilter;

import javax.ws.rs.client.ClientRequestContext;

public class ClientCacheRequestFilter implements ClientRequestFilter {

private Cache cache;

public ClientCacheRequestFilter(Cache cache) {

this.cache = cache;

}

public void filter(ClientRequestContext ctx) throws IOException {

if (!ctx.getMethod().equalsIgnoreCase("GET")) return;

CacheEntry entry = cache.getEntry(request.getUri());

if (entry == null) return;

if (!entry.isExpired()) {

ByteArrayInputStream is = new ByteArrayInputStream(entry.getContent());

Response response = Response.ok(is)

.type(entry.getContentType()).build();

ctx.abortWith(response);

return;

}

String etag = entry.getETagHeader();

String lastModified = entry.getLastModified();

if (etag != null) {

ctx.getHeaders.putSingle("If-None-Match", etag);

}

if (lastModified != null) {

ctx.getHeaders.putSingle("If-Modified-Since", lastModified);

}

}

}

I’ll show you later how to register these client-side filters, but our request filter must be registered as a singleton and constructed with an instance of a Cache. I’m not going to go into the details of this Cache class, but hopefully you can make an educated guess of how its implemented.

Our ClientCacheRequestFilter.filter() method performs a variety of actions based on the state of the underlying cache. First, it checks the ClientRequestContext to see if we’re doing an HTTP GET. If not, it just returns and does nothing. Next, we look up the request’s URI in the cache. If there is no entry, again, just return. If there is an entry, we must check to see if it’s expired or not. If it isn’t, we create a Response object that returns a 200, “OK,” status. We populate the Response object with the content and Content-Header stored in the cache entry and abort the invocation by calling ClientRequestContext.abortWith(). Depending on how the application initiated the client invocation, the aborted Response object will either be returned directly to the client application, or unmarshalled into the appropriate Java type. If the cache entry has expired, we perform a conditional GET by setting the If-None-Match and/or If-Modified-Since request headers with values stored in the cache entry.

Now that we’ve seen the request filter, let’s finish this example by implementing the response filter:

public class CacheResponseFilter implements ClientResponseFilter {

private Cache cache;

public CacheResponseFilter(Cache cache) {

this.cache = cache;

}

public void filter(ClientRequestContext request,

ClientResponseContext response)

throws IOException {

if (!request.getMethod().equalsIgnoreCase("GET")) return;

if (response.getStatus() == 200) {

cache.cacheResponse(response, request.getUri());

} else if (response.getStatus() == 304) {

CacheEntry entry = cache.getEntry(request.getUri());

entry.updateCacheHeaders(response);

response.getHeaders().clear();

response.setStatus(200);

response.getHeaders().putSingle("Content-Type", entry.getContentType());

ByteArrayInputStream is = new ByteArrayInputStream(entry.getContent());

response.setInputStream(is);

}

}

}

The CacheResponseFilter.filter() method starts off by checking if the invoked request was an HTTP GET. If not, it just returns. If the response status was 200, “OK,” then we ask the Cache object to cache the response for the specific request URI. The Cache.cacheResponse()method is responsible for buffering the response and storing relevant response headers and the message body. For brevity’s sake, I’m not going to go into the details of this method. If instead the response code is 304, “Not Modified,” this means that we have performed a successful conditional GET. We update the cache entry with any ETag or Last-Modified response headers. Also, because the response will have no message body, we must rebuild the response based on the cache entry. We clear all the headers from ClientResponseContext and set the appropriate Content-Type. Finally we override the response’s InputStream with the buffer stored in the cache entry.

Deploying Filters and Interceptors

On the server side, filters and interceptors are deployed the same way any other @Provider is deployed. You either annotate it with @Provider and let it be scanned and automatically registered, or you add the filter or interceptor to the Application class’s classes or singletons list.

On the client side, you register filters and interceptors the same way you would register any other provider. There are a few components in the Client API that implement the Configurable interface. This interface has a register() method that allows you to pass in your filter or interceptor class or singleton instance. ClientBuilder, Client, and WebTarget all implement the Configurable interface. What’s interesting here is that you can have different filters and interceptors per WebTarget. For example, you may have different security requirements for different HTTP resources. For one WebTarget instance, you might register a Basic Auth filter. For another, you might register a token filter.

Ordering Filters and Interceptors

When you have more than one registered filter or interceptor, there may be some sensitivities on the order in which these components are executed. For example, you usually don’t want unauthenticated users executing any of your JAX-RS components. So, if you have a custom authentication filter, you probably want that filter to be executed first. Another example is the combination of our GZIP encoding example with a separate WriterInterceptor that encrypts the message body. You probably don’t want to encrypt a GZIP-encoded representation. Instead you’ll want to GZIP-encode an encrypted representation. So ordering is important.

In JAX-RS, filters and interceptors are assigned a numeric priority either through the @Priority annotation or via a programmatic interface defined by Configurable. The JAX-RS runtime sorts filters and interceptors based on this numeric priority. Smaller numbers are first in the chain:

package javax.annotation;

public @interface Priority {

int value();

}

The @Priority annotation is actually reused from the injection framework that comes with JDK 7. This annotation would be used as follows:

import javax.annotation.Priority;

import javax.ws.rs.Priorities;

@Provider

@PreMatching

@Priority(Priorities.AUTHENTICATION)

public class BearerTokenFilter implements ContainerRequestFilter {

...

}

The @Priority annotation can take any numeric value you wish. The Priorities class specifies some common constants that you can use when applying the @Priority annotation:

package javax.ws.rs;

public final class Priorities {

private Priorities() {

// prevents construction

}

/**

* Security authentication filter/interceptor priority.

*/

public static final int AUTHENTICATION = 1000;

/**

* Security authorization filter/interceptor priority.

*/

public static final int AUTHORIZATION = 2000;

/**

* Header decorator filter/interceptor priority.

*/

public static final int HEADER_DECORATOR = 3000;

/**

* Message encoder or decoder filter/interceptor priority.

*/

public static final int ENTITY_CODER = 4000;

/**

* User-level filter/interceptor priority.

*/

public static final int USER = 5000;

}

If no priority is specified, the default is USER, 5000. There’s a few Configurable.register() methods that you can use as an alternative to the @Priority annotation to manually assign or override the priority for a filter or interceptor. As mentioned before, the client classesClientBuilder, Client, WebTarget, and Invocation.Builder all implement the Configurable interface. Here’s an example of manually setting an interceptor priority using this inherited Configurable.register():

ClientBuilder builder = ClientBuilder.newBuilder();

builder.register(GZipEncoder.class, Priorities.ENTITY_CODER);

On the server side, you can inject an instance of Configurable into the constructor of your Application class:

import javax.ws.rs.core.Configurable;

@ApplicationPath("/")

public class MyApplication {

public MyApplication(@Context Configurable configurable) {

configurable.register(BearerTokenFilter.class, Priorities.AUTHENTICATION);

}

}

Personally, I prefer using the @Priority annotation, as then my filters and interceptors are self-contained. Users can just plug in my components without having to worry about priorities.

Per-JAX-RS Method Bindings

On the server side, you can apply a filter or interceptor on a per-JAX-RS-method basis. This allows you to do some really cool things like adding annotation extensions to your JAX-RS container. There are two ways to accomplish this. One is by registering an implementation of theDynamicFeature interface. The other is through annotation binding. Let’s look at DynamicFeature first.

DynamicFeature

package javax.ws.rs.container;

public interface DynamicFeature {

public void configure(ResourceInfo resourceInfo, FeatureContext context);

}

public interface ResourceInfo {

/**

* Get the resource method that is the target of a request,

* or <code>null</code> if this information is not available.

*

* @return resource method instance or null

* @see #getResourceClass()

*/

Method getResourceMethod();

/**

* Get the resource class that is the target of a request,

* or <code>null</code> if this information is not available.

*

* @return resource class instance or null

* @see #getResourceMethod()

*/

Class<?> getResourceClass();

}

The DynamicFeature interface has one callback method, configure(). This configure() method is invoked for each and every deployed JAX-RS method. The ResourceInfo parameter contains information about the current JAX-RS method being deployed. The FeatureContextis an extension of the Configurable interface. You’ll use the register() methods of this parameter to bind the filters and interceptors you want to assign to this method.

To illustrate how you’d use DynamicFeature, let’s expand on the CacheControlFilter response filter we wrote earlier in this chapter. The previous incarnation of this class would set the same Cache-Control header value for each and every HTTP request. Let’s modify this filter and create a custom annotation called @MaxAge that will allow you to set the max-age of the Cache-Control header per JAX-RS method:

package com.commerce.MaxAge;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface MaxAge {

int value();

}

The modification of the filter looks like this:

import javax.ws.rs.container.ContainerResponseFilter;

import javax.ws.rs.container.ContainerRequestContext;

import javax.ws.rs.container.ContainerResponseContext;

import javax.ws.rs.core.CacheControl;

public class CacheControlFilter implements ContainerResponseFilter {

private int maxAge;

public CacheControlFilter(int maxAge) {

this.maxAge = maxAge;

}

public void filter(ContainerRequestContext req, ContainerResponseContext res)

throws IOException

{

if (req.getMethod().equals("GET")) {

CacheControl cc = new CacheControl();

cc.setMaxAge(this.maxAge);

res.getHeaders().add("Cache-Control", cc);

}

}

}

The CacheControlFilter has a new constructor that has a max age parameter. We’ll use this max age to set the Cache-Control header on the response. Notice that we do not annotate CacheControlFilter with @Provider. Removing @Provider will prevent this filter from being picked up on a scan when we deploy our JAX-RS application. Our DynamicFeature implementation is going to be responsible for creating and registering this filter:

import javax.ws.rs.container.DynamicFeature;

import javax.ws.rs.container.ResourceInfo;

import javax.ws.rs.core.FeatureContext;

@Provider

public class MaxAgeFeature implements DynamicFeature {

public void configure(ResourceInfo ri, FeatureContext ctx) {

MaxAge max = ri.getResourceMethod().getAnnotation(MaxAge.class);

if (max == null) return;

CacheControlFilter filter = new CacheControlFilter(max.value());

ctx.register(filter);

}

}

The MaxAgeFeature.configure() method is invoked for every deployed JAX-RS resource method. The configure() method first looks for the @MaxAge annotation on the ResourceInfo’s method. If it exists, it constructs an instance of the CacheControlFilter, passing in the value of the @MaxAge annotation. It then registers this created filter with the FeatureContext parameter. This filter is now bound to the JAX-RS resource method represented by the ResourceInfo parameter. We’ve just created a JAX-RS extension!

Name Bindings

The other way to bind a filter or interceptor to a particular JAX-RS method is to use the @NameBinding meta-annotation:

package javax.ws.rs;

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target(ElementType.ANNOTATION_TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface NameBinding {

}

You can bind a filter or interceptor to a particular annotation and when that custom annotation is applied, the filter or interceptor will automatically be bound to the annotated JAX-RS method. Let’s take our previous BearerTokenFilter example and bind to a new custom@TokenAuthenticated annotation. The first thing we do is define our new annotation:

import javax.ws.rs.NameBinding;

@NameBinding

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

public @interface TokenAuthenticated {}

Notice that @TokenAuthenticated is annotated with @NameBinding. This tells the JAX-RS runtime that this annotation triggers a specific filter or interceptor. Also notice that the @Target is set to both methods and classes. To bind the annotation to a specific filter, we’ll need to annotate the filter with it:

@Provider

@PreMatching

@TokenAuthenticated

public class BearerTokenFilter implements ContainerRequestFilter {

...

}

Now, we can use @TokenAuthenticated on any method we want and the BearerTokenFilter will be bound to that annotated method:

@Path("/customers")

public class CustomerResource {

@GET

@Path("{id}")

@TokenAuthenticated

public String getCustomer(@PathParam("id") String id) {...}

}

DynamicFeature Versus @NameBinding

To be honest, I’m not a big fan of @NameBinding and lobbied for its removal from early specification drafts. For one, any application of @NameBinding can be reimplemented as a DynamicFeature. Second, using @NameBinding can be pretty inefficient depending on your initialization requirements. For example, let’s reimplement our @MaxAge example as an @NameBinding. The filter class would need to change as follows:

import javax.ws.rs.container.ContainerResponseFilter;

import javax.ws.rs.container.ContainerRequestContext;

import javax.ws.rs.container.ContainerResponseContext;

import javax.ws.rs.core.CacheControl;

@MaxAge

@Provider

public class CacheControlFilter implements ContainerResponseFilter {

@Context ResourceInfo info;

public void filter(ContainerRequestContext req, ContainerResponseContext res)

throws IOException

{

if (req.getMethod().equals("GET")) {

MaxAge max = info.getMethod().getAnnotation(MaxAge.class);

CacheControl cc = new CacheControl();

cc.setMaxAge(max.value());

req.getHeaders().add("Cache-Control", cc);

}

}

}

If we bound CacheControlFilter via a name binding, the filter class would have to inject ResourceInfo, then look up the @MaxAge annotation of the JAX-RS method so it could determine the actual max age value to apply to the Cache-Control header. This is less efficient at runtime than our DynamicFeature implementation. Sure, in this case the overhead probably will not be noticeable, but if you have more complex initialization scenarios the overhead is bound to become a problem.

Exception Processing

So what happens if a filter or interceptor throws an exception? On the server side, the JAX-RS runtime will process exceptions in the same way as if an exception were thrown in a JAX-RS method. It will try to find an ExceptionMapper for the exception and then run it. If an exception is thrown by a ContainerRequestFilter or ReaderInterceptor and mapped by an ExceptionMapper, then any bound ContainerResponseFilter must be invoked. The JAX-RS runtime ensures that at most one ExceptionMapper will be invoked in a single request processing cycle. This avoids infinite loops.

On the client side, if the exception thrown is an instance of WebApplicationException, then the runtime will propagate it back to application code. Otherwise, the exception is wrapped in a javax.ws.rs.client.ProcessingException if it is thrown before the request goes over the wire. The exception is wrapped in a javax.ws.rs.client.ResponseProcessingException when processing a response.

Wrapping Up

In this chapter we learned about client- and server-side filters and interceptors. Filters generally interact with HTTP message headers, while interceptors are exclusive to processing HTTP message bodies. Filters and interceptors are applied to all HTTP requests by default, but you can bind them to individual JAX-RS resource methods by using DynamicFeature or @NameBinding. Chapter 26 walks you through a bunch of code examples that show most of these component features in action.