From deb3bee6fb4e7a9a7d6047ed8c45c878a5a64552 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 30 Apr 2026 19:30:38 +0000 Subject: [PATCH] Add support for Jakarta servlets --- pom.xml | 10 ++ .../jakarta/ServletContextCleaner.java | 147 ++++++++++++++++++ .../servlet/BasicJakartaServletTestCase.java | 70 +++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java create mode 100644 src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java diff --git a/pom.xml b/pom.xml index c2973a08c..be2daae57 100644 --- a/pom.xml +++ b/pom.xml @@ -60,8 +60,10 @@ under the License. 1.3.16 2.0.17 1.14.0 + 5.0.0 javax.servlet;version="[2.1.0, 5.0.0)";resolution:=optional, + jakarta.servlet;version="[4.0.2, 7.0.0)";resolution:=optional, org.apache.avalon.framework.logger;version="[4.1.3, 4.1.5]";resolution:=optional, org.apache.log;version="[1.0.1, 1.0.1]";resolution:=optional, org.apache.log4j;version="[1.2.15, 2.0.0)";resolution:=optional, @@ -275,6 +277,7 @@ under the License. ${org.apache.logging.log4j:log4j-api:jar} ${logkit:logkit:jar} ${javax.servlet:javax.servlet-api:jar} + ${jakarta.servlet:jakarta.servlet-api:jar} target/${project.build.finalName}.jar target/${project.build.finalName}-api.jar target/${project.build.finalName}-adapters.jar @@ -578,6 +581,13 @@ under the License. provided true + + jakarta.servlet + jakarta.servlet-api + ${jakarta.servlet-api.version} + provided + true + org.apache.commons commons-lang3 diff --git a/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java b/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java new file mode 100644 index 000000000..14d02d94b --- /dev/null +++ b/src/main/java/org/apache/commons/logging/jakarta/ServletContextCleaner.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.logging.jakarta; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +import org.apache.commons.logging.LogFactory; + +/** + * This class is capable of receiving notifications about the undeployment of + * a webapp, and responds by ensuring that commons-logging releases all + * memory associated with the undeployed webapp. + *

+ * In general, the WeakHashtable support added in commons-logging release 1.1 + * ensures that logging classes do not hold references that prevent an + * undeployed webapp's memory from being garbage-collected even when multiple + * copies of commons-logging are deployed via multiple class loaders (a + * situation that earlier versions had problems with). However there are + * some rare cases where the WeakHashtable approach does not work; in these + * situations specifying this class as a listener for the web application will + * ensure that all references held by commons-logging are fully released. + *

+ *

+ * To use this class, configure the webapp deployment descriptor to call + * this class on webapp undeploy; the contextDestroyed method will tell + * every accessible LogFactory class that the entry in its map for the + * current webapp's context class loader should be cleared. + *

+ * + * @since 1.4.0 + */ +public class ServletContextCleaner implements ServletContextListener { + + private static final Class[] RELEASE_SIGNATURE = { ClassLoader.class }; + + /** + * Constructs a new instance. + */ + public ServletContextCleaner() { + // empty + } + + /** + * Invoked when a webapp is undeployed, this tells the LogFactory + * class to release any logging information related to the current + * contextClassloader. + */ + @Override + public void contextDestroyed(final ServletContextEvent sce) { + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + + final Object[] params = new Object[1]; + params[0] = tccl; + + // Walk up the tree of class loaders, finding all the available + // LogFactory classes and releasing any objects associated with + // the tccl (ie the webapp). + // + // When there is only one LogFactory in the classpath, and it + // is within the webapp being undeployed then there is no problem; + // garbage collection works fine. + // + // When there are multiple LogFactory classes in the classpath but + // parent-first classloading is used everywhere, this loop is really + // short. The first instance of LogFactory found will + // be the highest in the classpath, and then no more will be found. + // This is ok, as with this setup this will be the only LogFactory + // holding any data associated with the tccl being released. + // + // When there are multiple LogFactory classes in the classpath and + // child-first classloading is used in any class loader, then multiple + // LogFactory instances may hold info about this TCCL; whenever the + // webapp makes a call into a class loaded via an ancestor class loader + // and that class calls LogFactory the tccl gets registered in + // the LogFactory instance that is visible from the ancestor + // class loader. However the concrete logging library it points + // to is expected to have been loaded via the TCCL, so the + // underlying logging lib is only initialized/configured once. + // These references from ancestor LogFactory classes down to + // TCCL class loaders are held via weak references and so should + // be released but there are circumstances where they may not. + // Walking up the class loader ancestry ladder releasing + // the current tccl at each level tree, though, will definitely + // clear any problem references. + ClassLoader loader = tccl; + while (loader != null) { + // Load via the current loader. Note that if the class is not accessible + // via this loader, but is accessible via some ancestor then that class + // will be returned. + try { + @SuppressWarnings("unchecked") + final Class logFactoryClass = (Class) loader.loadClass("org.apache.commons.logging.LogFactory"); + final Method releaseMethod = logFactoryClass.getMethod("release", RELEASE_SIGNATURE); + releaseMethod.invoke(null, params); + loader = logFactoryClass.getClassLoader().getParent(); + } catch (final ClassNotFoundException ex) { + // Neither the current class loader nor any of its ancestors could find + // the LogFactory class, so we can stop now. + loader = null; + } catch (final NoSuchMethodException ex) { + // This is not expected; every version of JCL has this method + System.err.println("LogFactory instance found which does not support release method!"); + loader = null; + } catch (final IllegalAccessException ex) { + // This is not expected; every ancestor class should be accessible + System.err.println("LogFactory instance found which is not accessible!"); + loader = null; + } catch (final InvocationTargetException ex) { + // This is not expected + System.err.println("LogFactory instance release method failed!"); + loader = null; + } + } + + // Just to be sure, invoke release on the LogFactory that is visible from + // this ServletContextCleaner class too. This should already have been caught + // by the above loop but just in case... + LogFactory.release(tccl); + } + + /** + * Invoked when a webapp is deployed. Nothing needs to be done here. + */ + @Override + public void contextInitialized(final ServletContextEvent sce) { + // do nothing + } +} diff --git a/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java b/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java new file mode 100644 index 000000000..e29456966 --- /dev/null +++ b/src/test/java/org/apache/commons/logging/servlet/BasicJakartaServletTestCase.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.logging.servlet; + +import junit.framework.Test; +import junit.framework.TestCase; + +import org.apache.commons.logging.PathableClassLoader; +import org.apache.commons.logging.PathableTestSuite; +import org.apache.commons.logging.jakarta.ServletContextCleaner; + +/** + * Tests for ServletContextCleaner utility class. + */ +public class BasicJakartaServletTestCase extends TestCase { + + /** + * Return the tests included in this test suite. + */ + public static Test suite() throws Exception { + // LogFactory in parent + // LogFactory in child (loads test) + // LogFactory in tccl + // + // Having the test loaded via a loader above the tccl emulates the situation + // where a web.xml file specifies ServletContextCleaner as a listener, and + // that class is deployed via a shared class loader. + + final PathableClassLoader parent = new PathableClassLoader(null); + parent.useExplicitLoader("junit.", Test.class.getClassLoader()); + parent.addLogicalLib("commons-logging"); + parent.addLogicalLib("jakarta-servlet-api"); + + final PathableClassLoader child = new PathableClassLoader(parent); + child.setParentFirst(false); + child.addLogicalLib("commons-logging"); + child.addLogicalLib("testclasses"); + + final PathableClassLoader tccl = new PathableClassLoader(child); + tccl.setParentFirst(false); + tccl.addLogicalLib("commons-logging"); + + final Class testClass = child.loadClass(BasicJakartaServletTestCase.class.getName()); + return new PathableTestSuite(testClass, tccl); + } + + /** + * Test that calling ServletContextCleaner.contextDestroyed doesn't crash. + * Testing anything else is rather difficult... + */ + public void testBasics() { + final ServletContextCleaner scc = new ServletContextCleaner(); + scc.contextDestroyed(null); + } +}