This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 998f73add298 CAMEL-25073: camel-management - Start a CamelContext
whose name has characters of JMX object names (item 1) (#27463)
998f73add298 is described below
commit 998f73add298e5367ba14cf9526bf1117265ad20
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Oct 7 13:40:17 2026 +0530
CAMEL-25073: camel-management - Start a CamelContext whose name has
characters of JMX object names (item 1) (#27463)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../management/JmxManagementLifecycleStrategy.java | 57 ++++++-
.../ManagedCamelContextNameObjectNameTest.java | 181 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 9 +
3 files changed, 244 insertions(+), 3 deletions(-)
diff --git
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
index fb4243c97a3c..be8b67f02ede 100644
---
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
+++
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
@@ -28,6 +28,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import javax.management.JMException;
+import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
@@ -199,13 +200,20 @@ public class JmxManagementLifecycleStrategy extends
ServiceSupport implements Li
if (managementName == null) {
managementName = context.getManagementNameStrategy().getName();
}
+ String sanitized = sanitizeManagementName(managementName);
+ if (sanitized != null && !sanitized.equals(managementName)) {
+ LOG.warn("The management name of CamelContext({}) contains
characters that cannot be used in a JMX ObjectName."
+ + " The management name: {} is used instead.",
+ name, sanitized);
+ managementName = sanitized;
+ }
try {
boolean done = false;
while (!done) {
ObjectName on =
getManagementStrategy().getManagementObjectNameStrategy()
.getObjectNameForCamelContext(managementName, name);
- boolean exists = getManagementStrategy().isManagedName(on);
+ boolean exists = isManagedContextName(on);
if (!exists) {
done = true;
} else {
@@ -301,10 +309,10 @@ public class JmxManagementLifecycleStrategy extends
ServiceSupport implements Li
String newName = null;
while (!done) {
// compute the next name
- newName = strategy.getNextName();
+ newName = sanitizeManagementName(strategy.getNextName());
ObjectName on
=
getManagementStrategy().getManagementObjectNameStrategy().getObjectNameForCamelContext(newName,
name);
- done = !getManagementStrategy().isManagedName(on);
+ done = !isManagedContextName(on);
if (LOG.isTraceEnabled()) {
LOG.trace("Using name: {} in ObjectName[{}] exists? {}", name,
on, done);
}
@@ -312,6 +320,49 @@ public class JmxManagementLifecycleStrategy extends
ServiceSupport implements Li
return newName;
}
+ /**
+ * The management name is the value of the (unquoted) context key in every
object name, so it must not contain a
+ * character that an unquoted value cannot have, or that makes the name a
pattern: such characters are replaced with
+ * an underscore.
+ */
+ private static String sanitizeManagementName(String managementName) {
+ if (managementName == null) {
+ return null;
+ }
+ StringBuilder sb = null;
+ for (int i = 0; i < managementName.length(); i++) {
+ char ch = managementName.charAt(i);
+ if (ch == ',' || ch == '=' || ch == ':' || ch == '"' || ch == '*'
|| ch == '?' || ch == '\n') {
+ if (sb == null) {
+ sb = new StringBuilder(managementName);
+ }
+ sb.setCharAt(i, '_');
+ }
+ }
+ return sb != null ? sb.toString() : managementName;
+ }
+
+ /**
+ * Whether the context object name is registered, or another CamelContext
already uses the same context key (with
+ * another name), as the object names of its routes, processors etc. would
then clash.
+ */
+ private boolean isManagedContextName(ObjectName on) throws
MalformedObjectNameException {
+ if (getManagementStrategy().isManagedName(on)) {
+ return true;
+ }
+ String key =
on.getKeyProperty(DefaultManagementObjectNameStrategy.KEY_CONTEXT);
+ String type =
on.getKeyProperty(DefaultManagementObjectNameStrategy.KEY_TYPE);
+ MBeanServer server = getManagementStrategy().getManagementAgent() !=
null
+ ?
getManagementStrategy().getManagementAgent().getMBeanServer() : null;
+ if (key == null || type == null || server == null) {
+ return false;
+ }
+ ObjectName query = new ObjectName(
+ on.getDomain() + ":" +
DefaultManagementObjectNameStrategy.KEY_CONTEXT + "=" + key + ","
+ +
DefaultManagementObjectNameStrategy.KEY_TYPE + "=" + type + ",*");
+ return !server.queryNames(query, null).isEmpty();
+ }
+
/**
* After {@link CamelContext} has been enlisted in JMX using
* {@link #onContextStarted(org.apache.camel.CamelContext)} then we can
enlist any pre-registered services as well,
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedCamelContextNameObjectNameTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedCamelContextNameObjectNameTest.java
new file mode 100644
index 000000000000..51a2a7ac8edc
--- /dev/null
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedCamelContextNameObjectNameTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.camel.management;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedRouteMBean;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The management name is the (unquoted) context key of every object name: a
CamelContext name with characters that an
+ * unquoted ObjectName value cannot have must still start, and must not share
its MBeans with another CamelContext.
+ */
+@DisabledOnOs(OS.AIX)
+class ManagedCamelContextNameObjectNameTest {
+
+ private final List<CamelContext> contexts = new ArrayList<>();
+
+ private CamelContext createCamelContext(String name, String
managementPattern) throws Exception {
+ DefaultCamelContext context = new DefaultCamelContext();
+ context.getCamelContextExtension().setName(name);
+ if (managementPattern != null) {
+
context.getManagementNameStrategy().setNamePattern(managementPattern);
+ }
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").routeId("foo").to("mock:result");
+ }
+ });
+ contexts.add(context);
+ return context;
+ }
+
+ @AfterEach
+ void tearDown() {
+ contexts.forEach(CamelContext::stop);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "my,camel", "my=camel", "my:camel", "my\"camel",
"my*camel", "my?camel", "my\ncamel" })
+ void testContextNameWithObjectNameCharacters(String name) throws Exception
{
+ CamelContext context = createCamelContext(name, null);
+ context.start();
+
+ assertTrue(context.getStatus().isStarted(), "Should be started");
+ assertEquals("my_camel", context.getManagementName());
+
+ MBeanServer mbeanServer =
context.getManagementStrategy().getManagementAgent().getMBeanServer();
+ ObjectName on = ObjectName.getInstance(
+ "org.apache.camel:context=my_camel,type=context,name=" +
ObjectName.quote(name));
+ assertTrue(mbeanServer.isRegistered(on), "The CamelContext MBean
should be registered");
+ assertEquals(name, mbeanServer.getAttribute(on, "CamelId"));
+ assertTrue(mbeanServer.isRegistered(
+
ObjectName.getInstance("org.apache.camel:context=my_camel,type=routes,name=\"foo\"")),
+ "The route MBean should be registered");
+
+ // the queries that build the context key from the management name
work as for any other name
+ ManagedCamelContext managed =
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+ ManagedRouteMBean route = managed.getManagedRoute("foo");
+ assertNotNull(route);
+ assertEquals(name, route.getCamelId());
+ }
+
+ @Test
+ void testSanitizedNameDoesNotShareMBeansWithExistingName() throws
Exception {
+ CamelContext camel1 = createCamelContext("my_camel", null);
+ CamelContext camel2 = createCamelContext("my,camel", null);
+ camel1.start();
+ camel2.start();
+
+ assertSeparateMBeans(camel1, camel2);
+ }
+
+ @Test
+ void testExistingNameDoesNotShareMBeansWithSanitizedName() throws
Exception {
+ CamelContext camel1 = createCamelContext("my,camel", null);
+ CamelContext camel2 = createCamelContext("my_camel", null);
+ camel1.start();
+ camel2.start();
+
+ assertSeparateMBeans(camel1, camel2);
+ }
+
+ @Test
+ void testNameOfCamelContextIsFreeNameOfAnother() throws Exception {
+ CamelContext camel1 = createCamelContext("foo", null);
+ CamelContext camel2 = createCamelContext("foo", null);
+ camel1.start();
+ camel2.start();
+
+ // the second foo got a free name such as foo-1, which is then the
name of a third CamelContext
+ String freeName = camel2.getManagementName();
+ assertNotEquals("foo", freeName);
+ CamelContext camel3 = createCamelContext(freeName, null);
+ camel3.start();
+
+ assertSeparateMBeans(camel2, camel3);
+ }
+
+ @Test
+ void testManagementNamePatternOfTheAgentPicksFreeName() throws Exception {
+ // camel-main and Spring Boot set jmxManagementNamePattern (default
#name#) on the management agent, not on the
+ // management name strategy: a clash still gets the next free name
instead of a veto
+ CamelContext camel1 = createCamelContext("foo", null);
+ CamelContext camel2 = createCamelContext("foo", null);
+
camel1.getManagementStrategy().getManagementAgent().setManagementNamePattern("#name#");
+
camel2.getManagementStrategy().getManagementAgent().setManagementNamePattern("#name#");
+ camel1.start();
+ camel2.start();
+
+ assertEquals("foo", camel1.getManagementName());
+ assertSeparateMBeans(camel1, camel2);
+ }
+
+ @Test
+ void testManagementNamePatternOfTheStrategyWithoutCounterVetoes() throws
Exception {
+ // a pattern without #counter# set on the management name strategy
(such as managementNamePattern of the XML
+ // camelContext) is fixed: there is no next free name
+ CamelContext camel1 = createCamelContext("foo", "#name#");
+ CamelContext camel2 = createCamelContext("foo", "#name#");
+ camel1.start();
+
+ Exception e = assertThrows(Exception.class, camel2::start);
+ assertTrue(e.getCause().getMessage().contains("is already
registered"), e.getCause().getMessage());
+ }
+
+ @Test
+ void testFixedManagementNameUsedByAnotherCamelContext() throws Exception {
+ CamelContext camel1 = createCamelContext("foo", "myFoo");
+ CamelContext camel2 = createCamelContext("bar", "myFoo");
+ camel1.start();
+
+ // another CamelContext uses the context key, so the object names of
the routes would clash
+ Exception e = assertThrows(Exception.class, camel2::start);
+ assertTrue(e.getCause().getMessage().contains("is already
registered"), e.getCause().getMessage());
+ }
+
+ private static void assertSeparateMBeans(CamelContext camel1, CamelContext
camel2) {
+ assertNotEquals(camel1.getManagementName(),
camel2.getManagementName());
+ for (CamelContext context : List.of(camel1, camel2)) {
+ ManagedCamelContext managed =
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+ ManagedRouteMBean route = managed.getManagedRoute("foo");
+ assertNotNull(route, "The route MBean of " + context.getName() + "
should be registered");
+ assertEquals(context.getName(), route.getCamelId(), "The route
MBean should be the one of its CamelContext");
+ }
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 9c7d82c1a1f7..60e5212a3d24 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -719,6 +719,15 @@ should be reviewed.
`InflightRepository.InflightExchange` and
`AsyncProcessorAwaitManager.AwaitThread` gained a `getNodeSource()` method
for the same value. Both are `default` methods returning `null`, so existing
implementations continue to compile.
+A CamelContext name (or management name) with `, = : " * ?` or a line feed
made the CamelContext fail to start with
+JMX. These characters are now replaced with `_` in the management name, the
`context` key of the object names (the
+`CamelId` attribute keeps the name). A CamelContext whose management name is
already the `context` key of another
+CamelContext now gets the next free management name, or fails to start with a
fixed management name pattern; before,
+both started and the second one had no MBeans for its routes, processors and
services. A fixed management name
+pattern is a pattern without `#counter#` set on the `ManagementNameStrategy`
(such as the `managementNamePattern` of
+`<camelContext>` in Spring XML); the `jmxManagementNamePattern` option of
camel-main and Spring Boot (default `#name#`)
+is the pattern of the management agent, and with it the next free management
name is used.
+
The `Redeliveries` statistic now also counts a redelivery attempt that
succeeds, and it is only counted where the
redelivery happened. For a processor it is the number of redelivery attempts
of that processor. For a route it is the
number of exchanges that were redelivered by a processor of that route, and
for the CamelContext the number of