What is a Deployment Descriptor — and Why Does it Still Exist?
A deployment descriptor is an XML file, living at
WEB-INF/web.xml inside a WAR, that tells the servlet
container how to wire up an application — which classes are servlets,
which URLs map to them, what filters run and in what order, how sessions
and security are configured. Before Servlet 3.0 (2009),
this file was mandatory — there was no other way to register a servlet
at all. Annotations removed that requirement for the common case, but
XML configuration didn't disappear: it remains the only mechanism for a
handful of things annotations genuinely can't express, and it's essential
for reading and maintaining the large body of Java EE applications
written before annotations existed.
// BEFORE Servlet 3.0 — mandatory, and this is the ENTIRE registration
<servlet>
<servlet-name>productServlet</servlet-name>
<servlet-class>com.shop.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>productServlet</servlet-name>
<url-pattern>/products</url-pattern>
</servlet-mapping>
// AFTER Servlet 3.0 — the same registration, one line, no XML at all
@WebServlet("/products")
public class ProductServlet extends HttpServlet { /* ... */ }
- Ops needs to change configuration (a URL pattern, a filter's enabled state) without a rebuild and redeploy of compiled code
- Overriding a third-party library's own servlet/filter registration, which you can't retroactively annotate
- Precise, explicit filter chain ordering across many filters from different sources
- Maintaining an existing legacy application that predates annotation-based configuration
Structure and Namespace
mywebapp.war
└── WEB-INF/
└── web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<display-name>Shop Catalog Service</display-name>
<!-- Configuration goes here -->
</web-app>
Tomcat 9 and earlier expect the older xmlns.jcp.org
namespace and javax classes; Tomcat 10+ expects the
jakarta.ee namespace shown above. Mismatching these is a
common, confusing failure mode — the descriptor may parse without
error while the classes it references don't exist under the
container's actual namespace. Full detail on this migration is on
Servlet Containers.
Configuring Servlets and Filters
Servlet registration with init parameters
<servlet>
<servlet-name>reportServlet</servlet-name>
<servlet-class>com.shop.ReportServlet</servlet-class>
<init-param>
<param-name>maxResults</param-name>
<param-value>100</param-value>
</init-param>
<load-on-startup>1</load-on-startup> <!-- lower number = higher startup priority -->
</servlet>
<servlet-mapping>
<servlet-name>reportServlet</servlet-name>
<url-pattern>/reports/*</url-pattern>
</servlet-mapping>
| Pattern type | Example | Matches | Priority |
|---|---|---|---|
| Exact match | /catalog/products | Only that exact path | Highest |
| Path match | /catalog/* | /catalog, /catalog/x, /catalog/x/y | 2nd |
| Extension match | *.do | Anything ending in .do | 3rd |
| Default | / | Anything not matched above | Lowest |
Filter chain — execution order is declaration order, and that's the whole rule
<!-- Filters execute in EXACTLY the order they appear in this file -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>com.shop.EncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>authFilter</filter-name>
<filter-class>com.shop.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>authFilter</filter-name>
<url-pattern>/secure/*</url-pattern>
</filter-mapping>
Without a web.xml to read top-to-bottom, ordering isn't
implicit — @WebFilter-annotated filters and Spring's own
FilterRegistrationBean both require an explicit
@Order value or setOrder() call, precisely
because declaration order in annotated/component-scanned code carries
no guaranteed sequence the way physical position in an XML file
does.
File uploads — multipart configuration
<multipart-config>
<max-file-size>5242880</max-file-size> <!-- 5 MB per file -->
<max-request-size>20971520</max-request-size> <!-- 20 MB total request -->
<file-size-threshold>0</file-size-threshold> <!-- 0 = write to disk immediately, don't buffer in memory -->
</multipart-config>
// Relevant for something like uploading a product image in an admin panel —
// without an explicit limit, a single oversized upload can exhaust memory
// or disk before your own validation code ever runs.
Session Configuration
<session-config>
<session-timeout>30</session-timeout> <!-- minutes; 0 = never expires — almost never what you want -->
<cookie-config>
<name>SHOPSESSIONID</name>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode> <!-- never URL rewriting — it exposes the session ID in logs, referrers, history -->
</session-config>
Every principle behind http-only, secure,
and avoiding URL-based session tracking is explained mechanically —
not just declared — on
Session Management and
JWT. A Spring Boot application
configures the equivalent settings via
server.servlet.session.* properties in
application.yml rather than this XML block, but the
underlying security reasoning is identical.
Container-Managed Security — Legacy, Superseded by Spring Security
The Servlet specification defines its own declarative authentication and
authorization mechanism, configured entirely in web.xml.
It's presented here for completeness and for maintaining applications
that still use it — modern Spring applications use Spring Security's
SecurityFilterChain
(see Authentication vs
Authorization) instead, which is more flexible and is where new
development should live.
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Area</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee> <!-- requires HTTPS -->
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login-error.html</form-error-page>
</form-login-config>
</login-config>
<security-role><role-name>admin</role-name></security-role>
<!-- login.html — these exact names are REQUIRED by the servlet spec,
not a convention you can rename -->
<form action="j_security_check" method="POST">
<input type="text" name="j_username" required>
<input type="password" name="j_password" required>
</form>
Container-managed security offers only what the Servlet spec defines —
role-based URL protection, four fixed auth methods
(BASIC, DIGEST, FORM,
CLIENT-CERT). It has no native concept of JWT, OAuth2,
method-level @PreAuthorize expressions, or the
fine-grained ABAC policies covered in the Security section of this
Bible. Spring Security implements all of that on top of the same
underlying servlet filter chain, which is why it's the correct
choice for anything beyond maintaining an existing legacy
deployment.
Error Pages
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/errors/not-found.html</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/WEB-INF/errors/generic-error.jsp</location> <!-- catch-all -->
</error-page>
Show a generic, friendly message to the caller and log the full
detail server-side. The modern Spring equivalent of this entire
section is a @ControllerAdvice with
@ExceptionHandler methods returning
ProblemDetail (RFC 9457) — declarative per-exception-type
mapping, the same underlying goal as <error-page>,
expressed in code rather than XML, and integrated with the
ProblemDetail convention used consistently across the
Jakarta EE and Spring topics in this Bible.
Context Parameters and Listeners
<context-param>
<param-name>maxUploadSize</param-name>
<param-value>10485760</param-value>
</context-param>
// Accessing it:
ServletContext context = getServletContext();
String maxUpload = context.getInitParameter("maxUploadSize");
<listener>
<listener-class>com.shop.InventorySyncListener</listener-class>
</listener>
| Listener interface | Events | Common use |
|---|---|---|
ServletContextListener | Application start/stop | Initialize resources at startup, clean up at shutdown |
HttpSessionListener | Session create/destroy | Track active user count |
ServletRequestListener | Request start/end | Request timing, logging |
web.xml → Modern Equivalent, Element by Element
Every capability above still exists in modern Spring applications — it just moved from XML into annotations and configuration properties. This table is the direct translation, useful both for reading legacy code with a modern mental model and for knowing exactly what to search for when a piece of inherited XML needs to be ported forward.
web.xml element | Modern equivalent |
|---|---|
<servlet> / <servlet-mapping> | @WebServlet, or Spring's @RestController + @RequestMapping |
<filter> / <filter-mapping> | @WebFilter, or a Spring Filter bean with @Order |
<listener> | @WebListener, or Spring's @EventListener / ApplicationListener |
<context-param> | application.yml properties, injected via @Value or @ConfigurationProperties |
<session-config> | server.servlet.session.* properties — see Session Management |
<security-constraint> / <login-config> | Spring Security's SecurityFilterChain — see Authentication vs Authorization |
<error-page> | @ControllerAdvice + @ExceptionHandler returning ProblemDetail |
<welcome-file-list> | Rarely needed — a REST API has no directory-index concept; for served static content, a default resource handler mapping |
Best Practices and Common Pitfalls
✅ Do
- Use annotations for new servlets/filters/listeners; reach for
web.xmlonly for genuine XML-only needs (runtime reconfiguration, overriding a third-party registration, precise cross-cutting filter ordering) - Match the
web-appnamespace and schema version to your container's actual Servlet spec version - Set an explicit
<multipart-config>size limit anywhere file uploads are accepted - Treat
<security-constraint>/<login-config>as legacy-maintenance knowledge, not a pattern for new development - Log full exception detail server-side while showing only a generic message to the client, regardless of whether that's done via
<error-page>or@ExceptionHandler
❌ Don't
- Don't build new authentication/authorization on container-managed security — Spring Security covers everything it does and far more
- Don't assume filter execution order is implicit in an annotation-based or Spring app — declare it explicitly with
@Order - Don't use URL-rewriting session tracking — it exposes the session identifier in server logs, browser history, and the Referer header
- Don't leave
<session-timeout>at0(never expires) outside a very deliberate, reviewed exception - Don't let a stack trace reach the client in any environment reachable by real users
Interview Questions
Q: What is a deployment descriptor, and is web.xml still required?
It's an XML file at WEB-INF/web.xml that configures how the
servlet container should wire up an application. It was mandatory before
Servlet 3.0 (2009); annotations now cover the common case, but
web.xml remains valid and is still the only option for a
handful of things annotations can't express, plus maintaining
applications written before annotations existed.
Q: In what order do filters execute if declared in web.xml?
In exactly the order they're declared in the file, top to bottom — this
is the entire rule, with no separate priority mechanism. An
annotation-based equivalent needs an explicit ordering mechanism instead,
since declaration order in scanned code carries no guaranteed sequence.
Q: What are the four URL pattern types in a servlet mapping, from highest to lowest priority?
Exact match (/catalog/products), path match
(/catalog/*), extension match (*.do), and the
default mapping (/), which catches anything not matched by
the others.
Q: A team maintaining a legacy Java EE application wants to migrate its container-managed <security-constraint>/<login-config> setup to Spring Security. What capability gap does this migration typically close, not just modernize?
Container-managed security is limited to the four auth methods the
Servlet spec itself defines and to coarse, URL-pattern-based role checks
— it has no native concept of method-level authorization expressions,
token-based auth (JWT/OAuth2), or attribute-based policies that consider
more than a role name. The migration isn't purely cosmetic: it typically
unlocks @PreAuthorize SpEL expressions evaluated against the
actual method arguments, integration with OAuth2/OIDC providers, and the
ability to express ownership-based rules (a user can only access their
own orders) that a purely URL-pattern-based constraint structurally
cannot represent regardless of how it's configured.
Q: Why does a ServletContextListener's contextDestroyed() matter for correctness, not just cleanliness, in a hot-redeploy scenario?
If a listener starts a background thread, a scheduled task, or holds a
reference to a static resource in contextInitialized()
without releasing it in contextDestroyed(), a hot redeploy —
common in development and in some production blue/green setups — leaves
that thread or resource running against a web application classloader
that the container believes has been unloaded. The old classloader can't
actually be garbage collected while something still references it,
producing a classloader leak that compounds with every redeploy until the
JVM exhausts metaspace. This is the same failure class covered for
static Set<Session> fields on WebSocket endpoints in
WebSockets — always release
what a listener acquires, symmetrically, on shutdown.
Q: An inherited web.xml configures <tracking-mode>URL</tracking-mode> instead of COOKIE. What's the concrete security consequence, and why would this configuration exist at all?
URL-based session tracking appends the session identifier directly to
every generated URL (;jsessionid=...), which means it ends
up in server access logs, in the browser's history, and in the
Referer header sent to any third-party resource linked from
the page — any of which can leak a live, authenticated session identifier
to a party that should never have it. This mode exists historically to
support clients with cookies disabled, which was a genuine constraint in
the early 2000s web and is essentially irrelevant today. Finding this
configuration in an inherited application is a strong signal to verify
whether it's still serving a real purpose or is simply an unreviewed
legacy default — in the overwhelming majority of current cases, it should
be switched to COOKIE.