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

dsoumis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new f35ac34085 Update StoreRegistry to dynamically load clustering classes 
(#1005)
f35ac34085 is described below

commit f35ac3408576f0ba3acdcec996470b27ec831208
Author: Coty Sutherland <[email protected]>
AuthorDate: Tue May 12 12:07:19 2026 -0400

    Update StoreRegistry to dynamically load clustering classes (#1005)
    
    * Fix StoreRegistry to dynamically load clustering classes
    
    Update StoreRegistry to use lazy initialization with dynamic class loading
    for optional clustering interfaces, preventing NoClassDefFoundError when
    StoreConfigLifecycleListener is configured but clustering classes are
    unavailable.
    
    This change aligns StoreConfig behavior with other Tomcat components like
    Catalina.addClusterRuleSet() which already handle optional clustering
    gracefully via reflection.
    
    * Catch NoClassDefFoundError in StoreRegistry.tryAddClass() for partial 
clustering installations where catalina-ha.jar is present but 
catalina-tribes.jar is not.
    
    ---------
    
    Co-authored-by: Dimitris Soumis <[email protected]>
---
 .../catalina/storeconfig/LocalStrings.properties   |   3 +
 .../catalina/storeconfig/StandardEngineSF.java     |  14 ++-
 .../catalina/storeconfig/StandardHostSF.java       |  14 ++-
 .../apache/catalina/storeconfig/StoreRegistry.java |  93 ++++++++++++++----
 .../catalina/storeconfig/TestStoreRegistry.java    | 104 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   7 ++
 6 files changed, 212 insertions(+), 23 deletions(-)

diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties 
b/java/org/apache/catalina/storeconfig/LocalStrings.properties
index 2facbe157c..b257859ea3 100644
--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties
+++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties
@@ -25,8 +25,11 @@ factory.storeTag=store tag [{0}] ( Object: [{1}] )
 globalNamingResourcesSF.noFactory=Cannot find NamingResources store factory
 globalNamingResourcesSF.wrongElement=Wrong element [{0}]
 
+registry.interfacesLoaded=Loaded [{0}] interface classes for registry
 registry.loadClassFailed=Failed to load class [{0}]
 registry.noDescriptor=Can't find descriptor for key [{0}]
+registry.optionalClassLoaded=Loaded optional class [{0}]
+registry.optionalClassNotFound=Optional class [{0}] not found, skipping
 
 standardContextSF.cannotWriteFile=Cannot write file at [{0}]
 standardContextSF.canonicalPathError=Failed to obtain the canonical path of 
the configuration file [{0}]
diff --git a/java/org/apache/catalina/storeconfig/StandardEngineSF.java 
b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
index bd5d074491..7621671e92 100644
--- a/java/org/apache/catalina/storeconfig/StandardEngineSF.java
+++ b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
 import org.apache.catalina.Realm;
 import org.apache.catalina.Valve;
 import org.apache.catalina.core.StandardEngine;
-import org.apache.catalina.ha.ClusterValve;
 
 /**
  * Store server.xml Element Engine
  */
 public class StandardEngineSF extends StoreFactoryBase {
 
+    private static final Class<?> clusterValveClass;
+    static {
+        Class<?> clazz = null;
+        try {
+            clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
+        } catch (ClassNotFoundException e) {
+            // Expected when clustering JARs are not present
+        }
+        clusterValveClass = clazz;
+    }
+
     /**
      * Constructs a new StandardEngineSF instance for storing Engine elements 
in server.xml.
      */
@@ -69,7 +79,7 @@ public class StandardEngineSF extends StoreFactoryBase {
             if (valves != null && valves.length > 0) {
                 List<Valve> engineValves = new ArrayList<>();
                 for (Valve valve : valves) {
-                    if (!(valve instanceof ClusterValve)) {
+                    if (clusterValveClass == null || 
!clusterValveClass.isInstance(valve)) {
                         engineValves.add(valve);
                     }
                 }
diff --git a/java/org/apache/catalina/storeconfig/StandardHostSF.java 
b/java/org/apache/catalina/storeconfig/StandardHostSF.java
index 0933da254a..126a27095d 100644
--- a/java/org/apache/catalina/storeconfig/StandardHostSF.java
+++ b/java/org/apache/catalina/storeconfig/StandardHostSF.java
@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
 import org.apache.catalina.Realm;
 import org.apache.catalina.Valve;
 import org.apache.catalina.core.StandardHost;
-import org.apache.catalina.ha.ClusterValve;
 
 /**
  * Store server.xml Element Host
  */
 public class StandardHostSF extends StoreFactoryBase {
 
+    private static final Class<?> clusterValveClass;
+    static {
+        Class<?> clazz = null;
+        try {
+            clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
+        } catch (ClassNotFoundException e) {
+            // Expected when clustering JARs are not present
+        }
+        clusterValveClass = clazz;
+    }
+
     /**
      * Constructs a new StandardHostSF instance for storing Host elements in 
server.xml.
      */
@@ -73,7 +83,7 @@ public class StandardHostSF extends StoreFactoryBase {
             if (valves != null && valves.length > 0) {
                 List<Valve> hostValves = new ArrayList<>();
                 for (Valve valve : valves) {
-                    if (!(valve instanceof ClusterValve)) {
+                    if (clusterValveClass == null || 
!clusterValveClass.isInstance(valve)) {
                         hostValves.add(valve);
                     }
                 }
diff --git a/java/org/apache/catalina/storeconfig/StoreRegistry.java 
b/java/org/apache/catalina/storeconfig/StoreRegistry.java
index 7389cf9ddf..1c1b21112e 100644
--- a/java/org/apache/catalina/storeconfig/StoreRegistry.java
+++ b/java/org/apache/catalina/storeconfig/StoreRegistry.java
@@ -16,7 +16,9 @@
  */
 package org.apache.catalina.storeconfig;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.naming.directory.DirContext;
@@ -28,17 +30,6 @@ import org.apache.catalina.Realm;
 import org.apache.catalina.Valve;
 import org.apache.catalina.WebResourceRoot;
 import org.apache.catalina.WebResourceSet;
-import org.apache.catalina.ha.CatalinaCluster;
-import org.apache.catalina.ha.ClusterDeployer;
-import org.apache.catalina.ha.ClusterListener;
-import org.apache.catalina.tribes.Channel;
-import org.apache.catalina.tribes.ChannelInterceptor;
-import org.apache.catalina.tribes.ChannelReceiver;
-import org.apache.catalina.tribes.ChannelSender;
-import org.apache.catalina.tribes.Member;
-import org.apache.catalina.tribes.MembershipService;
-import org.apache.catalina.tribes.MessageListener;
-import org.apache.catalina.tribes.transport.DataSender;
 import org.apache.coyote.UpgradeProtocol;
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
@@ -67,11 +58,74 @@ public class StoreRegistry {
     private String version;
 
     // Access Information
-    private static final Class<?>[] interfaces = { CatalinaCluster.class, 
ChannelSender.class, ChannelReceiver.class,
-            Channel.class, MembershipService.class, ClusterDeployer.class, 
Realm.class, Manager.class, DirContext.class,
-            LifecycleListener.class, Valve.class, ClusterListener.class, 
MessageListener.class, DataSender.class,
-            ChannelInterceptor.class, Member.class, WebResourceRoot.class, 
WebResourceSet.class,
-            CredentialHandler.class, UpgradeProtocol.class, 
CookieProcessor.class };
+    // Lazily initialized to gracefully handle optional features like 
clustering
+    private static volatile Class<?>[] interfaces = null;
+
+    /**
+     * Initialize the interfaces array with all available classes.
+     * Uses dynamic loading for optional classes (e.g., clustering) to avoid
+     * ClassNotFoundException when those JARs are not present. This approach
+     * is consistent with how Catalina.addClusterRuleSet() handles clustering.
+     */
+    private static Class<?>[] getInterfaces() {
+        if (interfaces == null) {
+            synchronized (StoreRegistry.class) {
+                if (interfaces == null) {
+                    // Required interfaces - always present
+                    List<Class<?>> list = new ArrayList<>();
+                    list.add(Realm.class);
+                    list.add(Manager.class);
+                    list.add(DirContext.class);
+                    list.add(LifecycleListener.class);
+                    list.add(Valve.class);
+                    list.add(WebResourceRoot.class);
+                    list.add(WebResourceSet.class);
+                    list.add(CredentialHandler.class);
+                    list.add(UpgradeProtocol.class);
+                    list.add(CookieProcessor.class);
+
+                    // Optional clustering interfaces - load dynamically to 
support
+                    // deployments where clustering JARs may not be present
+                    tryAddClass(list, 
"org.apache.catalina.ha.CatalinaCluster");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.ChannelSender");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.ChannelReceiver");
+                    tryAddClass(list, "org.apache.catalina.tribes.Channel");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.MembershipService");
+                    tryAddClass(list, 
"org.apache.catalina.ha.ClusterDeployer");
+                    tryAddClass(list, 
"org.apache.catalina.ha.ClusterListener");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.MessageListener");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.transport.DataSender");
+                    tryAddClass(list, 
"org.apache.catalina.tribes.ChannelInterceptor");
+                    tryAddClass(list, "org.apache.catalina.tribes.Member");
+
+                    interfaces = list.toArray(new Class<?>[0]);
+
+                    if (log.isDebugEnabled()) {
+                        log.debug(sm.getString("registry.interfacesLoaded", 
Integer.valueOf(interfaces.length)));
+                    }
+                }
+            }
+        }
+        return interfaces;
+    }
+
+    /**
+     * Try to load a class by name and add it to the list if successful.
+     * Logs at TRACE level if the class is not available.
+     */
+    private static void tryAddClass(List<Class<?>> list, String className) {
+        try {
+            Class<?> clazz = Class.forName(className, false, 
StoreRegistry.class.getClassLoader());
+            list.add(clazz);
+            if (log.isTraceEnabled()) {
+                log.trace(sm.getString("registry.optionalClassLoaded", 
className));
+            }
+        } catch (ClassNotFoundException | NoClassDefFoundError e) {
+            if (log.isTraceEnabled()) {
+                log.trace(sm.getString("registry.optionalClassNotFound", 
className));
+            }
+        }
+    }
 
     /**
      * Returns the name of this registry.
@@ -130,9 +184,10 @@ public class StoreRegistry {
             }
             if (aClass != null) {
                 desc = descriptors.get(aClass.getName());
-                for (int i = 0; desc == null && i < interfaces.length; i++) {
-                    if (interfaces[i].isAssignableFrom(aClass)) {
-                        desc = descriptors.get(interfaces[i].getName());
+                Class<?>[] availableInterfaces = getInterfaces();
+                for (int i = 0; desc == null && i < 
availableInterfaces.length; i++) {
+                    if (availableInterfaces[i].isAssignableFrom(aClass)) {
+                        desc = 
descriptors.get(availableInterfaces[i].getName());
                     }
                 }
             }
diff --git a/test/org/apache/catalina/storeconfig/TestStoreRegistry.java 
b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
new file mode 100644
index 0000000000..e6869d3642
--- /dev/null
+++ b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
@@ -0,0 +1,104 @@
+/*
+ * 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
+ *
+ *      http://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.catalina.storeconfig;
+
+import java.lang.reflect.Method;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Manager;
+import org.apache.catalina.Realm;
+import org.apache.catalina.Valve;
+
+/**
+ * Test StoreRegistry behavior, particularly dynamic loading of optional 
classes like clustering.
+ *
+ * Verifies StoreRegistry uses the same dynamic loading pattern.
+ */
+public class TestStoreRegistry {
+
+    /**
+     * Test that clustering classes are dynamically loaded like other Tomcat 
components.
+     *
+     * StoreRegistry should initialize successfully whether clustering is 
available or not.
+     * This matches the pattern used in Catalina.addClusterRuleSet().
+     */
+    @Test
+    public void testClusteringClassesOptional() throws Exception {
+        // Verify StoreRegistry initializes successfully with dynamic class 
loading
+        StoreRegistry registry = new StoreRegistry();
+        Assert.assertNotNull("Registry should initialize with dynamic 
loading", registry);
+
+        // Trigger lazy loading of interfaces array
+        Method getInterfacesMethod = 
StoreRegistry.class.getDeclaredMethod("getInterfaces");
+        getInterfacesMethod.setAccessible(true);
+
+        Class<?>[] interfaces = (Class<?>[]) getInterfacesMethod.invoke(null);
+        Assert.assertNotNull("Interfaces should load dynamically", interfaces);
+
+        // Test passes if we get here without ClassNotFoundException.
+        // The actual number of interfaces loaded depends on whether 
clustering is available,
+        // but we should always have at least the core 10 interfaces.
+        Assert.assertTrue("Should have at least 10 core interfaces",
+                interfaces.length >= 10);
+
+        // Verify required core interfaces are always present
+        boolean hasRealm = false;
+        boolean hasManager = false;
+        boolean hasValve = false;
+
+        for (Class<?> iface : interfaces) {
+            if (iface.equals(Realm.class)) {
+                hasRealm = true;
+            }
+            if (iface.equals(Manager.class)) {
+                hasManager = true;
+            }
+            if (iface.equals(Valve.class)) {
+                hasValve = true;
+            }
+        }
+
+        Assert.assertTrue("Should contain Realm interface", hasRealm);
+        Assert.assertTrue("Should contain Manager interface", hasManager);
+        Assert.assertTrue("Should contain Valve interface", hasValve);
+    }
+
+    /**
+     * Test that findDescription works with interface inheritance and
+     * dynamically loaded interfaces.
+     */
+    @Test
+    public void testFindDescriptionWithDynamicInterfaces() throws Exception {
+        StoreRegistry registry = new StoreRegistry();
+
+        // Register a description for the Valve interface
+        StoreDescription valveDesc = new StoreDescription();
+        valveDesc.setId(Valve.class.getName());
+        valveDesc.setTag("Valve");
+        valveDesc.setTagClass(Valve.class.getName());
+        registry.registerDescription(valveDesc);
+
+        // AccessLogValve implements Valve interface - should find via dynamic 
interface matching
+        String accessLogValveClass = 
"org.apache.catalina.valves.AccessLogValve";
+        StoreDescription foundDesc = 
registry.findDescription(accessLogValveClass);
+
+        Assert.assertNotNull("Should find description via interface matching", 
foundDesc);
+        Assert.assertEquals("Should match Valve descriptor", "Valve", 
foundDesc.getTag());
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4059918d82..172556c33d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -167,6 +167,13 @@
         always return <code>null</code> if the provided name starts with
         <code>/</code>. (markt)
       </fix>
+      <fix>
+        Update <code>StoreRegistry</code> to dynamically load optional 
clustering
+        classes rather than statically referencing them. This matches the 
pattern
+        used in <code>Catalina.addClusterRuleSet()</code> and prevents
+        <code>NoClassDefFoundError</code> when 
<code>StoreConfigLifecycleListener</code>
+        is configured but clustering classes are not available. (csutherl)
+      </fix>
       <update>
         Update the default value for the <code>allowPostAsGet</code>
         initialisation parameter of the Default servlet from <code>true</code>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to