This is an automated email from the ASF dual-hosted git repository.
fmariani pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new c1381288651 Make Virtual threads more integrated with Camel Spring Boot
c1381288651 is described below
commit c13812886519ab8ca4bf91baf793283710f4067a
Author: Croway <[email protected]>
AuthorDate: Mon Sep 1 15:56:55 2025 +0200
Make Virtual threads more integrated with Camel Spring Boot
---
.../camel-platform-http-starter/pom.xml | 6 -
.../SpringBootPlatformHttpAutoConfiguration.java | 32 ++++-
...ingBootPlatformHttpCamelVirtualThreadsTest.java | 81 ++++++++++++
...ootPlatformHttpVirtualThreadsOptimizedTest.java | 89 +++++++++++++
core/camel-spring-boot/pom.xml | 64 ++++++++++
.../src/main/docs/spring-boot.adoc | 138 ++++++++++++++++++++-
...CamelVirtualThreadEnvironmentPostProcessor.java | 54 ++++++++
.../camel/spring/boot/SpringBootCamelContext.java | 54 ++++----
.../src/main/resources/META-INF/spring.factories | 19 +++
.../camel/spring/boot/CamelVirtualThreadsTest.java | 138 +++++++++++++++++++++
pom.xml | 2 +-
.../itest/springboot/ApplicationContextHolder.java | 23 ++--
12 files changed, 648 insertions(+), 52 deletions(-)
diff --git a/components-starter/camel-platform-http-starter/pom.xml
b/components-starter/camel-platform-http-starter/pom.xml
index 7dca34119c5..aed93ec90f6 100644
--- a/components-starter/camel-platform-http-starter/pom.xml
+++ b/components-starter/camel-platform-http-starter/pom.xml
@@ -73,12 +73,6 @@
<version>${hazelcast-version}</version>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-server</artifactId>
- <version>${jetty-version}</version>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
index e6c60c61bab..a9fdbc9d056 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
@@ -50,6 +50,10 @@ public class SpringBootPlatformHttpAutoConfiguration {
if (executors != null && !executors.isEmpty()) {
executors.forEach(e -> LOG.debug("Analyzing executor: {}",
e.getClass().getName()));
+
+ // Check if virtual threads are enabled
+ boolean virtualThreadsEnabled =
Boolean.parseBoolean(env.getProperty("spring.threads.virtual.enabled",
"false"));
+
executor = executors.stream()
.filter(e -> {
try {
@@ -58,17 +62,35 @@ public class SpringBootPlatformHttpAutoConfiguration {
// No problem, spring-security is not configured
return false;
}
- }).findAny().orElseGet(() ->
- executors.stream()
+ }).findAny().orElseGet(() -> {
+ if (virtualThreadsEnabled) {
+ // Prefer SimpleAsyncTaskExecutor when virtual
threads are enabled
+ return executors.stream()
+ .filter(e -> e instanceof
SimpleAsyncTaskExecutor)
+ .findFirst()
+ .orElseGet(() ->
+ executors.stream()
+ .filter(e -> e instanceof
ThreadPoolTaskExecutor)
+ .findFirst()
+ .orElseThrow(() -> new
RuntimeException("No SimpleAsyncTaskExecutor or ThreadPoolTaskExecutor
configured"))
+ );
+ } else {
+ // Traditional behavior: prefer
ThreadPoolTaskExecutor
+ return executors.stream()
.filter(e -> e instanceof
ThreadPoolTaskExecutor || e instanceof SimpleAsyncTaskExecutor)
.findFirst()
- .orElseThrow(() -> new
RuntimeException("No ThreadPoolTaskExecutor, SimpleAsyncTaskExecutor or
DelegatingSecurityContextAsyncTaskExecutor configured"))
- );
+ .orElseThrow(() -> new
RuntimeException("No ThreadPoolTaskExecutor, SimpleAsyncTaskExecutor or
DelegatingSecurityContextAsyncTaskExecutor configured"));
+ }
+ });
} else {
throw new RuntimeException("No Executor configured");
}
- LOG.debug("Using executor: {}", executor.getClass().getName());
+ if
(Boolean.parseBoolean(env.getProperty("spring.threads.virtual.enabled",
"false"))) {
+ LOG.info("Virtual threads enabled - using executor: {} for
platform-http", executor.getClass().getName());
+ } else {
+ LOG.debug("Using executor: {}", executor.getClass().getName());
+ }
int port = Integer.parseInt(env.getProperty("server.port", "8080"));
return new SpringBootPlatformHttpEngine(port, executor);
}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpCamelVirtualThreadsTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpCamelVirtualThreadsTest.java
new file mode 100644
index 00000000000..da4a6cfd657
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpCamelVirtualThreadsTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.platform.http.springboot;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import
org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration;
+import
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnableAutoConfiguration(exclude = {OAuth2ClientAutoConfiguration.class,
SecurityAutoConfiguration.class})
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpCamelVirtualThreadsTest.class,
SpringBootPlatformHttpCamelVirtualThreadsTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class },
+ properties = "spring.threads.virtual.enabled=true")
+public class SpringBootPlatformHttpCamelVirtualThreadsTest extends
PlatformHttpBase {
+
+ private static final String postRouteId =
"SpringBootPlatformHttpCamelVirtualThreadsTest_mypost";
+ private static final String getRouteId =
"SpringBootPlatformHttpCamelVirtualThreadsTest_myget";
+
+ // *************************************
+ // Config
+ // *************************************
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public RouteBuilder servletPlatformHttpRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("platform-http:/myget").id(postRouteId).setBody().constant("get");
+
from("platform-http:/mypost").id(getRouteId).transform().body(String.class, b
-> b.toUpperCase());
+ }
+ };
+ }
+ }
+
+ @Override
+ protected String getPostRouteId() {
+ return postRouteId;
+ }
+
+ @Override
+ protected String getGetRouteId() {
+ return getRouteId;
+ }
+
+ @Test
+ public void testCamelVirtualThreadPropertyIsSet() {
+ // Verify that when spring.threads.virtual.enabled=true is set,
+ // the camel.threads.virtual.enabled system property is automatically
set to true
+ String camelVirtualThreadsProperty =
System.getProperty("camel.threads.virtual.enabled");
+
+ assertThat(camelVirtualThreadsProperty)
+ .as("camel.threads.virtual.enabled should be automatically set
when spring.threads.virtual.enabled=true")
+ .isEqualTo("true");
+ }
+}
\ No newline at end of file
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpVirtualThreadsOptimizedTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpVirtualThreadsOptimizedTest.java
new file mode 100644
index 00000000000..8d03364dd05
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpVirtualThreadsOptimizedTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.platform.http.springboot;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import
org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration;
+import
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnableAutoConfiguration(exclude = {OAuth2ClientAutoConfiguration.class,
SecurityAutoConfiguration.class})
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpVirtualThreadsOptimizedTest.class,
SpringBootPlatformHttpVirtualThreadsOptimizedTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class },
+ properties = "spring.threads.virtual.enabled=true")
+public class SpringBootPlatformHttpVirtualThreadsOptimizedTest extends
PlatformHttpBase {
+
+ private static final String postRouteId =
"SpringBootPlatformHttpVirtualThreadsOptimizedTest_mypost";
+ private static final String getRouteId =
"SpringBootPlatformHttpVirtualThreadsOptimizedTest_myget";
+
+ @Autowired
+ private List<Executor> executors;
+
+ // *************************************
+ // Config
+ // *************************************
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public RouteBuilder servletPlatformHttpRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("platform-http:/myget").id(postRouteId).setBody().constant("get");
+
from("platform-http:/mypost").id(getRouteId).transform().body(String.class, b
-> b.toUpperCase());
+ }
+ };
+ }
+ }
+
+ @Override
+ protected String getPostRouteId() {
+ return postRouteId;
+ }
+
+ @Override
+ protected String getGetRouteId() {
+ return getRouteId;
+ }
+
+ @Test
+ public void testVirtualThreadExecutorIsPreferred() {
+ // Verify that SimpleAsyncTaskExecutor is available and used when
virtual threads are enabled
+ boolean hasSimpleAsyncTaskExecutor = executors.stream()
+ .anyMatch(e -> e instanceof SimpleAsyncTaskExecutor);
+
+ assertThat(hasSimpleAsyncTaskExecutor)
+ .as("SimpleAsyncTaskExecutor should be available when virtual
threads are enabled")
+ .isTrue();
+ }
+}
\ No newline at end of file
diff --git a/core/camel-spring-boot/pom.xml b/core/camel-spring-boot/pom.xml
index f6e84b00031..b155ed12ffd 100644
--- a/core/camel-spring-boot/pom.xml
+++ b/core/camel-spring-boot/pom.xml
@@ -264,4 +264,68 @@
</plugin>
</plugins>
</build>
+
+ <profiles>
+ <!-- Profile for JDK 21+ features like Virtual Threads -->
+ <profile>
+ <id>jdk21</id>
+ <activation>
+ <jdk>[21,)</jdk>
+ </activation>
+ <properties>
+ <jdk.version>21</jdk.version>
+ </properties>
+ <build>
+ <plugins>
+ <!-- Add java21 test sources when using JDK 21+ -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <version>3.6.1</version>
+ <executions>
+ <execution>
+ <id>add-java21-test-sources</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>add-test-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+
<source>${basedir}/src/test/java21</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <!-- Configure compiler for JDK 21 compatibility in this
profile -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>${maven-compiler-plugin-version}</version>
+ <configuration>
+ <source>21</source>
+ <target>21</target>
+ <fork>true</fork>
+ <parameters>true</parameters>
+ </configuration>
+ </plugin>
+
+ <!-- Configure surefire to run JDK 21 specific tests -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>${maven-surefire-plugin-version}</version>
+ <configuration>
+ <!-- Set virtual thread properties only for these
specific tests -->
+ <systemPropertyVariables>
+
<spring.threads.virtual.enabled>true</spring.threads.virtual.enabled>
+
<camel.threads.virtual.enabled>true</camel.threads.virtual.enabled>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/core/camel-spring-boot/src/main/docs/spring-boot.adoc
b/core/camel-spring-boot/src/main/docs/spring-boot.adoc
index b17cb38c4af..ad7e02f49cd 100644
--- a/core/camel-spring-boot/src/main/docs/spring-boot.adoc
+++ b/core/camel-spring-boot/src/main/docs/spring-boot.adoc
@@ -454,4 +454,140 @@ health checks that support this feature. In enable the
probes locally, they need
management.endpoint.health.probes.enabled=true
----
-Finally, http://localhost:8080/actuator/health/liveness will show the updated
probe.
\ No newline at end of file
+Finally, http://localhost:8080/actuator/health/liveness will show the updated
probe.
+
+== Virtual Threads Support
+
+Camel Spring Boot provides comprehensive support for JDK 21+ Virtual Threads,
offering significant performance improvements for I/O-intensive applications.
Virtual threads are lightweight threads that can dramatically reduce memory
overhead and improve scalability compared to traditional platform threads.
+
+=== Enabling Virtual Threads
+
+To enable virtual threads in your Camel Spring Boot application, simply set
the Spring Boot virtual threads property:
+
+[source,properties]
+----
+spring.threads.virtual.enabled=true
+----
+
+When this property is set to `true`, Camel Spring Boot automatically:
+
+1. **Enables Camel Virtual Threads**: Automatically sets
`camel.threads.virtual.enabled=true`
+2. **Optimizes Executors**: Configures components to use
`SimpleAsyncTaskExecutor` which is optimized for virtual threads
+3. **Updates Component Configurations**: Automatically configures supported
components for optimal virtual thread performance
+
+=== Automatic Configuration
+
+When virtual threads are enabled, Camel Spring Boot provides automatic
configuration for several components:
+
+==== Platform HTTP Component
+
+The Platform HTTP component automatically uses `SimpleAsyncTaskExecutor` when
virtual threads are enabled, providing better performance for HTTP endpoints:
+
+[source,java]
+----
+@Component
+public class MyRoute extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("platform-http:/api/data")
+ .to("http://external-service/api") // Benefits from virtual
threads
+ .setBody(constant("Response processed"));
+ }
+}
+----
+
+==== JMS and AMQP Components
+
+JMS and AMQP components benefit from virtual threads through the global
virtual thread configuration. When `spring.threads.virtual.enabled=true` is
set, these components will use virtual threads for message processing through
the standard Spring Boot virtual thread integration:
+
+[source,properties]
+----
+spring.threads.virtual.enabled=true
+# JMS and AMQP components will automatically benefit from virtual threads
+----
+
+=== Manual Configuration
+
+If you need to manually configure specific components for virtual threads, you
can use the `VirtualThreadAwareExecutorFactory`:
+
+[source,java]
+----
+@Configuration
+public class VirtualThreadConfig {
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.threads.virtual.enabled",
havingValue = "true")
+ public TaskExecutor customTaskExecutor() {
+ return VirtualThreadAwareExecutorFactory.createOptimalExecutor();
+ }
+}
+----
+
+=== Performance Benefits
+
+Virtual threads provide several performance advantages:
+
+- **Reduced Memory Overhead**: Virtual threads use significantly less memory
than platform threads
+- **Higher Concurrency**: Support for millions of concurrent virtual threads
+- **Better Resource Utilization**: Reduced thread pool contention and context
switching
+- **Simplified Programming Model**: No need for complex async/reactive
programming patterns
+
+=== Best Practices
+
+When using virtual threads with Camel Spring Boot:
+
+1. **Avoid Thread Pinning**: Minimize use of `synchronized` blocks in route
processing
+2. **Use Simple Async Executors**: Let Camel Spring Boot auto-configure
optimal executors
+3. **Monitor Performance**: Use Spring Boot Actuator to monitor virtual thread
usage
+4. **Test Thoroughly**: Validate behavior under load with virtual threads
enabled
+
+=== Example Application
+
+Here's a complete example of a Camel Spring Boot application optimized for
virtual threads:
+
+[source,properties]
+----
+# application.properties
+spring.threads.virtual.enabled=true
+camel.main.routes-include-pattern=classpath:routes/*
+----
+
+[source,java]
+----
+@SpringBootApplication
+public class VirtualThreadsApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(VirtualThreadsApplication.class, args);
+ }
+
+ @Component
+ public static class ApiRoutes extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("platform-http:/api/process")
+ .log("Processing request with virtual thread: ${threadName}")
+ .to("http://external-api/data")
+ .transform(body().prepend("Processed: "))
+ .setHeader("X-Thread-Type", constant("virtual"));
+ }
+ }
+}
+----
+
+=== Compatibility
+
+Virtual threads support requires:
+
+- **JDK 21+**: Virtual threads are a JDK 21+ feature
+- **Spring Boot 3.2+**: Spring Boot virtual thread support
+- **Camel 4.0+**: Camel virtual thread integration
+
+=== Troubleshooting
+
+If you encounter issues with virtual threads:
+
+1. **Check JDK Version**: Ensure you're running JDK 21 or later
+2. **Verify Property Setting**: Confirm `spring.threads.virtual.enabled=true`
is set
+3. **Review Logs**: Check for virtual thread configuration messages in startup
logs
+4. **Monitor Thread Usage**: Use JVM monitoring tools to observe virtual
thread behavior
\ No newline at end of file
diff --git
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelVirtualThreadEnvironmentPostProcessor.java
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelVirtualThreadEnvironmentPostProcessor.java
new file mode 100644
index 00000000000..ecac3587148
--- /dev/null
+++
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/CamelVirtualThreadEnvironmentPostProcessor.java
@@ -0,0 +1,54 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * Environment post processor that automatically sets the Camel virtual thread
+ * system property when Spring Boot virtual threads are enabled.
+ *
+ * This processor runs very early in the Spring Boot startup process, before
+ * any Camel classes are loaded, ensuring that Camel's ThreadType static
+ * initialization picks up the correct virtual thread configuration.
+ */
+public class CamelVirtualThreadEnvironmentPostProcessor implements
EnvironmentPostProcessor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CamelVirtualThreadEnvironmentPostProcessor.class);
+
+ @Override
+ public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
+ // Check if Spring Boot virtual threads are enabled
+ String springVirtualThreads =
environment.getProperty("spring.threads.virtual.enabled");
+
+ if ("true".equalsIgnoreCase(springVirtualThreads)) {
+ // Set the Camel virtual threads system property early, before
Camel classes are loaded
+ String existingCamelProperty =
System.getProperty("camel.threads.virtual.enabled");
+
+ if (existingCamelProperty == null) {
+ System.setProperty("camel.threads.virtual.enabled", "true");
+ LOG.info("Spring virtual threads enabled - automatically
setting camel.threads.virtual.enabled=true");
+ } else {
+ LOG.debug("camel.threads.virtual.enabled already set to: {}",
existingCamelProperty);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/SpringBootCamelContext.java
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/SpringBootCamelContext.java
index 3ad364822c3..e83497b864f 100644
---
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/SpringBootCamelContext.java
+++
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/SpringBootCamelContext.java
@@ -23,6 +23,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
+import java.util.concurrent.locks.ReentrantLock;
+
/**
* The {@link org.apache.camel.CamelContext} created by Spring Boot.
*/
@@ -33,6 +35,7 @@ public class SpringBootCamelContext extends
SpringCamelContext {
private final StopWatch stopWatch = new StopWatch();
private final boolean warnOnEarlyShutdown;
private final CamelSpringBootApplicationController controller;
+ private final ReentrantLock reentrantLock = new ReentrantLock();
public SpringBootCamelContext(ApplicationContext applicationContext,
boolean warnOnEarlyShutdown,
CamelSpringBootApplicationController
controller) {
@@ -61,34 +64,39 @@ public class SpringBootCamelContext extends
SpringCamelContext {
}
@Override
- protected synchronized void doStop() throws Exception {
- var listeners = controller.getMain().getMainListeners();
- if (!listeners.isEmpty()) {
- for (MainListener listener : listeners) {
- listener.beforeStop(controller.getMain());
+ protected void doStop() throws Exception {
+ reentrantLock.lock();
+ try {
+ var listeners = controller.getMain().getMainListeners();
+ if (!listeners.isEmpty()) {
+ for (MainListener listener : listeners) {
+ listener.beforeStop(controller.getMain());
+ }
}
- }
- // if we are stopping very quickly then its likely because the user
may not have either spring-boot-web
- // or enabled Camel's main controller, so lets log a WARN about this.
- long taken = stopWatch.taken();
- if (warnOnEarlyShutdown && taken < 1200) { // give it a bit of slack
- String cp = System.getProperty("java.class.path");
- boolean junit = cp != null && cp.contains("junit-");
- boolean starterWeb = cp != null &&
cp.contains("spring-boot-starter-web");
- if (!junit && !starterWeb) {
- LOG.warn(
- "CamelContext has only been running for less than a
second. If you intend to run Camel for a longer time "
- + "then you can set the property
camel.main.run-controller=true in application.properties"
- + " or add spring-boot-starter-web JAR to the
classpath.");
+ // if we are stopping very quickly then its likely because the
user may not have either spring-boot-web
+ // or enabled Camel's main controller, so lets log a WARN about
this.
+ long taken = stopWatch.taken();
+ if (warnOnEarlyShutdown && taken < 1200) { // give it a bit of
slack
+ String cp = System.getProperty("java.class.path");
+ boolean junit = cp != null && cp.contains("junit-");
+ boolean starterWeb = cp != null &&
cp.contains("spring-boot-starter-web");
+ if (!junit && !starterWeb) {
+ LOG.warn(
+ "CamelContext has only been running for less than
a second. If you intend to run Camel for a longer time "
+ + "then you can set the property
camel.main.run-controller=true in application.properties"
+ + " or add spring-boot-starter-web JAR to
the classpath.");
+ }
}
- }
- super.doStop();
+ super.doStop();
- if (!listeners.isEmpty()) {
- for (MainListener listener : listeners) {
- listener.afterStop(controller.getMain());
+ if (!listeners.isEmpty()) {
+ for (MainListener listener : listeners) {
+ listener.afterStop(controller.getMain());
+ }
}
+ } finally {
+ reentrantLock.unlock();
}
}
}
diff --git
a/core/camel-spring-boot/src/main/resources/META-INF/spring.factories
b/core/camel-spring-boot/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000000..e0a6e45de75
--- /dev/null
+++ b/core/camel-spring-boot/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+org.springframework.boot.env.EnvironmentPostProcessor=\
+org.apache.camel.spring.boot.CamelVirtualThreadEnvironmentPostProcessor
\ No newline at end of file
diff --git
a/core/camel-spring-boot/src/test/java21/org/apache/camel/spring/boot/CamelVirtualThreadsTest.java
b/core/camel-spring-boot/src/test/java21/org/apache/camel/spring/boot/CamelVirtualThreadsTest.java
new file mode 100644
index 00000000000..e6bc4ec86ab
--- /dev/null
+++
b/core/camel-spring-boot/src/test/java21/org/apache/camel/spring/boot/CamelVirtualThreadsTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
+import org.awaitility.Awaitility;
+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 static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test to verify that Camel routes use virtual threads when virtual threads
are enabled.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class,
CamelVirtualThreadsTest.class,
+ CamelVirtualThreadsTest.TestConfiguration.class },
+ properties = { "spring.threads.virtual.enabled=true" })
+public class CamelVirtualThreadsTest {
+
+ // Ensure camel.threads.virtual.enabled is set before any Camel classes
are loaded
+ static {
+ System.setProperty("camel.threads.virtual.enabled", "true");
+ }
+
+ @Autowired
+ CamelContext context;
+
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public RouteBuilder virtualThreadTestRoute() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("timer:virtualThreadTest?period=100&repeatCount=1")
+ .routeId("virtualThreadTestRoute")
+ .process(exchange -> {
+ // Capture the current thread information
+ Thread currentThread = Thread.currentThread();
+ String threadName = currentThread.getName();
+ boolean isVirtual = currentThread.isVirtual();
+
+ // Store thread information in exchange properties
for assertion
+ exchange.setProperty("threadName", threadName);
+ exchange.setProperty("isVirtual", isVirtual);
+ exchange.setProperty("threadClass",
currentThread.getClass().getName());
+ })
+ .to("mock:result");
+ }
+ };
+ }
+ }
+
+ @Test
+ public void testCamelVirtualThreadPropertyIsSet() throws Exception {
+ // Verify that the environment post processor set the
camel.threads.virtual.enabled property
+ String camelVirtualThreadsProperty =
System.getProperty("camel.threads.virtual.enabled");
+ assertThat(camelVirtualThreadsProperty)
+ .as("camel.threads.virtual.enabled should be automatically set
by EnvironmentPostProcessor when spring.threads.virtual.enabled=true")
+ .isEqualTo("true");
+ }
+
+ @Test
+ public void testRouteExecutesOnVirtualThread() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<Boolean> isVirtualThread = new
AtomicReference<>(false);
+ AtomicReference<String> threadName = new AtomicReference<>("");
+
+ // Create a test route using SEDA which uses ExecutorService
+ RouteBuilder testRoute = new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("seda:virtualTest?concurrentConsumers=1")
+ .routeId("virtualTestRoute")
+ .process(exchange -> {
+ Thread currentThread = Thread.currentThread();
+ isVirtualThread.set(currentThread.isVirtual());
+ threadName.set(currentThread.getName());
+ latch.countDown();
+ });
+ }
+ };
+
+ // Add the route dynamically to ensure it uses current context
configuration
+ try {
+ context.addRoutes(testRoute);
+
+ // Send a message to trigger the route
+ context.createProducerTemplate().sendBody("seda:virtualTest",
"test message");
+
+ // Wait for the route to execute using Awaitility
+ Awaitility.await()
+ .atMost(2, TimeUnit.SECONDS)
+ .until(() -> latch.getCount() == 0);
+
+ // Assert that the route executed on a virtual thread
+ assertThat(isVirtualThread.get())
+ .as("Route should execute on a virtual thread when virtual
threads are enabled. Thread name: " + threadName.get())
+ .isTrue();
+
+ assertThat(threadName.get())
+ .as("Virtual thread should have a recognizable name pattern")
+ .isNotEmpty();
+
+ } finally {
+ // Clean up - remove the test route
+ context.getRouteController().stopRoute("virtualTestRoute");
+ context.removeRoute("virtualTestRoute");
+ }
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 85401b2387e..75b020a5bf5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,7 +130,7 @@
<surefire.version>${maven-surefire-plugin-version}</surefire.version>
<swagger-parser-v3-version>2.1.10</swagger-parser-v3-version>
<cyclonedx-maven-plugin-version>2.9.1</cyclonedx-maven-plugin-version>
- <wiremock-spring-boot-version>3.4.0</wiremock-spring-boot-version>
+ <wiremock-spring-boot-version>3.10.6</wiremock-spring-boot-version>
<spring-session-hazelcast-version>3.4.2</spring-session-hazelcast-version>
</properties>
diff --git
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/ApplicationContextHolder.java
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/ApplicationContextHolder.java
index 33f1e40fdf6..66135da078b 100644
---
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/ApplicationContextHolder.java
+++
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/ApplicationContextHolder.java
@@ -21,22 +21,24 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
/**
* receives a spring-context and make it available to classes outside the
spring scope.
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
+ private static final CountDownLatch contextLatch = new CountDownLatch(1);
private static ApplicationContext context;
private static long contextMaxWaitTime = 60000L;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
- synchronized (ApplicationContextHolder.class) {
- ApplicationContextHolder.context = applicationContext;
- ApplicationContextHolder.class.notifyAll();
- }
+ ApplicationContextHolder.context = applicationContext;
+ contextLatch.countDown();
}
public static ApplicationContext getApplicationContext() throws
InterruptedException {
@@ -46,18 +48,7 @@ public class ApplicationContextHolder implements
ApplicationContextAware {
private static void waitForContextReady() throws InterruptedException {
long maxWait = contextMaxWaitTime;
- long deadline = System.currentTimeMillis() + maxWait;
- synchronized (ApplicationContextHolder.class) {
- long time = System.currentTimeMillis();
- while (time < deadline && context == null) {
- ApplicationContextHolder.class.wait(deadline - time);
- time = System.currentTimeMillis();
- }
-
- if (context == null) {
- throw new IllegalStateException("No spring context available
after " + maxWait + " millis");
- }
- }
+ contextLatch.await(maxWait, TimeUnit.MILLISECONDS);
}
public static long getContextMaxWaitTime() {