Skip to content

Annotated commands are not registered in GraalVM native binary #1229

Description

@fmbenhassine

After fixing native runtime hints in 891a148, the native compilation now passes but the user's defined annotated commands are not discovered correctly:

package org.springframework.shell.samples.helloworld.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.core.command.annotation.Command;
import org.springframework.shell.core.command.annotation.Option;

@SpringBootApplication
public class SpringShellApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringShellApplication.class, args);
	}

	@Command(name = "hello", description = "Say hello to a given name", group = "Greetings",
			help = "A command that greets the user with 'Hello ${name}!'. Usage: hello [-n | --name]=<name>")
	public void sayHello(@Option(shortName = 'n', longName = "name", description = "the name of the person to greet",
			defaultValue = "World") String name) {
		System.out.println("Hello " + name + "!");
	}

}

Here is a minimal complete app to use with GraalVM 25+ and the snapshots of Spring Shell v4 at 891a148 or later:

spring-shell-sample-spring-boot.zip

The following instructions can be found in the README of the attached minimal project:

$>sdk install java 25.0.1-graalce
$>sdk use java 25.0.1-graalce
$>java -version
openjdk version "25.0.1" 2025-10-21
OpenJDK Runtime Environment GraalVM CE 25.0.1+8.1 (build 25.0.1+8-jvmci-b01)
OpenJDK 64-Bit Server VM GraalVM CE 25.0.1+8.1 (build 25.0.1+8-jvmci-b01, mixed mode, sharing)
$>./mvnw clean install
$>./mvnw -Pnative native:compile

Once the native build is complete, run the native executable located in the target directory and see how the hello command does not show up:

$>./target/spring-shell-sample-spring-boot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.0.1)

[...]

06:35:21.547 [main] INFO  o.s.s.s.h.b.SpringShellApplication - Started SpringShellApplication in 0.273 seconds (process running for 0.298)
shell:>help
06:35:26.760 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 9.0.1.Final
Available commands:
Built-In Commands
	clear: Clear the terminal screen
	help: Display help about available commands
	version: Show version info
	script: Execute commands from a script file

shell:>

Note that this issue happens only with annotated commands, commands created programmatically with Command.Builder and defined as beans are correctly registered:

package org.springframework.shell.samples.helloworld.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.core.command.Command;

@SpringBootApplication
public class SpringShellApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringShellApplication.class, args);
	}

	@Bean
	public Command sayHello() {
		return Command.builder()
				.name("hello")
				.group("Greeting Commands")
				.description("Say Hello")
				.execute(context -> { System.out.println("Hello, World!");});
	}

}

With this sample, the hello command is shown and runs as expected:

$>./target/spring-shell-sample-spring-boot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.0.1)

16:09:54.040 [main] INFO  o.s.s.s.h.b.SpringShellApplication - Started SpringShellApplication in 0.277 seconds (process running for 0.304)
shell:>help
16:09:59.747 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 9.0.1.Final
Available commands:
Built-In Commands
	script: Execute commands from a script file
	version: Show version info
	clear: Clear the terminal screen
	help: Display help about available commands
Greeting Commands
	hello: Say Hello

shell:>hello
Hello, World!
shell:>exit
Exiting the shell
$>

