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

gnodet 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 98eacfb58be3 CAMEL-26237: camel-ehcache: fix shared CacheManager, null 
body handling, consumer events, and type defaults (#28822)
98eacfb58be3 is described below

commit 98eacfb58be358bd34344fca7984914f029d43f1
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Nov 2 14:59:09 2026 +0100

    CAMEL-26237: camel-ehcache: fix shared CacheManager, null body handling, 
consumer events, and type defaults (#28822)
    
    * CAMEL-26237: camel-ehcache: fix shared CacheManager, null body, consumer 
events, and type defaults
    
    Four bugs fixed in the camel-ehcache component:
    - EhcacheComponent: share a single default CacheManager across all endpoints
      without explicit config via a sentinel key in the managers map
    - EhcacheManager: replace UserManagedCache (no event executors) with a fully
      managed cache so consumer event listeners fire correctly
    - EhcacheProducer: fix null body storing the DefaultMessage instead of 
throwing;
      use message.getBody() instead of message.getBody(Object.class) for Object 
type
    - EhcacheManager: default keyType and valueType independently so setting 
only
      one of them does not cause NPE
    
    The getCache() method now accepts an optional endpoint-specific 
configuration
    parameter so that a shared default CacheManager can still use per-endpoint
    cache-level configurations (named or default CacheConfiguration instances).
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-26237: address review: unbounded heap, test coverage, concurrent 
createCache, docs and upgrade guide
    
    * CAMEL-26237: fix tests per review: distinct URIs in EhcacheDefaultsTest, 
assert refcount and use second component in EhcacheRemoveRouteTest
    
    * CAMEL-26237: address review: add Status.UNINITIALIZED assertion and fix 
stale comment in EhcacheRemoveRouteTest
    
    * CAMEL-26237: remove unnecessary @SuppressWarnings("unchecked") from 
createCacheOrGetExisting
    
    * CAMEL-26237: fix doc NOTE wording and upgrade guide lifecycle sentence
    
    * CAMEL-26237: strengthen EhcacheRemoveRouteTest: add Status.UNINITIALIZED 
assertion and verify k1 gone after restart
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../camel/catalog/docs/ehcache-component.adoc      |  16 +-
 .../src/main/docs/ehcache-component.adoc           |  16 +-
 .../camel/component/ehcache/EhcacheComponent.java  |   5 +-
 .../camel/component/ehcache/EhcacheConsumer.java   |   2 +-
 .../camel/component/ehcache/EhcacheManager.java    |  95 +++++-----
 .../camel/component/ehcache/EhcacheProducer.java   |   4 +-
 .../ehcache/EhcacheCacheConfigurationTest.java     |   4 +-
 .../component/ehcache/EhcacheDefaultsTest.java     | 207 +++++++++++++++++++++
 .../component/ehcache/EhcacheRemoveRouteTest.java  |  77 ++++----
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  27 +++
 10 files changed, 349 insertions(+), 104 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
index aa4e4cf710b5..2d01f34a06ee 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
@@ -99,14 +99,14 @@ The producer runs the action of the `CamelEhcacheAction` 
header (or of the `acti
                     message: "Order ${header.orderId} is not in the cache"
 ----
 
-NOTE: Without a cache manager or a cache configuration, each endpoint creates 
its own cache.
-`ehcache:orders?action=PUT` and `ehcache:orders?action=GET` are then two 
different caches.
-Set the action in a header and use one endpoint, as above, or share a cache 
manager with `configurationUri`,
-`cacheManager` or `cacheManagerConfiguration`.
-
-The consumer receives cache events only when the cache comes from a 
configuration or a cache manager.
-The default cache of an endpoint has no events. In this example, both 
endpoints use the same `ehcache.xml`,
-so they share its cache manager:
+NOTE: Endpoints without a `cacheManager`, `cacheManagerConfiguration` or 
`configurationUri` share one
+`CacheManager` per component. `ehcache:orders?action=PUT` and 
`ehcache:orders?action=GET` use the same
+cache, and all endpoints for the same cache name must use the same `keyType` 
and `valueType`.
+A cache created on demand has no size limit; use a cache configuration or 
`configurationUri` to bound it
+or to set an expiry.
+
+The consumer receives cache events for caches created on demand in the shared 
manager. In this example, both
+endpoints use the same `ehcache.xml` for a custom capacity or expiry:
 
 [source,yaml]
 ----
diff --git a/components/camel-ehcache/src/main/docs/ehcache-component.adoc 
b/components/camel-ehcache/src/main/docs/ehcache-component.adoc
index aa4e4cf710b5..2d01f34a06ee 100644
--- a/components/camel-ehcache/src/main/docs/ehcache-component.adoc
+++ b/components/camel-ehcache/src/main/docs/ehcache-component.adoc
@@ -99,14 +99,14 @@ The producer runs the action of the `CamelEhcacheAction` 
header (or of the `acti
                     message: "Order ${header.orderId} is not in the cache"
 ----
 
-NOTE: Without a cache manager or a cache configuration, each endpoint creates 
its own cache.
-`ehcache:orders?action=PUT` and `ehcache:orders?action=GET` are then two 
different caches.
-Set the action in a header and use one endpoint, as above, or share a cache 
manager with `configurationUri`,
-`cacheManager` or `cacheManagerConfiguration`.
-
-The consumer receives cache events only when the cache comes from a 
configuration or a cache manager.
-The default cache of an endpoint has no events. In this example, both 
endpoints use the same `ehcache.xml`,
-so they share its cache manager:
+NOTE: Endpoints without a `cacheManager`, `cacheManagerConfiguration` or 
`configurationUri` share one
+`CacheManager` per component. `ehcache:orders?action=PUT` and 
`ehcache:orders?action=GET` use the same
+cache, and all endpoints for the same cache name must use the same `keyType` 
and `valueType`.
+A cache created on demand has no size limit; use a cache configuration or 
`configurationUri` to bound it
+or to set an expiry.
+
+The consumer receives cache events for caches created on demand in the shared 
manager. In this example, both
+endpoints use the same `ehcache.xml` for a custom capacity or expiry:
 
 [source,yaml]
 ----
diff --git 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
index e0ff6dbfa248..65e96005123e 100644
--- 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
+++ 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
@@ -110,7 +110,10 @@ public class EhcacheComponent extends DefaultComponent {
         }
 
         LOGGER.info("EhcacheManager configured with default builder");
-        return new 
EhcacheManager(CacheManagerBuilder.newCacheManagerBuilder().build(), true, 
configuration);
+        return managers.computeIfAbsent(
+                EhcacheComponent.class,
+                k -> new EhcacheManager(
+                        CacheManagerBuilder.newCacheManagerBuilder().build(), 
true, new EhcacheConfiguration()));
     }
 
     // ****************************
diff --git 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
index 64c4e3e5f1c0..06324dbe1e9e 100644
--- 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
+++ 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
@@ -59,7 +59,7 @@ public class EhcacheConsumer extends DefaultConsumer 
implements CacheEventListen
         // the producer, as it opens the manager again if it was closed
         endpointManager.incRef();
         try {
-            this.cache = endpointManager.getCache(cacheName, kt, vt);
+            this.cache = endpointManager.getCache(cacheName, kt, vt, 
configuration);
             this.cache.getRuntimeConfiguration().registerCacheEventListener(
                     this,
                     configuration.getEventOrdering(),
diff --git 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
index 4b7e4f9f38f6..4b1d5858a068 100644
--- 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
+++ 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.component.ehcache;
 
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -28,10 +26,9 @@ import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.ReferenceCount;
 import org.ehcache.Cache;
 import org.ehcache.CacheManager;
-import org.ehcache.UserManagedCache;
 import org.ehcache.config.CacheConfiguration;
 import org.ehcache.config.builders.CacheConfigurationBuilder;
-import org.ehcache.config.builders.UserManagedCacheBuilder;
+import org.ehcache.config.builders.ResourcePoolsBuilder;
 import org.ehcache.spi.service.ServiceConfiguration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,7 +38,6 @@ public class EhcacheManager extends ServiceSupport {
 
     private final EhcacheConfiguration configuration;
     private final CacheManager cacheManager;
-    private final ConcurrentMap<String, UserManagedCache<?, ?>> userCaches;
     private final ReferenceCount refCount;
     // the reference count changes together with the start and stop of the 
manager, so that the last two users that
     // stop at the same time do not both (or neither) see the count reach 0, 
and a user that starts while the last one
@@ -50,15 +46,11 @@ public class EhcacheManager extends ServiceSupport {
 
     public EhcacheManager(CacheManager cacheManager, boolean managed, 
EhcacheConfiguration configuration) {
         this.cacheManager = ObjectHelper.notNull(cacheManager, "cacheManager");
-        this.userCaches = new ConcurrentHashMap<>();
         this.configuration = configuration;
         this.refCount = ReferenceCount.on(
                 managed ? cacheManager::init : () -> {
                 },
                 () -> {
-                    // the last user closes the caches created without a 
configuration, as the endpoint that shuts
-                    // the manager down may have been shut down while other 
routes still used them
-                    closeUserCaches();
                     if (managed) {
                         cacheManager.close();
                     }
@@ -113,47 +105,32 @@ public class EhcacheManager extends ServiceSupport {
         }
     }
 
-    @Override
-    protected void doShutdown() throws Exception {
-        closeUserCaches();
-    }
-
-    private void closeUserCaches() {
-        if (userCaches != null && !userCaches.isEmpty()) {
-            for (UserManagedCache cache : userCaches.values()) {
-                try {
-                    cache.close();
-                } catch (Exception e) {
-                    // ignore
-                }
-            }
-            userCaches.clear();
-        }
+    public <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V> 
valueType) throws Exception {
+        return getCache(name, keyType, valueType, configuration);
     }
 
     @SuppressWarnings("unchecked")
-    public <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V> 
valueType) throws Exception {
+    <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V> 
valueType, EhcacheConfiguration endpointConfiguration)
+            throws Exception {
+        EhcacheConfiguration effectiveConfig = endpointConfiguration != null ? 
endpointConfiguration : configuration;
         CacheConfiguration<K, V> cacheConfiguration = null;
-        if (configuration != null) {
-            if (configuration.hasConfiguration(name)) {
+        if (effectiveConfig != null) {
+            if (effectiveConfig.hasConfiguration(name)) {
                 LOGGER.debug("Using custom cache configuration for cache {}", 
name);
-                cacheConfiguration = 
CacheConfiguration.class.cast(configuration.getConfigurations().get(name));
-            } else if (configuration.hasConfiguration()) {
+                cacheConfiguration = 
CacheConfiguration.class.cast(effectiveConfig.getConfigurations().get(name));
+            } else if (effectiveConfig.hasConfiguration()) {
                 LOGGER.debug("Using global cache configuration for cache {}", 
name);
-                cacheConfiguration = 
CacheConfiguration.class.cast(configuration.getConfiguration());
+                cacheConfiguration = 
CacheConfiguration.class.cast(effectiveConfig.getConfiguration());
             }
         }
-        if (keyType == null && valueType == null) {
-            if (cacheConfiguration != null) {
-                keyType = cacheConfiguration.getKeyType();
-                valueType = cacheConfiguration.getValueType();
-            } else {
-                keyType = (Class<K>) Object.class;
-                valueType = (Class<V>) Object.class;
-            }
+        if (keyType == null) {
+            keyType = cacheConfiguration != null ? 
cacheConfiguration.getKeyType() : (Class<K>) Object.class;
+        }
+        if (valueType == null) {
+            valueType = cacheConfiguration != null ? 
cacheConfiguration.getValueType() : (Class<V>) Object.class;
         }
         Cache<K, V> cache = cacheManager.getCache(name, keyType, valueType);
-        if (cache == null && configuration != null && 
configuration.isCreateCacheIfNotExist()) {
+        if (cache == null && effectiveConfig != null && 
effectiveConfig.isCreateCacheIfNotExist()) {
             if (cacheConfiguration != null) {
                 if (keyType != cacheConfiguration.getKeyType() || valueType != 
cacheConfiguration.getValueType()) {
                     LOGGER.info("Mismatch keyType / valueType configuration 
for cache {}", name);
@@ -167,16 +144,16 @@ public class EhcacheManager extends ServiceSupport {
                     }
                     cacheConfiguration = builder.build();
                 }
-                cache = cacheManager.createCache(name, cacheConfiguration);
+                cache = createCacheOrGetExisting(name, cacheConfiguration, 
keyType, valueType);
             } else {
-                // If a cache configuration is not provided, create a User 
Managed
-                // Cache
-                LOGGER.debug("Using a UserManagedCache for cache {} as no 
configuration has been found", name);
-                Class<K> kt = keyType;
-                Class<V> vt = valueType;
-                cache = Cache.class.cast(userCaches.computeIfAbsent(
-                        name,
-                        key -> 
UserManagedCacheBuilder.newUserManagedCacheBuilder(kt, vt).build(true)));
+                // If a cache configuration is not provided, create a managed 
cache with default configuration
+                // so that event listeners work correctly (UserManagedCache 
lacks event executor services)
+                LOGGER.debug("Creating a managed cache for {} with default 
configuration as no configuration has been found",
+                        name);
+                cache = createCacheOrGetExisting(name,
+                        CacheConfigurationBuilder.newCacheConfigurationBuilder(
+                                keyType, valueType, 
ResourcePoolsBuilder.heap(Long.MAX_VALUE)).build(),
+                        keyType, valueType);
             }
         }
 
@@ -187,6 +164,26 @@ public class EhcacheManager extends ServiceSupport {
         return cache;
     }
 
+    /**
+     * Creates a cache with the given configuration, or returns the existing 
one if another thread raced to create it
+     * first. Ehcache throws {@link IllegalArgumentException} when two callers 
create the same cache concurrently; this
+     * method catches that and falls back to {@link CacheManager#getCache}.
+     */
+    private <K, V> Cache<K, V> createCacheOrGetExisting(
+            String name, CacheConfiguration<K, V> cacheConfiguration, Class<K> 
keyType, Class<V> valueType) {
+        try {
+            return cacheManager.createCache(name, cacheConfiguration);
+        } catch (IllegalArgumentException e) {
+            // another thread created the cache between our getCache() and 
createCache() calls
+            LOGGER.debug("Cache {} was created concurrently; reusing the 
existing one", name);
+            Cache<K, V> existing = cacheManager.getCache(name, keyType, 
valueType);
+            if (existing == null) {
+                throw e;
+            }
+            return existing;
+        }
+    }
+
     CacheManager getCacheManager() {
         return this.cacheManager;
     }
diff --git 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
index 8ac278211eb2..4059a0431635 100644
--- 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
+++ 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
@@ -65,7 +65,7 @@ public class EhcacheProducer extends HeaderSelectorProducer {
         manager.incRef();
         retained = true;
         // after the reference, which opens the manager again if it was closed
-        cache = manager.getCache(cacheName, keyType, valueType);
+        cache = manager.getCache(cacheName, keyType, valueType, configuration);
     }
 
     @Override
@@ -193,7 +193,7 @@ public class EhcacheProducer extends HeaderSelectorProducer 
{
             if (type instanceof String) {
                 Class<?> clazz = 
getEndpoint().getCamelContext().getClassResolver().resolveClass((String) type);
                 value = message.getBody(clazz);
-            } else if (type instanceof Class) {
+            } else if (type instanceof Class && type != Object.class) {
                 value = message.getBody((Class) type);
             } else {
                 value = message.getBody();
diff --git 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
index d410910f2db0..69290c3eca94 100644
--- 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
+++ 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
@@ -22,7 +22,6 @@ import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.ehcache.Cache;
 import org.ehcache.CacheManager;
-import org.ehcache.UserManagedCache;
 import org.ehcache.config.CacheConfiguration;
 import org.ehcache.config.ResourcePools;
 import org.ehcache.config.ResourceType;
@@ -36,7 +35,6 @@ import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class EhcacheCacheConfigurationTest extends CamelTestSupport {
     
@EndpointInject("ehcache:myProgrammaticCacheConf?configuration=#myProgrammaticConfiguration")
@@ -111,7 +109,7 @@ public class EhcacheCacheConfigurationTest extends 
CamelTestSupport {
 
         Cache<Object, Object> cache = 
ehcacheUserConf.getManager().getCache("myUserCacheConf", Object.class, 
Object.class);
 
-        assertTrue(cache instanceof UserManagedCache);
+        assertNotNull(cache);
         assertEquals("user-val", cache.get("user-key"));
     }
 
diff --git 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
new file mode 100644
index 000000000000..7c2b0ca20232
--- /dev/null
+++ 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.component.ehcache;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.ehcache.event.EventType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests for the default (no explicit cacheManager) behavior of camel-ehcache: 
shared CacheManager across endpoints,
+ * consumer event delivery on unconfigured caches, null body handling, and 
independent key/value type defaults.
+ */
+class EhcacheDefaultsTest extends CamelTestSupport {
+
+    /**
+     * Bug 1: Two endpoints sharing the same cache name without explicit 
cacheManager should share data. A PUT on one
+     * endpoint followed by a GET on another endpoint must return the stored 
value.
+     */
+    @Test
+    void testSharedDefaultCacheManager() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:get-result");
+        mock.expectedMinimumMessageCount(1);
+        mock.expectedHeaderReceived(EhcacheConstants.ACTION_SUCCEEDED, true);
+        mock.expectedHeaderReceived(EhcacheConstants.ACTION_HAS_RESULT, true);
+
+        String key = "shared-key";
+        String val = "shared-value";
+
+        // Two distinct endpoint instances (different URIs due to action 
param) must share the same manager,
+        // because both refer to the same cache name with no explicit 
cacheManager option.
+        EhcacheEndpoint putEndpoint
+                = context.getEndpoint("ehcache://defaultTestCache?action=PUT", 
EhcacheEndpoint.class);
+        EhcacheEndpoint getEndpoint
+                = context.getEndpoint("ehcache://defaultTestCache?action=GET", 
EhcacheEndpoint.class);
+        assertSame(putEndpoint.getManager(), getEndpoint.getManager(),
+                "distinct endpoints for the same cache name must share the 
default CacheManager");
+
+        // PUT via the first endpoint (action=PUT in the URI, distinct from 
the GET route)
+        fluentTemplate()
+                .withHeader(EhcacheConstants.KEY, key)
+                .withBody(val)
+                .to("direct://put")
+                .send();
+
+        // GET via a different endpoint (action=GET — same cache name, no 
explicit cacheManager, different URI)
+        fluentTemplate()
+                .withHeader(EhcacheConstants.KEY, key)
+                .to("direct://get")
+                .send();
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        assertEquals(val, 
mock.getExchanges().get(0).getIn().getBody(String.class));
+    }
+
+    /**
+     * Bug 2: A consumer on a cache without explicit configuration should 
receive CREATED events when values are PUT via
+     * the producer. Previously, the UserManagedCache fallback lacked event 
executor services, so listeners were
+     * silently never called.
+     */
+    @Test
+    void testConsumerReceivesEventsWithoutExplicitConfig() throws Exception {
+        MockEndpoint created = getMockEndpoint("mock:events");
+        created.expectedMinimumMessageCount(1);
+        created.expectedHeaderReceived(EhcacheConstants.EVENT_TYPE, 
EventType.CREATED);
+
+        String key = "event-key";
+        String val = "event-value";
+
+        fluentTemplate()
+                .withHeader(EhcacheConstants.ACTION, 
EhcacheConstants.ACTION_PUT)
+                .withHeader(EhcacheConstants.KEY, key)
+                .withBody(val)
+                .to("direct://put-events")
+                .send();
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        assertEquals(key, 
created.getExchanges().get(0).getIn().getHeader(EhcacheConstants.KEY));
+        assertEquals(val, 
created.getExchanges().get(0).getIn().getBody(String.class));
+    }
+
+    /**
+     * Bug 3: A PUT with null body and no CamelEhcacheValue header must throw 
CamelExchangeException, not silently store
+     * the DefaultMessage object.
+     */
+    @Test
+    void testPutWithNullBodyThrows() throws Exception {
+        Exchange result = fluentTemplate()
+                .withHeader(EhcacheConstants.ACTION, 
EhcacheConstants.ACTION_PUT)
+                .withHeader(EhcacheConstants.KEY, "null-body-key")
+                .withBody((Object) null)
+                .to("direct://put")
+                .send();
+
+        assertNotNull(result.getException());
+        assertInstanceOf(CamelExchangeException.class, result.getException());
+    }
+
+    /**
+     * Bug 4: Setting valueType without keyType should not cause NPE. The 
keyType must independently default to
+     * Object.class.
+     */
+    @Test
+    void testValueTypeWithoutKeyTypeDoesNotThrowNPE() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:typed-result");
+        mock.expectedMinimumMessageCount(1);
+        mock.expectedHeaderReceived(EhcacheConstants.ACTION_SUCCEEDED, true);
+
+        String key = "typed-key";
+        String val = "typed-value";
+
+        fluentTemplate()
+                .withHeader(EhcacheConstants.ACTION, 
EhcacheConstants.ACTION_PUT)
+                .withHeader(EhcacheConstants.KEY, key)
+                .withBody(val)
+                .to("direct://put-typed")
+                .send();
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        // Verify we can GET the value back
+        MockEndpoint getMock = getMockEndpoint("mock:typed-get-result");
+        getMock.expectedMinimumMessageCount(1);
+        getMock.expectedHeaderReceived(EhcacheConstants.ACTION_HAS_RESULT, 
true);
+
+        fluentTemplate()
+                .withHeader(EhcacheConstants.ACTION, 
EhcacheConstants.ACTION_GET)
+                .withHeader(EhcacheConstants.KEY, key)
+                .to("direct://get-typed")
+                .send();
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        assertEquals(val, 
getMock.getExchanges().get(0).getIn().getBody(String.class));
+    }
+
+    /**
+     * Bug 3 (variant): PUT_IF_ABSENT with null body must also throw.
+     */
+    @Test
+    void testPutIfAbsentWithNullBodyThrows() throws Exception {
+        Exchange result = fluentTemplate()
+                .withHeader(EhcacheConstants.ACTION, 
EhcacheConstants.ACTION_PUT_IF_ABSENT)
+                .withHeader(EhcacheConstants.KEY, "null-body-key-2")
+                .withBody((Object) null)
+                .to("direct://put")
+                .send();
+
+        assertNotNull(result.getException());
+        assertInstanceOf(CamelExchangeException.class, result.getException());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                // Bug 1: two endpoints with distinct URIs (action=PUT vs 
action=GET) for the same cache name,
+                // no explicit cacheManager — they must share one CacheManager 
and thus the same cache data.
+                from("direct://put")
+                        .to("ehcache://defaultTestCache?action=PUT")
+                        .to("mock:put-result");
+                from("direct://get")
+                        .to("ehcache://defaultTestCache?action=GET")
+                        .to("mock:get-result");
+
+                // Bug 2: consumer on a cache without explicit config
+                from("direct://put-events")
+                        .to("ehcache://eventsCache")
+                        .to("mock:put-events-result");
+                
from("ehcache://eventsCache?eventTypes=CREATED&eventFiring=SYNCHRONOUS")
+                        .to("mock:events");
+
+                // Bug 4: valueType set without keyType
+                from("direct://put-typed")
+                        .to("ehcache://typedCache?valueType=java.lang.String")
+                        .to("mock:typed-result");
+                from("direct://get-typed")
+                        .to("ehcache://typedCache?valueType=java.lang.String")
+                        .to("mock:typed-get-result");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
index 8e5a7b9e3631..b7f363c95d60 100644
--- 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
+++ 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
@@ -26,9 +26,9 @@ import org.apache.camel.ProducerTemplate;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.test.junit6.CamelTestSupport;
+import org.ehcache.Cache;
 import org.ehcache.CacheManager;
 import org.ehcache.Status;
-import org.ehcache.UserManagedCache;
 import org.ehcache.config.CacheConfiguration;
 import org.ehcache.config.builders.CacheConfigurationBuilder;
 import org.ehcache.config.builders.CacheManagerBuilder;
@@ -36,6 +36,7 @@ import org.ehcache.config.builders.ResourcePoolsBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -144,42 +145,56 @@ class EhcacheRemoveRouteTest extends CamelTestSupport {
         put("direct:plain", "k2", "v2");
         assertEquals("v2", get("direct:plain", "k2"));
 
-        // without a configuration the endpoint's own EhcacheManager creates a 
user managed cache
+        // without a configuration the endpoints share the component's default 
EhcacheManager, which creates the cache on demand
         EhcacheEndpoint endpoint = context.getEndpoint("ehcache:plain", 
EhcacheEndpoint.class);
-        UserManagedCache<?, ?> cache
-                = (UserManagedCache<?, ?>) 
endpoint.getManager().getCache("plain", Object.class, Object.class);
-        assertEquals(Status.AVAILABLE, cache.getStatus());
+        Cache<?, ?> cache = endpoint.getManager().getCache("plain", 
Object.class, Object.class);
+        assertNotNull(cache);
 
-        // the endpoint was shut down with the removed route while the 
producer still used the cache: the producer, as
-        // the last user, closes it
         context.stop();
-        assertEquals(Status.UNINITIALIZED, cache.getStatus());
+
+        // all endpoints were stopped with the context: the manager reference 
count must have reached 0
+        assertEquals(0, endpoint.getManager().getReferenceCount().get());
+        // and the underlying CacheManager was closed (ref count hit 0 → 
managed close)
+        assertEquals(Status.UNINITIALIZED, 
endpoint.getManager().getCacheManager().getStatus());
     }
 
     @Test
     void producerTakesTheCacheAgainWhenItsManagerWasClosedAndOpenedAgain() 
throws Exception {
-        EhcacheEndpoint endpoint = context.getEndpoint("ehcache:restarted", 
EhcacheEndpoint.class);
-        Producer producer = endpoint.createProducer();
-        // as a route or a ProducerTemplate does with the producers they create
-        CamelContextAware.trySetCamelContext(producer, context);
-        producer.start();
-        assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT, "k1", 
"v1").getException());
-
-        // the last users stop: the manager closes the cache created on demand
-        producer.stop();
-        endpoint.stop();
-        assertEquals(0, endpoint.getManager().getReferenceCount().get());
-
-        // the same producer is started again after its endpoint, and uses the 
cache the manager has now
-        endpoint.start();
-        producer.start();
+        // Register a second component instance so this endpoint gets its own 
default CacheManager,
+        // independent of the default one shared by other test routes (which 
would keep references
+        // and prevent the manager from closing when the producer and endpoint 
below stop).
+        EhcacheComponent ehcache2 = new EhcacheComponent();
+        context.addComponent("ehcache2", ehcache2);
         try {
-            assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT, 
"k2", "v2").getException());
-            Exchange got = send(endpoint, producer, 
EhcacheConstants.ACTION_GET, "k2", null);
-            assertNull(got.getException());
-            assertEquals("v2", got.getMessage().getBody());
-        } finally {
+            EhcacheEndpoint endpoint = 
context.getEndpoint("ehcache2:restarted", EhcacheEndpoint.class);
+            Producer producer = endpoint.createProducer();
+            // as a route or a ProducerTemplate does with the producers they 
create
+            CamelContextAware.trySetCamelContext(producer, context);
+            producer.start();
+            assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT, 
"k1", "v1").getException());
+
+            // the last users release their reference; the manager closes 
because ref count reaches 0
             producer.stop();
+            endpoint.stop();
+            assertEquals(0, endpoint.getManager().getReferenceCount().get());
+            // the underlying CacheManager was closed when the ref count hit 0
+            assertEquals(Status.UNINITIALIZED, 
endpoint.getManager().getCacheManager().getStatus());
+
+            // the same producer is started again after its endpoint, and uses 
the cache the manager has now
+            endpoint.start();
+            producer.start();
+            try {
+                // k1 is gone: the manager was closed and re-opened with a 
fresh cache
+                assertNull(send(endpoint, producer, 
EhcacheConstants.ACTION_GET, "k1", null).getMessage().getBody());
+                assertNull(send(endpoint, producer, 
EhcacheConstants.ACTION_PUT, "k2", "v2").getException());
+                Exchange got = send(endpoint, producer, 
EhcacheConstants.ACTION_GET, "k2", null);
+                assertNull(got.getException());
+                assertEquals("v2", got.getMessage().getBody());
+            } finally {
+                producer.stop();
+            }
+        } finally {
+            context.removeComponent("ehcache2");
         }
     }
 
@@ -191,14 +206,12 @@ class EhcacheRemoveRouteTest extends CamelTestSupport {
         userTemplate.sendBodyAndHeaders("ehcache:fromTemplate", "v1",
                 Map.of(EhcacheConstants.ACTION, EhcacheConstants.ACTION_PUT, 
EhcacheConstants.KEY, "k1"));
         EhcacheEndpoint endpoint = context.getEndpoint("ehcache:fromTemplate", 
EhcacheEndpoint.class);
-        UserManagedCache<?, ?> cache
-                = (UserManagedCache<?, ?>) 
endpoint.getManager().getCache("fromTemplate", Object.class, Object.class);
-        assertEquals(Status.AVAILABLE, cache.getStatus());
+        Cache<?, ?> cache = endpoint.getManager().getCache("fromTemplate", 
Object.class, Object.class);
+        assertNotNull(cache);
 
         context.stop();
 
         assertEquals(0, endpoint.getManager().getReferenceCount().get());
-        assertEquals(Status.UNINITIALIZED, cache.getStatus());
         userTemplate.stop();
     }
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 9becf9752ee5..08b16636c546 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -13,6 +13,33 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading Camel 4.23 to 4.24
 
+=== camel-ehcache - shared CacheManager and changed defaults
+
+The following user-visible changes take effect in 4.24:
+
+* **Shared CacheManager.** Endpoints that have the same `cacheName` and no 
explicit `cacheManager`,
+  `cacheManagerConfiguration`, or `configurationUri` now share one 
`CacheManager` per component instance.
+  Previously each endpoint created its own unmanaged (`UserManagedCache`) 
cache.
+  - The same name now means the same cache. `ehcache:foo?action=PUT` and 
`ehcache:foo?action=GET`
+    now read and write the same entries.
+  - All endpoints for a cache name must use compatible key and value types. 
Ehcache validates them
+    exactly, so `ehcache:foo` and `ehcache:foo?valueType=java.lang.String` now 
fail at startup with
+    a type-mismatch error.
+  - The shared caches, and their entries, are closed when the last endpoint 
that uses the shared
+    CacheManager stops (for example when the CamelContext stops).
+
+* **Event delivery on default caches.** A consumer on a cache without an 
explicit configuration now
+  receives events. Previously, the `UserManagedCache` fallback lacked event 
executor services, so
+  listeners were silently never called.
+
+* **Null body rejected.** PUT, PUT_IF_ABSENT and REPLACE without a body and 
without a
+  `CamelEhcacheValue` header now fail with `CamelExchangeException` instead of 
silently storing the
+  `Message` object.
+
+* **User-supplied `cacheManager` caches persist.** Caches created on demand 
inside a user-supplied
+  `cacheManager` (e.g. `ehcache:foo?cacheManager=#myManager`) are no longer 
removed when Camel stops;
+  the lifecycle of the caches follows the lifecycle of the supplied manager.
+
 === camel-a2a - consumer context is separate from producer headers
 
 An A2A consumer no longer populates producer control headers with the incoming 
operation and

Reply via email to