This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
commit d36daaec92cc3c75e1eb25d06bb5e038bdfec0d7 Author: croway <[email protected]> AuthorDate: Fri Jul 10 18:13:39 2026 +0200 CAMEL-24000: camel-spring-boot - notify MainListener beans exactly once with run-controller enabled With camel.main.run-controller=true, MainListener beans were notified twice for beforeStart/afterStart (and beforeStop/afterStop): once via the controller created by CamelAutoConfiguration (the complete callback stream added in CAMEL-21359) and once more via the run controller's internal Main, which registered the same listener beans and fires the start/stop callbacks from MainSupport.run(). The run controller's Main is only used to keep the JVM running, so it no longer registers the MainListener beans. Co-Authored-By: Claude Fable 5 <[email protected]> --- .../camel/spring/boot/CamelMainRunController.java | 14 +- .../boot/CamelSpringBootApplicationController.java | 21 ++- .../boot/CamelMainListenerRunControllerTest.java | 155 +++++++++++++++++++++ 3 files changed, 176 insertions(+), 14 deletions(-) diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelMainRunController.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelMainRunController.java index e146adfd2d0..4a4e7a63ef8 100644 --- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelMainRunController.java +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelMainRunController.java @@ -17,12 +17,9 @@ package org.apache.camel.spring.boot; import org.apache.camel.CamelContext; -import org.apache.camel.main.MainListener; import org.apache.camel.main.MainShutdownStrategy; import org.springframework.context.ApplicationContext; -import java.util.Map; - /** * Controller to keep the main running and perform graceful shutdown when the JVM is stopped. */ @@ -32,15 +29,12 @@ public class CamelMainRunController { private final Thread daemon; public CamelMainRunController(ApplicationContext applicationContext, CamelContext camelContext) { - controller = new CamelSpringBootApplicationController(applicationContext); + // do not register the MainListener beans on this controller: they are already notified + // via the controller created by CamelAutoConfiguration, and this Main is only used + // to keep the JVM running + controller = new CamelSpringBootApplicationController(applicationContext, false); controller.setCamelContext(camelContext); - // setup main listeners eager on controller - final Map<String, MainListener> listeners = applicationContext.getBeansOfType(MainListener.class); - for (MainListener listener : listeners.values()) { - controller.getMain().addMainListener(listener); - } - daemon = new Thread(new DaemonTask(), "CamelMainRunController"); } diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelSpringBootApplicationController.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelSpringBootApplicationController.java index 859f332fb61..b157f48164e 100644 --- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelSpringBootApplicationController.java +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelSpringBootApplicationController.java @@ -37,11 +37,24 @@ public class CamelSpringBootApplicationController implements CamelContextAware { private CamelContext camelContext; public CamelSpringBootApplicationController(final ApplicationContext applicationContext) { + this(applicationContext, true); + } + + /** + * @param registerMainListeners whether to register the {@link MainListener} beans from the application context on + * the internal {@link Main}. The controller used by the main run controller must not + * register them, as the listeners are already notified via the controller created by + * {@link CamelAutoConfiguration}, and registering them on both would notify each + * listener twice. + */ + CamelSpringBootApplicationController(final ApplicationContext applicationContext, boolean registerMainListeners) { this.main = new CamelSpringMain(applicationContext, this); - // inject main listeners - final Map<String, MainListener> listeners = applicationContext.getBeansOfType(MainListener.class); - for (MainListener listener : listeners.values()) { - this.main.addMainListener(listener); + if (registerMainListeners) { + // inject main listeners + final Map<String, MainListener> listeners = applicationContext.getBeansOfType(MainListener.class); + for (MainListener listener : listeners.values()) { + this.main.addMainListener(listener); + } } } diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/CamelMainListenerRunControllerTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/CamelMainListenerRunControllerTest.java new file mode 100644 index 00000000000..c7d377e5783 --- /dev/null +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/CamelMainListenerRunControllerTest.java @@ -0,0 +1,155 @@ +/* + * 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.spring.boot; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.main.BaseMainSupport; +import org.apache.camel.main.MainListener; +import org.apache.camel.main.MainListenerSupport; +import org.apache.camel.test.spring.junit6.CamelSpringBootTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that a {@link MainListener} bean receives each lifecycle callback exactly once when the main run + * controller is enabled (camel.main.run-controller=true). + */ +@DirtiesContext +@CamelSpringBootTest +@EnableAutoConfiguration +@SpringBootTest(properties = "camel.main.run-controller=true") +public class CamelMainListenerRunControllerTest { + + @Configuration + static class Config { + + @Bean + RouteBuilder routeBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start").to("mock:result"); + } + }; + } + + @Bean + CountingMainListener countingMainListener() { + return new CountingMainListener(); + } + + } + + @Autowired + CamelContext camelContext; + + @Autowired + CountingMainListener listener; + + @Test + public void testMainListenerCallbacksFireExactlyOnce() { + // the run controller notifies listeners from a background daemon thread, + // so wait until the start phase has completed + await().atMost(20, TimeUnit.SECONDS) + .untilAsserted(() -> assertTrue(listener.count("afterStart") >= 1, + "afterStart should have been invoked")); + + // the counts must remain exactly 1 (hold for a while to catch late duplicate notifications + // from the run controller background thread) + await().during(2, TimeUnit.SECONDS).atMost(10, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, listener.count("beforeInitialize"), () -> "beforeInitialize: " + listener.counts); + assertEquals(1, listener.count("beforeConfigure"), () -> "beforeConfigure: " + listener.counts); + assertEquals(1, listener.count("afterConfigure"), () -> "afterConfigure: " + listener.counts); + assertEquals(1, listener.count("beforeStart"), () -> "beforeStart: " + listener.counts); + assertEquals(1, listener.count("afterStart"), () -> "afterStart: " + listener.counts); + }); + + camelContext.stop(); + + await().during(2, TimeUnit.SECONDS).atMost(10, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, listener.count("beforeStop"), () -> "beforeStop: " + listener.counts); + assertEquals(1, listener.count("afterStop"), () -> "afterStop: " + listener.counts); + }); + } + + public static class CountingMainListener extends MainListenerSupport { + + private final Map<String, AtomicInteger> counts = new ConcurrentHashMap<>(); + + int count(String callback) { + AtomicInteger counter = counts.get(callback); + return counter != null ? counter.get() : 0; + } + + private void record(String callback) { + counts.computeIfAbsent(callback, k -> new AtomicInteger()).incrementAndGet(); + } + + @Override + public void beforeInitialize(BaseMainSupport main) { + record("beforeInitialize"); + } + + @Override + public void beforeConfigure(BaseMainSupport main) { + record("beforeConfigure"); + } + + @Override + public void afterConfigure(BaseMainSupport main) { + record("afterConfigure"); + } + + @Override + public void beforeStart(BaseMainSupport main) { + record("beforeStart"); + } + + @Override + public void afterStart(BaseMainSupport main) { + record("afterStart"); + } + + @Override + public void beforeStop(BaseMainSupport main) { + record("beforeStop"); + } + + @Override + public void afterStop(BaseMainSupport main) { + record("afterStop"); + } + } + +}