Activity

  1. added this to the 4.0.0 milestone on Dec 23, 2025
  2. changed the title [-]Broken command discovery in GraalVM native binary[/-] [+]Annotated commands are not registered in GraalVM native binary[/+] on Dec 28, 2025
  3. snicoll commented on Dec 29, 2025

    @snicoll
    Member

    This cannot work as the code currently does classpath scanning at runtime which native image can't do.

    Irrespective of the native issue problem, I think it is a little surprising that code does classpath scanning this way. An easy fix would be to look for the presence of the annotation on beans. Doing so should probably work as @Reflective will add reflection for the method to be found.

    I've also attached a revised version of the sample that cleans up the build: spring-shell-sample-spring-boot.zip

  4. modified the milestones: 4.0.0, 4.0.1 on Dec 30, 2025
  5. modified the milestones: 4.0.1, 4.0.2 on Jan 30, 2026
  6. joshlong commented on Mar 7, 2026

    @joshlong
    Member

    Hi - I have a working proof-of-concept that makes GraalVM native images work when using annotated @Commands here. It would be some small changes to the implementation of CommandRegistry and some new beans in the autoconfiguration to make it work.

  7. fmbenhassine commented on Mar 30, 2026

    @fmbenhassine
    ContributorAuthor

    @snicoll Thank you for your feedback.

    This cannot work as the code currently does classpath scanning at runtime which native image can't do.

    That was copied from v3, but I am not sure it ever worked (I didn't get a chance to test that with v3 back then).

    Irrespective of the native issue problem, I think it is a little surprising that code does classpath scanning this way. An easy fix would be to look for the presence of the annotation on beans. Doing so should probably work as @Reflective will add reflection for the method to be found.

    That works. Thank you for the hint.

    I will include the sample app as a test when introducing the native build (#1182).

  8. snicoll commented on Mar 30, 2026

    @snicoll
    Member

    @fmbenhassine I am little bit confused by the fix. Why are you looking for @Component types? Your commit message states " beans from the application context", but that's not what the code seems to do. Sorry if I missed the context as I read the code from GitHub's UI.

    If you want a reference of a component that loops over the beans of the context to check for a method annotated with a given annotation, EventListenerMethodProcessor is a good candidate.

  9. added a commit that references this issue on Mar 30, 2026
    236dbc9
  10. fmbenhassine commented on Mar 31, 2026

    @fmbenhassine
    ContributorAuthor

    The reason is that commands are expected to be defined in a class that is declared as a Spring @Component (or any stereotype component, we have a note about it here). Thank you for the pointer, I will take a look.

  11. snicoll commented on Mar 31, 2026

    @snicoll
    Member

    The reason is that commands are expected to be defined in a class that is declared as a Spring @component (or any stereotype component)

    Why?

    I am asking as I don't remember having seen such a pattern before. Usually, we apply a feature on a bean irrespective of how it has been contributing to the context. I've read the reference documentation about this and, as far as I understand, these are the ways you can contribute a command:

    • A class that is annotated with @Component with a @Command-annotated method.
    • A class that is referenced in @EnabledCommand (with the same pattern)
    • A class that implements Command that's registered as a bean.

    I can see a number of issues with the current approach:

    • You could annotated a class with @Component in a class that is not a target for classpath scanning and it wouldn't be taken into account. Reading the doc, that's not obvious, especially for beginners ("you define your methods in a @component class and the methods with specific annotations").
    • You cannot expose such a bean yourself, such as:
    @Bean
    Example1 exampleCommand() {
        return new Example1();
    }
    • However, you can do so with a Command implementation, and therefore the following would work:
    @Component
    public class MyCommand implements Command { ... }

    That looks quite inconsistency to me. Besides, I can only assume that the processor for @Command is creating a Command implementation that calls the method reflectively. This is a pattern used in many places and considering all beans in the context would be probably simpler in terms of implementation. It'd probably make EnableCommand less attractive.

  12. fmbenhassine commented on Apr 1, 2026

    @fmbenhassine
    ContributorAuthor

    That's how the annotation programming model of Spring Shell was designed since the beginning. There were a couple specific annotations to define commands (@ShellComponent / @ShellMethod), but they were deprecated in v3 and removed in v4, but the idea/pattern is the same in recent versions.

    I believe the idea of the pattern is to be able to share state between commands grouped in the same class (the typical example is a boolean flag at the class level that is flipped by two different commands like login/logout for instance)

    I don't recall users having issues with this model, but I agree we need to update the docs to mention that the enclosing @Component class should not be excluded from classpath scanning.

  13. snicoll commented on Apr 1, 2026

    @snicoll
    Member

    That's how the annotation programming model of Spring Shell was designed since the beginning.

    OK. If the argument is backward compatibility, streamlining this would increase the coverage of the feature, not decrease it.

    I believe the idea of the pattern

    Is "pattern" the same "pattern" I've used in my first comment? If so, I am afraid I don't see the link. My take is purely about filtering on classes annotated with @Component. That's just a way to create beans from classpath scanning so we could argue it's an implementation detail.

    What makes those classes (i.e. annotated with @Component) more special than any other bean? Why is a bean not contributed this way not taken into account?

  14. fmbenhassine commented on Apr 3, 2026

    @fmbenhassine
    ContributorAuthor

    Yes, I meant pattern as in your first comment. I see your point, and that's a different story. As far as I can tell by looking at previous versions, including beans contributed in other ways was never supported. But I am open to discuss this in a different issue and improve things if possible.

    For now, the fix in this issue is in line with how commands are expected to be contributed with the annotation programming model (ie a class that is annotated with @Component having @Command-annotated methods).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions