
The Payara Monthly Catch -September 2025
Welcome aboard the September issue of The Monthly Catch! With summer holidays wrapping up, the Java world is back […]
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!
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?
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:
Let’s explore each component and see how they fit together.
@Provider
public class MyClientRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add("X-Custom-Header", "MyValue");
}
}
@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());
}
}
@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());
}
}
}
@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");
}
}
Filter chains offer several key benefits:
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);
}
}
}
}
Beyond security, filters can solve many common enterprise needs. Here are some practical examples:
@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
));
}
}
}
@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")));
}
}
}
@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");
}
}
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:
2. Create your filter and mark it with the binding annotation:
@Provider
@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")
3. Apply the annotation to specific resource methods or classes:
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:
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.
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:
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!
Share:
Welcome aboard the September issue of The Monthly Catch! With summer holidays wrapping up, the Java world is back […]
We’re excited to announce that Payara Platform Community 7 Beta application server is now fully certified as Jakarta EE 11 […]
If your Java EE 8 applications run on Red Hat JBoss Enterprise Application Platform (EAP) 7, you can’t afford […]