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

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


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

commit da245b931c1f0b5980d6a69713c20b425bfd6191
Author: Coty Sutherland <[email protected]>
AuthorDate: Tue May 12 19:07:19 2026 +0300

    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 d3e8585df5..4c2aa53bee 100644
--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties
+++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties
@@ -28,8 +28,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 17674a9f3d..4c69624ddb 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.
      */
@@ -70,7 +80,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 a4d976f81c..ab6de6d318 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.
      */
@@ -74,7 +84,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 69cd411b69..04fd13c26d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -401,6 +401,13 @@
         Log an information message if an APR Connector is used, recommending
         that the appropriate NIO Connector is used instead. (markt)
       </add>
+      <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>
       <fix>
         Respect the value for the <code>jdk.tls.namedGroups</code> system
         property as the default value for the configured group list on the


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

Reply via email to