Jetty 12 ClassNotFoundException for ResourceServlet Fix
Resolve Jetty 12 ClassNotFoundException for org.eclipse.jetty.ee10.servlet.ResourceServlet on /webapp/* paths. Fix WebAppClassLoader isolation with parentLoaderPriority or bundle ee10-servlet JARs in web.xml for static resources in Java 21 WAR deployments.
Jetty 12 ClassNotFoundException for org.eclipse.jetty.ee10.servlet.ResourceServlet when configuring for /webapp/* path
I’m running a Java 21 web application with static resources under the /webapp path. In Jetty 11, DefaultServlet worked fine, but Jetty now recommends ResourceServlet for non-root paths.
However, Jetty 12 fails to load ResourceServlet with a ClassNotFoundException. Here’s my configuration in /WEB-INF/web.xml:
<servlet>
<servlet-name>webapp-servlet</servlet-name>
<servlet-class>org.eclipse.jetty.ee10.servlet.ResourceServlet</servlet-class>
<init-param>
<param-name>dirAllowed</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>baseResource</param-name>
<param-value>webapp/</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>webapp-servlet</servlet-name>
<url-pattern>/webapp/*</url-pattern>
</servlet-mapping>
After compiling to WAR and deploying to Jetty 12, startup fails with:
java.lang.ClassNotFoundException: org.eclipse.jetty.ee10.servlet.ResourceServlet
WARN :oejew.WebAppContext:main: Failed startup of context oeje10w.WebAppContext@{...}
jakarta.servlet.UnavailableException: Class loading error for holder webapp-servlet==org.eclipse.jetty.ee10.servlet.ResourceServlet@{... /webapp/WEB-INF/web.xml,STARTING}
I’ve confirmed org.eclipse.jetty.ee10.servlet.ResourceServlet is in the ee10-servlet module and included these Jetty 12 standard modules:
- client
- ee10-deploy
- ee10-servlet
- ext
- http
- resources
- server
DefaultServlet works but triggers a warning about “invalid servlet mapping /webapp/*”.
What am I missing? How to correctly configure ResourceServlet in Jetty 12 for serving static resources from /webapp/*?
If you’re hitting a Jetty 12 ClassNotFoundException for org.eclipse.jetty.ee10.servlet.ResourceServlet on /webapp/* paths, it’s almost always the WebAppContext’s classloader hiding Jetty’s own classes from your WAR. Your web.xml config looks spot-on—baseResource=webapp/ and dirAllowed=false match the official docs—but Jetty 12’s stricter isolation needs a tweak like parentLoaderPriority=true or bundling the ee10-servlet JAR directly in WEB-INF/lib. This fixes the jakarta.servlet.UnavailableException at startup without warnings, letting you serve static resources cleanly from your Java 21 WAR.
Contents
- Understanding Jetty 12 ClassNotFoundException for ResourceServlet
- Why ResourceServlet Replaces DefaultServlet
- Root Cause: WebAppClassLoader Isolation
- Fix 1: Enable parentLoaderPriority
- Fix 2: Bundle ee10-servlet Module in WEB-INF/lib
- Complete web.xml Configuration for Jetty 12 Static Resources
- Testing and Troubleshooting
- Sources
- Conclusion
Understanding Jetty 12 ClassNotFoundException for ResourceServlet
Ever deployed a WAR to Jetty 12 and watched it choke on java.lang.ClassNotFoundException: org.eclipse.jetty.ee10.servlet.ResourceServlet right at startup? You’re not alone. This hits folks migrating from Jetty 11, especially when swapping DefaultServlet for ResourceServlet on paths like /webapp/*. The error bubbles up as a jakarta.servlet.UnavailableException in the logs:
WARN :oejew.WebAppContext:main: Failed startup of context oeje10w.WebAppContext@{...}
jakarta.servlet.UnavailableException: Class loading error for holder webapp-servlet==org.eclipse.jetty.ee10.servlet.ResourceServlet@{...}
Your setup—Java 21, modules like ee10-servlet, http, resources—checks out on paper. Jetty 12 ships that class in the ee10-servlet module, as confirmed in the API docs. But during WAR deployment, the WebAppContext treats Jetty internals as off-limits. It’s a deliberate security/feature choice, not a bug. Short fix? Tell the classloader to look parent-first. We’ll get to that.
What makes this sneaky? Jetty 11 was looser; Jetty 12 (Jakarta EE 10 era) tightens the screws for better isolation in shared server setups. Your DefaultServlet fallback works but nags with “invalid servlet mapping /webapp/*—use ResourceServlet” warnings, per this GitHub thread.
Why ResourceServlet Replaces DefaultServlet
Why ditch DefaultServlet for non-root paths in Jetty 12? Simple: it’s optimized for root (/*), but subpaths like /webapp/* trigger those pesky warnings. Enter ResourceServlet—tailor-made for static resources under custom prefixes.
Compare them side-by-side:
| Feature | DefaultServlet | ResourceServlet |
|---|---|---|
| Best for | Root path /* |
Subpaths like /webapp/* |
| baseResource param | Assumes root | Flexible: webapp/, ./static/, etc. |
| Warnings on /webapp/* | Yes, “invalid mapping” | None |
| Jetty 12 Docs Rec | Legacy for root only | Preferred for static subdirs |
From the ResourceServlet Javadoc, it’s a lightweight swap: same init-params (baseResource, dirAllowed, pathInfoOnly), but no root assumptions. Your config nails it—baseResource=webapp/ serves files from ./webapp/ relative to the context. DefaultServlet would scan the whole app root, which is overkill (and risky) for isolated static dirs.
Jetty devs pushed this in issues like #12476, urging migration. Stick with it; the ClassNotFoundException is just a classloader hiccup away from resolution.
Root Cause: WebAppClassLoader Isolation
Here’s the culprit: Jetty’s WebAppClassLoader. By default, it uses PARENT_LAST delegation—your WAR’s classes first, server classes last. Worse, WebAppContext hides Jetty packages entirely via DEFAULT_HIDDEN_CLASSES, including org.eclipse.jetty.ee10.servlet.*.
Peek at the WebAppContext docs: server classes like ResourceServlet live outside your WAR, in Jetty modules. But ee10-servlet isn’t bundled in WEB-INF/lib, so the isolated loader can’t see it. Boom—ClassNotFoundException.
Even with modules enabled (ee10-servlet, etc.), WAR deployment sandboxes them. Seen this before? GitHub issue #12100 reports identical ee10 class failures despite modules. It’s not missing deps; it’s deliberate isolation for multi-app servers. Question is, do you fight it or bundle?
Pros of isolation: Safer, no version clashes. Cons: Extra config for Jetty servlets in WARs.
Fix 1: Enable parentLoaderPriority
Quickest win—no JAR shuffling. Flip the classloader to PARENT_FIRST via context-param in web.xml. Jetty peeks server classes (including ResourceServlet) before your app.
Add this before your <servlet>:
<context-param>
<param-name>org.eclipse.jetty.server.webapp.parentLoaderPriority</param-name>
<param-value>true</param-value>
</context-param>
Full effect? Startup scans Jetty modules first. From WebAppContext docs, this overrides hiding for your context. Works on Java 21, no restarts needed.
Alternative: JVM arg -Dorg.eclipse.jetty.server.webapp.parentLoaderPriority=true for global, but per-app is cleaner.
Test it: Redeploy WAR. No more UnavailableException. Drawback? Slightly less isolation, but fine for trusted WARs.
Fix 2: Bundle ee10-servlet Module in WEB-INF/lib
Prefer full isolation? Grab jetty-ee10-servlet-12.x.jar (and deps: jetty-http, jetty-resources) from Maven:
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<version>12.0.10</version> <!-- Match your Jetty -->
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>12.0.10</version>
</dependency>
mvn package bundles them in WEB-INF/lib. Now ResourceServlet loads from your WAR—classloader happy.
| Fix | Pros | Cons |
|---|---|---|
| parentLoaderPriority | Zero build changes, fast | Less isolation |
| Bundle JARs | Total self-contained | Bigger WAR, version sync |
The ee10-servlet package summary lists exact deps. Matches your modules perfectly.
Complete web.xml Configuration for Jetty 12 Static Resources
Put it together. Here’s battle-tested web.xml for /webapp/*:
<?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">
<!-- Fix classloader -->
<context-param>
<param-name>org.eclipse.jetty.server.webapp.parentLoaderPriority</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>webapp-servlet</servlet-name>
<servlet-class>org.eclipse.jetty.ee10.servlet.ResourceServlet</servlet-class>
<init-param>
<param-name>baseResource</param-name>
<param-value>webapp/</param-value>
</init-param>
<init-param>
<param-name>dirAllowed</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>webapp-servlet</servlet-name>
<url-pattern>/webapp/*</url-pattern>
</servlet-mapping>
</web-app>
Tweaks: Added load-on-startup for eager init. Handles 404s gracefully. For programmatic (no web.xml), see issue #11791:
ServletHolder holder = context.addServlet(ResourceServlet.class, "/webapp/*");
holder.setInitParameter("baseResource", "webapp/");
Testing and Troubleshooting
Deploy, hit http://localhost:8080/webapp/yourfile.css. 200 OK? Golden.
Stuck? Check:
- Logs: Grep “UnavailableException” or “ClassNotFound”.
- Modules:
java -jar start.jar --list-modulesconfirmsee10-servlet. - Java 21:
--add-opensif reflection gripes (rare). - 404s: Issue #12455 notes
pathInfoOnly=truefor clean aliases.
Edge case: Multi-context? Per-app parentLoaderPriority. Still warns? Nuke DefaultServlet entirely.
Pro tip: curl -v your path—headers reveal servlet in action.
Sources
- ResourceServlet — Official API for ResourceServlet configuration and init-params: https://javadoc.jetty.org/jetty-12/org/eclipse/jetty/ee10/servlet/ResourceServlet.html
- ee10-servlet Package Summary — Details on servlet modules and dependencies for Jetty 12: https://javadoc.jetty.org/jetty-12/org/eclipse/jetty/ee10/servlet/package-summary.html
- WebAppContext — Classloader configuration including parentLoaderPriority and hidden classes: https://javadoc.jetty.org/jetty-12/org/eclipse/jetty/ee10/webapp/WebAppContext.html
- Jetty Issue #12476 — Discussion of DefaultServlet warnings and ResourceServlet migration: https://github.com/jetty/jetty.project/issues/12476
- Jetty Issue #12100 — Reports of similar ClassNotFoundException for ee10 classes: https://github.com/jetty/jetty.project/issues/12100
Conclusion
Tame that Jetty 12 ClassNotFoundException for ResourceServlet by prioritizing the parent classloader or bundling JARs—your /webapp/* static serving will hum along warning-free. Start with parentLoaderPriority=true; it’s the least invasive for most setups. Once fixed, enjoy cleaner deployments on Java 21, ditching DefaultServlet legacy nag. Dive into the docs for tweaks, and you’re set.