This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-logging.git


The following commit(s) were added to refs/heads/master by this push:
     new d1a8dce  Add support for Jakarta servlets (#419)
d1a8dce is described below

commit d1a8dce167e164dbba245e735bff7698a92bafbb
Author: Gary Gregory <[email protected]>
AuthorDate: Thu Apr 30 16:33:27 2026 -0400

    Add support for Jakarta servlets (#419)
---
 pom.xml                                            |  10 ++
 .../logging/jakarta/ServletContextCleaner.java     | 147 +++++++++++++++++++++
 .../servlet/BasicJakartaServletTestCase.java       |  70 ++++++++++
 3 files changed, 227 insertions(+)

diff --git a/pom.xml b/pom.xml
index c2973a0..be2daae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,8 +60,10 @@ under the License.
     <logback.version>1.3.16</logback.version>
     <slf4j.version>2.0.17</slf4j.version>
     <findsecbugs.version>1.14.0</findsecbugs.version>
+    <jakarta.servlet-api.version>5.0.0</jakarta.servlet-api.version>
     <commons.osgi.import>
       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.
                 
<log4j-api>${org.apache.logging.log4j:log4j-api:jar}</log4j-api>
                 <logkit>${logkit:logkit:jar}</logkit>
                 
<servlet-api>${javax.servlet:javax.servlet-api:jar}</servlet-api>
+                
<jakarta-servlet-api>${jakarta.servlet:jakarta.servlet-api:jar}</jakarta-servlet-api>
                 
<commons-logging>target/${project.build.finalName}.jar</commons-logging>
                 
<commons-logging-api>target/${project.build.finalName}-api.jar</commons-logging-api>
                 
<commons-logging-adapters>target/${project.build.finalName}-adapters.jar</commons-logging-adapters>
@@ -578,6 +581,13 @@ under the License.
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>jakarta.servlet</groupId>
+      <artifactId>jakarta.servlet-api</artifactId>
+      <version>${jakarta.servlet-api.version}</version>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
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 0000000..14d02d9
--- /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.
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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<LogFactory> logFactoryClass = (Class<LogFactory>) 
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 0000000..e294569
--- /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);
+    }
+}

Reply via email to