Skip to content

CAMEL-23835: camel-launcher - set classloader on embedded plugins so TUI ServiceLoader works in fat-jar#24258

Merged
davsclaus merged 1 commit into
apache:mainfrom
ammachado:CAMEL-23835
Jun 26, 2026
Merged

CAMEL-23835: camel-launcher - set classloader on embedded plugins so TUI ServiceLoader works in fat-jar#24258
davsclaus merged 1 commit into
apache:mainfrom
ammachado:CAMEL-23835

Conversation

@ammachado

@ammachado ammachado commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Running the TUI from the camel-launcher fat-jar (java -jar camel-launcher-*.jar tui, ./camel.sh tui, or podman run camel-launcher tui) fails immediately with:

dev.tamboui.terminal.BackendException: No BackendProvider found on classpath.
Add a backend dependency such as tamboui-jline3-backend or tamboui-panama-backend.
	at dev.tamboui.terminal.BackendFactory.tryProviders(BackendFactory.java:139)
	at dev.tamboui.terminal.BackendFactory.create(BackendFactory.java:91)
	at dev.tamboui.tui.TuiRunner.create(TuiRunner.java:186)
	at org.apache.camel.dsl.jbang.core.commands.tui.TuiBackendHelper.createTuiRunner(TuiBackendHelper.java:36)
	...

The same command works under JBang. This is not a packaging problem: tamboui-jline3-backend and its META-INF/services/dev.tamboui.terminal.BackendProvider SPI entry are correctly bundled under BOOT-INF/lib/.

JIRA: https://issues.apache.org/jira/browse/CAMEL-23835

Root cause

tamboui discovers its terminal backend via ServiceLoader.load(BackendProvider.class), which keys off the thread-context classloader (TCCL). The TUI deliberately installs its plugin classloader as the TCCL before starting (CamelMonitor.java:266-267):

// to make ServiceLoader work with tamboui for downloaded JARs
Thread.currentThread().setContextClassLoader(classLoader);

The launcher registers its bundled plugins in CamelLauncherMain.postAddCommands by instantiating them directly and calling customize(...), but never calling setClassLoader:

new TuiPlugin().customize(commandLine, this);   // classLoader stays null

So TuiPlugin.classLoader (and the CamelMonitor it creates) is null, the TCCL is set to null, and ServiceLoader falls back to the system class loader (AppClassLoader). In a Spring Boot fat-jar that loader cannot see the nested BOOT-INF/lib/*.jar (only LaunchedClassLoader can), so the backend is never found. tamboui's SafeServiceLoader.load(Class) then swallows the resulting ServiceConfigurationError/LinkageError (it passes a null error handler), producing the misleading "No BackendProvider found".

Evidence gathered inside the actual fat-jar:

launcher CL: LaunchedClassLoader
[TCCL=null]     SPI class not visible: ClassNotFoundException: dev.tamboui.terminal.BackendProvider  -> providers=0
[TCCL=launched] providers=1

The embedded-plugin scanning path (PluginHelper.addEmbeddedPlugins) is not involved here: it finds the service resources but their nested URLs (jar:nested:.../fat.jar/!BOOT-INF/lib/...) make new JarFile(...) throw, so it silently registers nothing. The launcher relies entirely on postAddCommands. JBang works because there the TUI plugin is a downloaded plugin loaded through a path that already calls setClassLoader(...).

Fix

CamelLauncherMain.postAddCommands now sets the fat-jar classloader (this class's classloader, i.e. LaunchedClassLoader) on each embedded plugin before customizing it:

ClassLoader classLoader = getClass().getClassLoader();
for (Plugin plugin : List.of(new GeneratePlugin(), new KubernetesPlugin(),
        new TuiPlugin(), new ValidatePlugin(), new TestPlugin())) {
    plugin.setClassLoader(classLoader);
    plugin.customize(commandLine, this);
}

Plugin.setClassLoader is a default no-op, so this is harmless for the other bundled plugins.

Verification

  • End-to-end against the rebuilt fat-jar: java -jar camel-launcher-*.jar tui no longer throws BackendException (zero occurrences of "No BackendProvider found"); JLine's terminal provider initializes and the TUI enters raw mode and renders.
  • Regression test: CamelLauncherTest#testEmbeddedTuiPluginReceivesClassLoader drives postAddCommands and asserts the registered TUI monitor command received a non-null classloader that can load dev.tamboui.terminal.BackendProvider. Verified it fails without the fix (expected: not <null>) and passes with it. Full CamelLauncherTest green.

Notes

  • The misleading error message originates upstream in tamboui (SafeServiceLoader.load(Class) discards the real LinkageError/ServiceConfigurationError). Worth reporting to tamboui separately; out of scope here.
  • No upgrade-guide entry: pure bug fix with nothing to migrate.

Claude Code on behalf of Adriano Machado

🤖 Generated with Claude Code

@ammachado ammachado requested review from davsclaus and gnodet June 26, 2026 02:34
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions github-actions Bot added the dsl label Jun 26, 2026
@ammachado ammachado marked this pull request as ready for review June 26, 2026 02:38
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • dsl/camel-jbang/camel-launcher

💡 Manual integration tests recommended:

You modified dsl/camel-jbang/camel-launcher. The related integration tests in dsl/camel-jbang/camel-jbang-it are excluded from CI. Consider running them manually:

mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
All tested modules (2 modules)
  • Camel :: Launcher
  • Camel :: Launcher :: Container

⚙️ View full build and test results

…TUI ServiceLoader works in fat-jar

The launcher registers its bundled plugins directly in
CamelLauncherMain.postAddCommands by instantiating them and calling customize,
without ever setting a classloader on them. The TUI plugin relies on that
classloader: CamelMonitor installs it as the thread-context classloader so
tamboui can discover its terminal backend via ServiceLoader. With a null
classloader the TCCL becomes null and ServiceLoader falls back to the system
classloader, which cannot see the nested BOOT-INF/lib jars of the Spring Boot
fat-jar. As a result `camel tui` from the camel-launcher failed with
"No BackendProvider found on classpath".

Fix: postAddCommands now sets the fat-jar classloader (this class's classloader)
on each embedded plugin before customizing it. Verified end-to-end against the
rebuilt fat-jar: the TUI now starts instead of failing. Added a regression test
that fails without the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean fix for the TUI "No BackendProvider found" failure in the fat-jar. Setting the classloader on all embedded plugins in postAddCommands is the right approach, and the regression test verifies the wiring end-to-end.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

@davsclaus davsclaus merged commit 392d620 into apache:main Jun 26, 2026
5 checks passed
@ammachado ammachado deleted the CAMEL-23835 branch June 27, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants