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 4326211d08b..cb2e7611053 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 @@ -2016,7 +2016,7 @@ public ClientModule deploy(final ClientModule clientModule) throws OpenEJBExcept buildAnnotatedRefs(client, annotationFinder, classLoader); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load main class {1} for client module {2} / {3}", + logger.debug("Could not load main class {0} for client module {1} / {2}", className, clientModule.getJarLocation(), clientModule.getFile().getName()); /* @@ -2041,7 +2041,7 @@ public ClientModule deploy(final ClientModule clientModule) throws OpenEJBExcept clazz = classLoader.loadClass(className); remoteClients.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load RemoteClient class {1} for client module {2} / {3}", + logger.debug("Could not load RemoteClient class {0} for client module {1} / {2}", className, clientModule.getJarLocation(), clientModule.getFile().getName()); throw new OpenEJBException("Unable to load RemoteClient class: " + className, e); @@ -2058,7 +2058,7 @@ public ClientModule deploy(final ClientModule clientModule) throws OpenEJBExcept try { clazz = classLoader.loadClass(className); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load LocalClient class {1} for client module {2} / {3}", + logger.debug("Could not load LocalClient class {0} for client module {1} / {2}", className, clientModule.getJarLocation(), clientModule.getFile().getName()); throw new OpenEJBException("Unable to load LocalClient class: " + className, e); } @@ -2189,7 +2189,7 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { clazz = classLoader.loadClass(application); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load rest Application class {1} for module {2} / {3}", + logger.debug("Could not load rest Application class {0} for module {1} / {2}", application, webModule.getJarLocation(), webModule.getFile().getName()); throw new OpenEJBException("Unable to load Application class: " + application, e); } @@ -2263,10 +2263,18 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { 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. + // Note this also skips @Resource/@EJB processing for that servlet, so if the + // class does resolve later it comes up without its injections. + logger.warning("Unable to load servlet class: " + servletClass + " for web module " + + webModule.getJarLocation(), e); } else { logger.error("servlet " + servletName + " has no servlet-class defined and is not a subclass of Application"); } @@ -2294,9 +2302,16 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { 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); + // Jakarta Servlet 6.1 section 6.2.1 requires the filter to be instantiated before a + // request reaches a resource it is mapped to - i.e. later than this, but unlike a + // servlet it cannot be skipped if it is ever used. So a missing class is very likely a + // real packaging error; log it at error level, but keep it local to the filter rather + // than failing the whole application (the spec's "must fail to deploy" rules cover + // web-fragment ordering conflicts, not unresolvable component classes). + logger.error("Unable to load servlet filter class: " + filterClass + " for web module " + + webModule.getJarLocation(), e); } } } @@ -2311,9 +2326,13 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { 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 declared listener is instantiated at application startup, so - as for filters + // above - a missing class is very likely a real packaging error rather than something + // that can be deferred. Report it at error level but keep the failure local. + logger.error("Unable to load servlet listener class: " + listenerClass + " for web module " + + webModule.getJarLocation(), e); } } } @@ -2329,7 +2348,7 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { 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); } @@ -2346,7 +2365,7 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { final Class clazz = classLoader.loadClass(tagClass); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load tag class {1} for web module {2} / {3}", + logger.debug("Could not load tag class {0} for web module {1} / {2}", tagClass, webModule.getJarLocation(), webModule.getFile().getName()); logger.error("Unable to load tag library tag class: " + tagClass); } @@ -2376,8 +2395,12 @@ public WebModule deploy(final WebModule webModule) throws OpenEJBException { final Class clazz = classLoader.loadClass(handlerClass); classes.add(clazz); } catch (final ClassNotFoundException | NoClassDefFoundError e) { - logger.debug("Could not load web service handler class {1} for web module {2} / {3}", + logger.debug("Could not load web service handler class {0} for web module {1} / {2}", handlerClass, webModule.getJarLocation(), webModule.getFile().getName()); + // Deliberately still fatal, unlike the servlet/filter/listener sites above: + // a handler chain silently missing a handler would leave the endpoint + // running with a different (weaker) contract than declared, e.g. dropping + // a security or logging handler, rather than just disabling one component. throw new OpenEJBException("Unable to load webservice handler class: " + handlerClass, e); } } @@ -2926,7 +2949,7 @@ public EjbModule deploy(final EjbModule ejbModule) throws OpenEJBException { try { clazz = classLoader.loadClass(realClassName(interceptor.getInterceptorClass())); } catch (final ClassNotFoundException e) { - logger.debug("Could not load interceptor class {1} for enterprise beans module {2} / {3}", + logger.debug("Could not load interceptor class {0} for enterprise beans module {1} / {2}", interceptor.getInterceptorClass(), ejbModule.getJarLocation(), ejbModule.getFile().getName()); throw new OpenEJBException("Unable to load interceptor class: " + interceptor.getInterceptorClass(), e); @@ -5693,7 +5716,7 @@ private static void addRestClassesToScannedClasses(final WebModule webModule, fi clazz = classLoader.loadClass(className); classes.add(clazz); } catch (final ClassNotFoundException e) { - logger.debug("Could not load REST class {1} for web module {2} / {3}", + logger.debug("Could not load REST class {0} for web module {1} / {2}", className, webModule.getJarLocation(), webModule.getFile().getName()); throw new OpenEJBException("Unable to load REST class: " + className, e); diff --git a/container/openejb-core/src/main/java/org/apache/openejb/config/WsDeployer.java b/container/openejb-core/src/main/java/org/apache/openejb/config/WsDeployer.java index 476c35a587c..a445c432762 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/config/WsDeployer.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/config/WsDeployer.java @@ -146,8 +146,19 @@ private void processPorts(final WebModule webModule) throws OpenEJBException { continue; } + // TOMEE-4642: this runs for every servlet, not just webservice endpoints, so a servlet + // class the war does not package must not fail the whole deployment here either. + // NoClassDefFoundError is an Error, so the catch below would not cover it. + final Class clazz; + try { + clazz = webModule.getClassLoader().loadClass(className); + } catch (final ClassNotFoundException | NoClassDefFoundError e) { + logger.warning("Unable to load servlet class: " + className + " for web module " + + webModule.getJarLocation() + ", skipping webservice detection for it", e); + continue; + } + try { - final Class clazz = webModule.getClassLoader().loadClass(className); if (JaxWsUtils.isWebService(clazz)) { // add servlet mapping if not already declared ServletMapping servletMapping = servletMappings.get(servlet.getServletName()); @@ -231,7 +242,8 @@ private void processPorts(final WebModule webModule) throws OpenEJBException { } } } catch (final Exception e) { - throw new OpenEJBException("Unable to load servlet class: " + className, e); + // the class resolved fine, so this is a genuine webservice configuration problem + throw new OpenEJBException("Unable to configure webservice for servlet class: " + className, e); } } } diff --git a/container/openejb-core/src/main/java/org/apache/openejb/web/LightweightWebAppBuilder.java b/container/openejb-core/src/main/java/org/apache/openejb/web/LightweightWebAppBuilder.java index 82afeef1719..37e0c446c6b 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/web/LightweightWebAppBuilder.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/web/LightweightWebAppBuilder.java @@ -182,7 +182,16 @@ public void deployWebApps(final AppInfo appInfo, final ClassLoader appClassLoade // listeners for (final ListenerInfo listener : webAppInfo.listeners) { - final Class clazz = webContext.getClassLoader().loadClass(listener.classname); + final Class clazz; + try { + clazz = webContext.getClassLoader().loadClass(listener.classname); + } catch (final ClassNotFoundException | NoClassDefFoundError e) { + // TOMEE-4642: a listener class the war does not package is reported by the + // deployer and must not abort the rest of the application here either. + LOGGER.error("Unable to load listener class: " + listener.classname + + " for web application " + webAppInfo.contextRoot, e); + continue; + } final Object instance = webContext.newInstance(clazz); if (ServletContextListener.class.isInstance(instance)) { switchServletContextIfNeeded(sce.getServletContext(), new Runnable() { 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 9e547ac635f..5cd18d21b42 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 @@ -20,11 +20,15 @@ import org.apache.openejb.assembler.classic.AppInfo; import org.apache.openejb.assembler.classic.Assembler; import org.apache.openejb.assembler.classic.ClientInfo; +import org.apache.openejb.assembler.classic.WebAppInfo; import org.apache.openejb.jee.AssemblyDescriptor; 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; @@ -82,6 +86,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -496,6 +501,82 @@ public void setMyNumber(final int myNumber) { } } + /** + * 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()); + } + + /** + * TOMEE-4642: same as above, but driven through the whole deployer chain rather than + * AnnotationDeployer alone. WsDeployer is part of that chain whenever wsdl4j is on the + * classpath (Plus, Plume, openejb-standalone) and loads every servlet class before testing + * whether it is a webservice, so it used to fail the deployment on those distributions even + * with AnnotationDeployer fixed. + */ + @Test + public void missingServletClassDoesNotFailFullConfiguration() 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); + + final WebModule webModule = new WebModule(webApp, webApp.getContextRoot(), + Thread.currentThread().getContextClassLoader(), "myapp", webApp.getId()); + webModule.setFinder(new AnnotationFinder(new ClassesArchive()).link()); + + final WebAppInfo webAppInfo = new ConfigurationFactory().configureApplication(webModule); + + assertNotNull(webAppInfo); + assertEquals(1, webAppInfo.servlets.size()); + assertEquals("TestServlet1", webAppInfo.servlets.iterator().next().servletName); + } + @Test public void findRestClasses() throws Exception { final WebApp webApp = new WebApp();