This is an automated email from the ASF dual-hosted git repository. jungm pushed a commit to branch claude/tomee-4642-fix-23d281 in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 3faf4fcb4ce7727cd2ed0027eec8385fe73e8faa Author: Markus Jung <[email protected]> AuthorDate: Thu Jul 23 21:01:49 2026 +0200 TOMEE-4642 - Don't fail deployment when a servlet/filter/listener class is missing When a war's web.xml or annotations name a servlet, filter or listener class that is not packaged in the war, ProcessAnnotatedBeans.deploy rethrew the ClassNotFoundException/NoClassDefFoundError as an OpenEJBException, which aborted startup of the whole web context. These loads only feed the annotation scanner, so a missing class simply means there is nothing to scan - it must not bring down the context. Jakarta Servlet 6.1 section 2.3.1 allows servlet loading to be delayed "until the container determines the servlet is needed to service a request", so an unresolved class is deferred, not fatal. The three paths now log a warning and continue, matching the existing tolerant handling of taglib listeners and the servlet name-fallback case. Also fixed off-by-one MessageFormat placeholder indices ({1}{2}{3} -> {0}{1}{2}) in the four related logger.debug calls, which dropped the first argument. Two Jakarta Servlet TCK deployments triggered this (RegistrationTests naming filter AddFilterString, DefaultMappingTests naming servlet TestServlet1). --- .../apache/openejb/config/AnnotationDeployer.java | 23 +++++++++---- .../openejb/config/AnnotationDeployerTest.java | 40 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java b/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java index 4326211d08..50a4f18ac5 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java @@ -2263,10 +2263,15 @@ public class AnnotationDeployer implements DynamicDeployer { servlet.setServletClass(servletClass); } } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load Servlet class {1} for web module {2} / {3}", + logger.debug("Could not load Servlet class {0} for web module {1} / {2}", servletClass, webModule.getJarLocation(), webModule.getFile().getName()); if (servlet.getServletClass() != null) { - throw new OpenEJBException("Unable to load servlet class: " + servletClass, e); + // The class is not available for annotation scanning, but that must not fail the + // whole context. Jakarta Servlet 6.1, section 2.3.1 "Loading and Instantiation" + // allows loading to be "delayed until the container determines the servlet is + // needed to service a request", so a servlet whose class is absent may simply + // never be used (or be registered programmatically). Let resolution happen later. + logger.warning("Unable to load servlet class: " + servletClass); } else { logger.error("servlet " + servletName + " has no servlet-class defined and is not a subclass of Application"); } @@ -2294,9 +2299,11 @@ public class AnnotationDeployer implements DynamicDeployer { final Class clazz = classLoader.loadClass(filterClass); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load Servlet Filter class {1} for web module {2} / {3}", + logger.debug("Could not load Servlet Filter class {0} for web module {1} / {2}", filterClass, webModule.getJarLocation(), webModule.getFile().getName()); - throw new OpenEJBException("Unable to load servlet filter class: " + filterClass, e); + // A missing filter class must not fail the whole context; as with servlets + // (Jakarta Servlet 6.1, section 2.3.1) an unresolved class is deferred, not fatal here. + logger.warning("Unable to load servlet filter class: " + filterClass); } } } @@ -2311,9 +2318,11 @@ public class AnnotationDeployer implements DynamicDeployer { final Class clazz = classLoader.loadClass(listenerClass); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load Servlet listener class {1} for web module {2} / {3}", + logger.debug("Could not load Servlet listener class {0} for web module {1} / {2}", listenerClass, webModule.getJarLocation(), webModule.getFile().getName()); - throw new OpenEJBException("Unable to load servlet listener class: " + listenerClass, e); + // A missing listener class must not fail the whole context; as with servlets + // (Jakarta Servlet 6.1, section 2.3.1) an unresolved class is deferred, not fatal here. + logger.warning("Unable to load servlet listener class: " + listenerClass); } } } @@ -2329,7 +2338,7 @@ public class AnnotationDeployer implements DynamicDeployer { final Class clazz = classLoader.loadClass(listenerClass); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load TagLib listener class {1} for web module {2} / {3}", + logger.debug("Could not load TagLib listener class {0} for web module {1} / {2}", listenerClass, webModule.getJarLocation(), webModule.getFile().getName()); logger.error("Unable to load tag library servlet listener class: " + listenerClass); } diff --git a/container/openejb-core/src/test/java/org/apache/openejb/config/AnnotationDeployerTest.java b/container/openejb-core/src/test/java/org/apache/openejb/config/AnnotationDeployerTest.java index 9e547ac635..4f1dda0ea7 100644 --- a/container/openejb-core/src/test/java/org/apache/openejb/config/AnnotationDeployerTest.java +++ b/container/openejb-core/src/test/java/org/apache/openejb/config/AnnotationDeployerTest.java @@ -25,6 +25,9 @@ import org.apache.openejb.jee.ConfigProperty; import org.apache.openejb.jee.Connector; import org.apache.openejb.jee.EjbJar; import org.apache.openejb.jee.EnterpriseBean; +import org.apache.openejb.jee.Filter; +import org.apache.openejb.jee.Listener; +import org.apache.openejb.jee.Servlet; import org.apache.openejb.jee.SessionBean; import org.apache.openejb.jee.TransactionSupportType; import org.apache.openejb.jee.WebApp; @@ -496,6 +499,43 @@ public class AnnotationDeployerTest { } } + /** + * TOMEE-4642: a servlet, filter or listener class named in web.xml but not packaged in the war + * must not abort the deployment of the whole web module. + */ + @Test + public void missingServletFilterAndListenerClassesDoNotFailDeployment() throws Exception { + final WebApp webApp = new WebApp(); + webApp.setContextRoot("/"); + webApp.setId("web"); + webApp.setVersion("2.5"); + + final Servlet servlet = new Servlet(); + servlet.setServletName("TestServlet1"); + servlet.setServletClass("org.apache.openejb.config.missing.TestServlet1"); + webApp.getServlet().add(servlet); + + final Filter filter = new Filter(); + filter.setFilterName("AddFilterString"); + filter.setFilterClass("org.apache.openejb.config.missing.AddFilterString"); + webApp.getFilter().add(filter); + + final Listener listener = new Listener(); + listener.setListenerClass("org.apache.openejb.config.missing.MissingListener"); + webApp.getListener().add(listener); + + WebModule webModule = new WebModule(webApp, webApp.getContextRoot(), Thread.currentThread().getContextClassLoader(), "myapp", webApp.getId()); + webModule.setFinder(new AnnotationFinder(new ClassesArchive()).link()); + + final AnnotationDeployer annotationDeployer = new AnnotationDeployer(); + webModule = annotationDeployer.deploy(webModule); + + // the declarations are kept as-is, they are simply not resolved for annotation scanning + assertEquals(1, webModule.getWebApp().getServlet().size()); + assertEquals(1, webModule.getWebApp().getFilter().size()); + assertEquals(1, webModule.getWebApp().getListener().size()); + } + @Test public void findRestClasses() throws Exception { final WebApp webApp = new WebApp();
