This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new cb6415db88 Update StoreRegistry to dynamically load clustering classes
(#1005)
cb6415db88 is described below
commit cb6415db88362944c3c90b6dbf83a70819a79b23
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 c58534c7dd..a2544e1f93 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -390,6 +390,13 @@
when closing am HTTP/2 connection. Pull request <pr>917</pr> provided
by
Kai Burjack. (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>
<update>
Update the minimum recommended version of Tomcat Native so that users
of
1.3.x are recommended to update to 2.0.x. (markt)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]