TL;DR
- Extend
ValveBaseand overrideinvoke(): always callgetNext().invoke(request, response)to continue the pipeline. - Valves run at the Tomcat container level, making them suitable for cross-application concerns like logging, authentication, and request enrichment.
- Tomcat 10+ uses
jakarta.servlet.*instead ofjavax.servlet.*. Your imports and build dependencies must match your Tomcat version.
What You'll Learn
- What Tomcat Valves are and how they differ from Servlet Filters
- How to write a working custom Valve in Java (with correct imports for Tomcat 9 and Tomcat 10+)
- How the Valve pipeline works and why calling
getNext().invoke()is critical - Where and how to register a Valve (context.xml, server.xml, jboss-web.xml)
- Lifecycle hooks available in ValveBase for initialization and cleanup
The Problem
Every HTTP request that arrives at Tomcat travels through a pipeline before it reaches your application. Tomcat's pipeline is made up of Valves: processing components chained together, each responsible for a specific concern. The final Valve in every pipeline is the Standard Valve (e.g., StandardContextValve), which hands the request to the servlet container.
This design gives you a clean extension point: insert your own Valve into the pipeline and you can inspect, modify, or short-circuit any request before your application code ever runs.
Why not just use a Servlet Filter?
Servlet Filters are part of the Java Servlet specification and live inside a web application. They are portable across any compliant servlet container (Tomcat, Jetty, WildFly, etc.), but that portability is also their constraint: they only apply to the one application they are packaged with, and they can only be configured via the application's web.xml or annotations.
Tomcat Valves operate at a higher level:
| Concern | Servlet Filter | Tomcat Valve |
|---|---|---|
| Scope | Single web application | Engine, Host, or Context container |
| Portability | Any servlet container | Tomcat-specific |
| Configuration | web.xml, annotations |
server.xml, context.xml, deployment descriptors |
| Global coverage | No (per-app only) | Yes (can cover all deployed apps) |
| Access to Tomcat internals | No | Yes (Coyote request, MimeHeaders, etc.) |
Use a Valve when you need server-level control, for example adding a correlation ID to every request across all applications, enforcing IP-based access rules globally, or implementing SSO at the container layer. The built-in AccessLogValve and RemoteAddrValve are good examples of this pattern.
Quick Answer
To create a custom Tomcat Valve:
- Add a Maven dependency on
tomcat-catalina(scopeprovided). - Create a class that extends
org.apache.catalina.valves.ValveBase. - Override the
invoke(Request request, Response response)method with your logic. - Always call
getNext().invoke(request, response)at the end (unless you intentionally want to block the request). - Package the compiled class into a JAR and place it on Tomcat's classpath (typically
$CATALINA_HOME/lib). - Register the Valve in
context.xml,server.xml, or your deployment descriptor.
The ValveBase abstract class handles the boilerplate (pipeline chaining via getNext(), JMX registration, and lifecycle management), so your subclass only needs to implement the core logic.
Implementation
Maven Dependency
For Tomcat 9 (javax namespace):
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.102</version>
<scope>provided</scope>
</dependency>
For Tomcat 10 and 11 (jakarta namespace):
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>10.1.34</version>
<scope>provided</scope>
</dependency>
The dependency is provided because Tomcat supplies these classes at runtime. You must not bundle them in your JAR.
Custom Valve: Tomcat 9 (javax.*)
import java.io.IOException;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
public class CorrelationIdValve extends ValveBase {
@Override
public void invoke(Request request, Response response)
throws IOException, ServletException {
// Add a correlation ID header to the incoming request
String correlationId = java.util.UUID.randomUUID().toString();
// Note: `setValue` adds a new entry if the header already exists —
// check for and remove existing values first if you need idempotent header setting.
request.getCoyoteRequest()
.getMimeHeaders()
.setValue("X-Correlation-Id")
.setString(correlationId);
// IMPORTANT: always pass control to the next valve in the pipeline
getNext().invoke(request, response);
// Post-processing runs here, after the response has been generated
// e.g., log the correlation ID alongside the response status
}
}
Custom Valve: Tomcat 10+ (jakarta.*)
The only difference from the Tomcat 9 version is the import for ServletException. Everything else (the Catalina API, ValveBase, Request, Response) stays the same because those classes live in Tomcat's own packages, not in the Jakarta EE spec.
import java.io.IOException;
import jakarta.servlet.ServletException; // <-- jakarta, not javax
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
public class CorrelationIdValve extends ValveBase {
@Override
public void invoke(Request request, Response response)
throws IOException, ServletException {
String correlationId = java.util.UUID.randomUUID().toString();
// Note: `setValue` adds a new entry if the header already exists —
// check for and remove existing values first if you need idempotent header setting.
request.getCoyoteRequest()
.getMimeHeaders()
.setValue("X-Correlation-Id")
.setString(correlationId);
getNext().invoke(request, response);
}
}
Why getNext().invoke() Is Non-Negotiable
The Valve pipeline is a linked chain. When your invoke method returns without calling getNext().invoke(), you have silently swallowed the request: the servlet never runs, and the caller gets no response (or a hung connection). Only skip the call when you deliberately intend to block the request and have written the response yourself, for example:
@Override
public void invoke(Request request, Response response)
throws IOException, ServletException {
String ip = request.getRemoteAddr();
if (isBlocked(ip)) {
response.sendError(403, "Forbidden");
return; // intentionally not calling getNext()
}
getNext().invoke(request, response);
}
Lifecycle Hooks
ValveBase extends LifecycleMBeanBase, so you can hook into Tomcat's start/stop lifecycle. This is the right place to acquire resources (database connections, thread pools) or perform validation:
import java.io.IOException;
import jakarta.servlet.ServletException;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.valves.ValveBase;
public class MyStatefulValve extends ValveBase {
private SomeResource resource;
@Override
protected void initInternal() throws LifecycleException {
super.initInternal(); // always call super first
resource = SomeResource.create();
}
@Override
protected void startInternal() throws LifecycleException {
super.startInternal();
resource.connect();
}
@Override
protected void stopInternal() throws LifecycleException {
resource.disconnect();
super.stopInternal(); // always call super last on stop
}
@Override
public void invoke(Request request, Response response)
throws IOException, jakarta.servlet.ServletException {
// use resource here
getNext().invoke(request, response);
}
}
Registering the Valve
Option 1: context.xml (per-application, recommended)
Place this file at META-INF/context.xml inside your WAR, or at $CATALINA_BASE/conf/Catalina/localhost/myapp.xml:
<Context>
<Valve className="com.example.CorrelationIdValve" />
</Context>
This scopes the Valve to a single application context.
Option 2: server.xml (global, all applications)
Add a <Valve> element inside the <Host> element to apply it to every application on that virtual host, or inside <Engine> to apply it globally:
<Host name="localhost" appBase="webapps" ...>
<Valve className="com.example.CorrelationIdValve" />
</Host>
Note: Modifying server.xml requires a Tomcat restart.
Option 3: jboss-web.xml (older JBoss AS / JBoss Web)
On older JBoss AS versions (up to and including WildFly 7), JBoss Web (a Tomcat-derived web layer) handled HTTP. In those versions you can register a Valve in the application's WEB-INF/jboss-web.xml deployment descriptor:
<jboss-web>
<context-root>/myapp</context-root>
<valve>
<class-name>com.example.CorrelationIdValve</class-name>
</valve>
</jboss-web>
Important: WildFly 8 (released 2013) replaced JBoss Web with Undertow as its web layer. Undertow is not Tomcat-based and does not support Tomcat Valves. If you are on WildFly 8 or later, jboss-web.xml valve registration does not apply. You would need to use Undertow's own handler/filter extension points instead. The jboss-web.xml approach is therefore relevant only to legacy JBoss AS 7 / WildFly 7 deployments.
This keeps the Valve configuration within the application deployment and avoids touching server-wide configuration files.
Configurable Valve Properties
ValveBase integrates with Tomcat's standard property injection. Any public setter on your Valve class can be set directly in the XML element as an attribute:
<Valve className="com.example.MyValve" headerName="X-Request-Id" enabled="true" />
public class MyValve extends ValveBase {
private String headerName = "X-Request-Id";
private boolean enabled = true;
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
// ...
}
Tomcat reads the attributes from the XML element and calls the corresponding setters before initInternal() is invoked.
Frequently Asked Questions
Q: What is the difference between a Tomcat Valve and a Servlet Filter?
A: A Servlet Filter is defined by the Java Servlet specification and runs inside a web application. It is portable across any compliant servlet container but is limited to the application that declares it. A Tomcat Valve is a proprietary Tomcat concept that lives in Tomcat's request processing pipeline, above the servlet layer. A Valve can be registered at the Engine, Host, or Context level, meaning it can intercept requests for all applications on a server, not just one. Valves also have direct access to Tomcat's internal Request and Response objects (the Coyote layer), which gives them capabilities that filters do not have, such as reading and modifying raw MIME headers before the request is parsed at the application level. The trade-off is portability: Valves tie you to Tomcat.
Q: Which Tomcat versions support custom Valves, and does the package name change?
A: Custom Valves have been supported since Tomcat 4.x. The Catalina API (ValveBase, Request, Response) is stable across versions, but there is a critical breaking change in the Java EE namespace:
- Tomcat 4–9: Uses
javax.servlet.*(Java EE specification). Your imports should beimport javax.servlet.ServletException; - Tomcat 10+: Uses
jakarta.servlet.*(Jakarta EE specification). Your imports must beimport jakarta.servlet.ServletException;
This change was introduced when the Eclipse Foundation took stewardship of Java EE and rebranded it Jakarta EE, which required renaming the javax.* packages. If you compile a Valve against javax.servlet.* and deploy it on Tomcat 10, you will get a ClassNotFoundException or NoClassDefFoundError at runtime. You must recompile against the correct Tomcat version's API jar. The Apache Tomcat project provides a migration tool that can automate package renaming in existing codebases.
Q: How do I register a Valve in context.xml vs server.xml?
A: The choice depends on scope:
-
context.xml(per-application): UseMETA-INF/context.xmlinside your WAR to scope the Valve to that application only. This is the most common choice for application-specific concerns and does not require changes to the Tomcat server configuration. Changes take effect on application redeploy, not full server restart.<Context> <Valve className="com.example.MyValve" /> </Context> -
server.xml(global): Add the<Valve>element inside<Engine>or<Host>to apply it globally to all applications or all applications on a virtual host. Requires a Tomcat restart to pick up changes.<Host name="localhost" appBase="webapps"> <Valve className="com.example.MyValve" /> </Host>
The Tomcat documentation explicitly recommends against placing <Context> elements in server.xml directly, since server.xml cannot be reloaded without restarting Tomcat. For per-application context configuration, prefer a separate file at $CATALINA_BASE/conf/Catalina/localhost/myapp.xml.
Key Takeaways
- ValveBase is your starting point: Extend it, override
invoke(), and Tomcat handles the rest: pipeline chaining, JMX registration, and lifecycle callbacks. - Always call
getNext().invoke(): Skipping this call silently drops the request unless you have explicitly written an error response; this is the most common mistake when writing a first Valve. - jakarta vs javax is a hard break at Tomcat 10: Compile and test against the exact Tomcat version you deploy to. Mixing namespace versions produces runtime failures, not compile-time errors.
What's Next?
Recommended Reading:
- Apache Tomcat Configuration Reference: Valves: full reference for all built-in Valves and configuration attributes
- Apache Tomcat Migration Guide: Tomcat 10: covers the javax → jakarta namespace change in detail
- ValveBase API: Tomcat 11: full JavaDoc for the base class
Action Items:
- Clone or inspect the
AccessLogValvesource in the Tomcat codebase. It is one of the most complete real-world Valve implementations and demonstrates thread safety, lifecycle management, and configurable properties all in one place. - Write a minimal Valve that logs the request URI and response status code, register it in a local Tomcat's
context.xml, and observe how it sits outside your application's log configuration. This makes the container-vs-application boundary concrete.