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 392d620d9a8f CAMEL-23835: camel-launcher - set classloader on embedded
plugins so TUI ServiceLoader works in fat-jar
392d620d9a8f is described below
commit 392d620d9a8fccda2dc48aec958336cc0f2406f0
Author: Adriano Machado <[email protected]>
AuthorDate: Fri Jun 26 01:50:52 2026 -0400
CAMEL-23835: camel-launcher - set classloader on embedded plugins so TUI
ServiceLoader works in fat-jar
The launcher registers embedded plugins in postAddCommands without setting a
classloader. The TUI plugin installs it as the TCCL for tamboui's
ServiceLoader
backend discovery. With null classloader, ServiceLoader falls back to the
system
classloader which cannot see nested BOOT-INF/lib jars in the Spring Boot
fat-jar,
causing "No BackendProvider found on classpath".
Fix: postAddCommands now sets the fat-jar classloader on each embedded
plugin
before calling customize(). Added regression test.
Closes #24258
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
.../dsl/jbang/launcher/CamelLauncherMain.java | 26 ++++++++++++----
.../dsl/jbang/launcher/CamelLauncherTest.java | 36 ++++++++++++++++++++++
2 files changed, 56 insertions(+), 6 deletions(-)
diff --git
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
index 7c36cddb6b17..4df426be4e31 100644
---
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
+++
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
@@ -16,12 +16,15 @@
*/
package org.apache.camel.dsl.jbang.launcher;
+import java.util.List;
+
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.generate.GeneratePlugin;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.KubernetesPlugin;
import org.apache.camel.dsl.jbang.core.commands.test.TestPlugin;
import org.apache.camel.dsl.jbang.core.commands.tui.TuiPlugin;
import org.apache.camel.dsl.jbang.core.commands.validate.ValidatePlugin;
+import org.apache.camel.dsl.jbang.core.common.Plugin;
import picocli.CommandLine;
/**
@@ -31,12 +34,23 @@ public class CamelLauncherMain extends CamelJBangMain {
@Override
public void postAddCommands(CommandLine commandLine, String[] args) {
- // install embedded plugins
- new GeneratePlugin().customize(commandLine, this);
- new KubernetesPlugin().customize(commandLine, this);
- new TuiPlugin().customize(commandLine, this);
- new ValidatePlugin().customize(commandLine, this);
- new TestPlugin().customize(commandLine, this);
+ // Install embedded plugins. Each plugin must be given the classloader
that loaded it (this
+ // class's classloader, i.e. the fat-jar classloader) so that plugins
relying on ServiceLoader
+ // discovery can see the dependencies bundled in the launcher. The TUI
plugin installs this
+ // classloader as the thread-context classloader so tamboui can
discover its terminal backend;
+ // without it the discovery falls back to the system classloader,
which cannot see the nested
+ // BOOT-INF/lib jars of the Spring Boot fat-jar, and the TUI fails
with "No BackendProvider found".
+ ClassLoader classLoader = getClass().getClassLoader();
+ List<Plugin> plugins = List.of(
+ new GeneratePlugin(),
+ new KubernetesPlugin(),
+ new TuiPlugin(),
+ new ValidatePlugin(),
+ new TestPlugin());
+ for (Plugin plugin : plugins) {
+ plugin.setClassLoader(classLoader);
+ plugin.customize(commandLine, this);
+ }
}
}
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
index 8c5c52fe87b8..d09f76deb102 100644
---
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
@@ -18,12 +18,16 @@ package org.apache.camel.dsl.jbang.launcher;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
+import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.camel.dsl.jbang.core.common.Printer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -87,6 +91,38 @@ public class CamelLauncherTest {
assertTrue(output.contains("Camel CLI version:"), "Output should
contain version information");
}
+ @Test
+ public void testEmbeddedTuiPluginReceivesClassLoader() throws Exception {
+ // Regression for the launcher TUI failure "No BackendProvider found
on classpath".
+ // The launcher installs embedded plugins directly in postAddCommands.
Each plugin must be
+ // given the fat-jar classloader so that the TUI can install it as the
thread-context
+ // classloader for tamboui's ServiceLoader-based terminal backend
discovery. Without it the
+ // discovery falls back to the system classloader, which cannot see
the nested BOOT-INF/lib
+ // jars of the Spring Boot fat-jar.
+ CamelLauncherMain main = new CamelLauncherMain();
+
+ CommandLine commandLine = new CommandLine(main);
+ main.postAddCommands(commandLine, new String[] { "tui" });
+
+ CommandLine tui = commandLine.getSubcommands().get("tui");
+ assertNotNull(tui, "tui command should be registered by the launcher");
+ CommandLine monitor = tui.getSubcommands().get("monitor");
+ assertNotNull(monitor, "tui monitor subcommand should be registered");
+
+ ClassLoader pluginClassLoader =
readClassLoaderField(monitor.getCommand());
+ assertNotNull(pluginClassLoader,
+ "embedded TUI plugin must receive a non-null classloader (else
tamboui ServiceLoader breaks in the fat-jar)");
+ // The classloader handed to the plugin must be able to resolve
tamboui's backend SPI.
+ assertDoesNotThrow(() ->
pluginClassLoader.loadClass("dev.tamboui.terminal.BackendProvider"),
+ "the plugin classloader must be able to load tamboui's
BackendProvider");
+ }
+
+ private static ClassLoader readClassLoaderField(Object command) throws
Exception {
+ Field field = command.getClass().getDeclaredField("classLoader");
+ field.setAccessible(true);
+ return (ClassLoader) field.get(command);
+ }
+
@Test
public void testLauncherValidatePlugin() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();