Nugget Friday - Mastering Jakarta REST Filter Chains
Published on 07 Feb 2025

In today's Nugget Friday, we're tackling a powerful but often misunderstood feature of Jakarta REST (formerly JAX-RS): filters and filter chains. Whether you're handling security, logging, compression or other cross-cutting concerns, understanding filters is important for building reliable Jakarta REST applications on the Jakarta EE (formerly Java EE) platform. So grab your favorite beverage, and let's dig in!
The Problem
When building enterprise REST applications, you often need to perform common operations across multiple endpoints, such as logging requests, authenticating users or compressing responses. Adding this logic directly in resource methods leads to code duplication and maintenance headaches. How can we handle these cross-cutting concerns elegantly without cluttering our resource methods?
The Solution: JAX-RS Filter
At their core, filters enable you to execute code at well-defined points in the request/response processing chain. There are four main types of filters:
- ClientRequestFilter - Executes before sending a client request
- ClientResponseFilter - Executes after receiving a client response
- ContainerRequestFilter - Executes when receiving a server request
- ContainerResponseFilter - Executes before sending a server response
Let's explore each component and see how they fit together.
1. ClientRequestFilter
- What it is: This filter intercepts requests before they're sent from the client to the server. It allows you to modify the outgoing request in various ways.
- Possible uses:
- Adding custom headers to the request, e.g. for authentication or logging
- Modifying the request entity, e.g. encrypting the payload
- Logging request details for debugging
@Provider
public class MyClientRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add("X-Custom-Header", "MyValue");
}
}
2. ClientResponseFilter
- What it is: This filter intercepts responses after they're received by the client from the server. It gives you a chance to process the incoming response.
- Possible uses:
- Logging response details
- Modifying the response entity, e.g. decrypting the payload
- Handling errors or redirects
@Provider
public class MyClientResponseFilter implements ClientResponseFilter {
@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
// Log the response status code
System.out.println("Response Status Code: " + responseContext.getStatus());
}
}
3. ContainerRequestFilter
- What it is: This filter intercepts requests when they arrive at the server, before they reach your resource methods. Think of it as pre-processing for incoming requests.
- Possible uses:
- Authentication and authorization, e.g. checking API keys or JWTs
- Input validation
- Request logging and auditing
@Provider
public class MyContainerRequestFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Check for an API key in the request headers
String apiKey = requestContext.getHeaderString("X-Api-Key");
if (apiKey == null || !apiKey.equals("your_api_key")) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
}
4. ContainerResponseFilter
- What it is: This filter intercepts responses before they're sent back from the server to the client. It allows you to modify the outgoing response.
- Possible uses:
- Adding custom headers to the response, e.g. CORS headers
- Modifying the response entity
- Logging response data
@Provider
public class MyContainerResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
responseContext.getHeaders().add("X-Powered-By", "My Awesome API");
}
}
Important Notes on JAX-RS Filters:
- @Provider annotation: Make sure to use the @Provider annotation to register your filter classes with the JAX-RS runtime
- Order of execution: If you have multiple filters of the same type, you can control their execution order using the @Priority annotation
Why You Should Care
Filter chains offer several key benefits:
- Separation of Concerns: Keep your resource methods focused on business logic while handling cross-cutting concerns in filters
- Reusability: Write once, apply anywhere - filters can be reused across different resources
- Flexibility: Control exactly where and when filters are applied using binding mechanisms
- Ordered Processing: Use priorities to ensure filters execute in the correct sequence
Advanced Features and Considerations
Pre-Matching Filters
Sometimes you need to execute a filter before URI matching occurs. The @PreMatching annotation enables this:
@Provider
@PreMatching
public class HttpMethodOverrideFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) {
if (requestContext.getMethod().equalsIgnoreCase("POST")) {
String override = requestContext.getHeaders()
.getFirst("X-HTTP-Method-Override");
if (override != null) {
requestContext.setMethod(override);
}
}
}
}
Practical Filter Use Cases
Beyond security, filters can solve many common enterprise needs. Here are some practical examples:
Request/Response Timing
@Provider
public class TimingFilter implements ContainerRequestFilter,
ContainerResponseFilter {
private static final String TIMING_KEY = "request-timer";
@Override
public void filter(ContainerRequestContext requestContext) {
requestContext.setProperty(TIMING_KEY, System.nanoTime());
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
Long startTime = (Long) requestContext.getProperty(TIMING_KEY);
long duration = System.nanoTime() - startTime;
responseContext.getHeaders().add(
"X-Request-Duration",
String.format("%.2f ms", duration / 1_000_000.0)
);
if (duration > TimeUnit.SECONDS.toNanos(1)) {
logger.warning(String.format(
"Slow request to %s: %.2f ms",
requestContext.getUriInfo().getPath(),
duration / 1_000_000.0
));
}
}
}
Response Compression
@Provider
public class CompressionFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
// Get the "Accept-Encoding" header from the request
String acceptEncoding = requestContext.getHeaderString("Accept-Encoding");
// Check if the client accepts gzip compression and the response should be compressed
if (acceptEncoding != null && acceptEncoding.contains("gzip") && shouldCompress(responseContext)) {
// Get the response entity
Object entity = responseContext.getEntity();
// Set the response entity with gzip content encoding
responseContext.setEntity(entity,
responseContext.getEntityAnnotations(),
new Variant(responseContext.getMediaType(),
responseContext.getLanguage(),
new ContentEncoding("gzip")));
}
}
}
Request Tracing
@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class CorrelationFilter implements ContainerRequestFilter,
ContainerResponseFilter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
@Override
public void filter(ContainerRequestContext requestContext) {
String correlationId = requestContext.getHeaderString(CORRELATION_ID_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
requestContext.setProperty("correlationId", correlationId);
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
String correlationId =
(String) requestContext.getProperty("correlationId");
responseContext.getHeaders()
.add(CORRELATION_ID_HEADER, correlationId);
MDC.remove("correlationId");
}
}
Selective Filter Application Using Name Binding
Sometimes applying a filter globally doesn't make sense. For example, you might want detailed request logging only for certain sensitive operations, not for every endpoint in your application. This is where name binding comes in: it provides a way to selectively apply filters to specific resource methods or classes.
Let's break down how name binding works with a practical example:
1. First, create a custom binding annotation:@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Logged { }
Here's what each part means:
- @NameBinding tells Jakarta REST this is a binding annotation
- @Retention(RetentionPolicy.RUNTIME) ensures the annotation is available during execution
- @Target({ElementType.TYPE, ElementType.METHOD}) allows the annotation on both classes and methods
@Provider3. Apply the annotation to specific resource methods or classes:
@Logged
public class DetailedLoggingFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) {
String method = requestContext.getMethod();
String path = requestContext.getUriInfo().getPath();
String body = extractBody(requestContext);
logger.info("Detailed request log:");
logger.info("Method: {}", method);
logger.info("Path: {}", path);
logger.info("Headers: {}", requestContext.getHeaders());
logger.info("Body: {}", body);
logger.info("Query params: {}", requestContext.getUriInfo()
.getQueryParameters());
}
private String extractBody(ContainerRequestContext context) {
// Body extraction logic
}
}@Path("/sensitive")
public class SensitiveResource {
@GET
@Path("/user-data")
@Logged // This method will use the detailed logging
public Response getSensitiveData() {
return Response.ok(fetchSensitiveData()).build();
}
@GET
@Path("/public-data")
// No @Logged annotation - filter won't apply here
public Response getPublicData() {
return Response.ok(fetchPublicData()).build();
}
}
You can also apply the binding at the class level to have it affect all methods:
@Path("/sensitive")
@Logged // All methods in this class will use the detailed logging
public class VerySensitiveResource {
@GET
@Path("/data1")
public Response getData1() { ... }
@GET
@Path("/data2")
public Response getData2() { ... }
}
The filter will only execute for methods or classes annotated with @Logged. This gives you fine-grained control over where the filter applies.
You can even combine multiple binding annotations for more complex scenarios:
@NameBinding
public @interface Authenticated { }
@NameBinding
public @interface Logged { }
@Path("/data")
public class DataResource {
@GET
@Authenticated
@Logged // Both authentication and logging filters will apply
public Response getProtectedData() { ... }
}
This approach helps you:
- Reduce overhead by only applying filters where needed
- Create clear documentation about which endpoints have special handling
- Keep resource-intensive operations, like detailed logging, focused on critical paths
- Maintain cleaner code by making filter application explicit
Remember that if you need even more dynamic control over filter application, you can use dynamic binding through the DynamicFeature interface instead of name binding.
That's the power of selective filter application - you get precise control over where and when your filters execute, leading to better performance and clearer code organization.
Caveats
- Filter Order Matters: Be careful with filter ordering. Use @Priority to control the sequence
- Performance Impact: Each filter adds processing overhead. Use them judiciously
- Thread Safety: Filters must be thread-safe as they can handle multiple requests simultaneously
- Exception Handling: Properly handle exceptions in filters to avoid request processing failures
- Context Sharing: Use request properties to share data between filters in the chain
Conclusions
JAX-RS filters are a powerful tool in the Jakarta REST toolkit. They provide a clean, maintainable way to handle cross-cutting concerns in your REST applications. Whether you're implementing security, logging, compression or any other common functionality, filters help keep your code organized and maintainable.
Key takeaways:
- Use filters for cross-cutting concerns
- Use priorities to control execution order
- Apply filters selectively using name binding
- Consider performance implications
- Handle exceptions appropriately
That's it for this week's deep dive! Download Payara Server Community for free and start building more maintainable Jakarta REST services with filters. And stay tuned for more Jakarta EE and Java nuggets. Happy coding!
Related Posts
Join Payara at Devnexus 2025 for Java Ecosystem Insights
Published on 18 Feb 2025
by Chiara Civardi
0 Comments
Join our webinar! Modernizing Enterprise Java Applications: Jakarta EE, Spring Boot, and AI Integration
Published on 12 Feb 2025
by Nastasija Trajanova
0 Comments
Join us for a power-packed webinar in collaboration with DZone, where we’ll dive into the latest innovations in Jakarta EE, Spring Boot, and AI integration. Get ready for live coding, real-world case studies, and hands-on insights into the ...