返回 Skill 列表
extension
分类: 开发与工程无需 API Key

spring-shell

Spring Shell用于构建交互式CLI应用程序。涵盖@ShellComponent、@ShellMethod、输入验证、表格和命令组。使用场景:用户提到“spring shell”、“@ShellComponent”、“@ShellMethod”、“CLI application Spring”、“interactive shell”、“command line tool Spring”。不适用于:简单的脚本-应使用常规的main方法,批处理-应使用`spring-batch`技能,Web端点-应使用`spring-rest`技能

person作者: jakexiaohubgithub

Spring Shell - Quick Reference

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: spring-shell for comprehensive documentation.

Dependencies

<dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell-starter</artifactId>
</dependency>

Configuration

application.yml

spring:
  shell:
    interactive:
      enabled: true
    noninteractive:
      enabled: true
    history:
      enabled: true
      name: .myapp_history
    command:
      version:
        enabled: true

Basic Commands

Simple Command

@ShellComponent
public class GreetingCommands {

    @ShellMethod(value = "Say hello", key = "hello")
    public String hello(
            @ShellOption(defaultValue = "World") String name) {
        return "Hello, " + name + "!";
    }

    @ShellMethod("Add two numbers")
    public int add(int a, int b) {
        return a + b;
    }

    @ShellMethod("Show current date")
    public String date() {
        return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

Command Groups

@ShellComponent
@ShellCommandGroup("User Management")
public class UserCommands {

    @ShellMethod("List all users")
    public Table listUsers() {
        List<User> users = userService.findAll();
        return buildUserTable(users);
    }

    @ShellMethod("Create a new user")
    public String createUser(
            @ShellOption(help = "Username") String username,
            @ShellOption(help = "Email address") String email,
            @ShellOption(help = "User role", defaultValue = "USER") String role) {

        User user = userService.create(username, email, role);
        return "Created user: " + user.getId();
    }

    @ShellMethod("Delete a user")
    public String deleteUser(@ShellOption(help = "User ID") Long id) {
        userService.delete(id);
        return "User deleted successfully";
    }
}

Shell Options

Option Annotations

@ShellComponent
public class AdvancedCommands {

    @ShellMethod("Process file with options")
    public String process(
            // Required option
            @ShellOption(help = "Input file path") String input,

            // Optional with default
            @ShellOption(defaultValue = "output.txt", help = "Output file") String output,

            // Boolean flag
            @ShellOption(defaultValue = "false", help = "Verbose mode") boolean verbose,

            // Array/List option
            @ShellOption(help = "Tags to apply") String[] tags,

            // Arity (number of values)
            @ShellOption(arity = 2, help = "Coordinates x y") int[] coords) {

        // Implementation
        return "Processing " + input;
    }

    // Named options with aliases
    @ShellMethod("Export data")
    public String export(
            @ShellOption(value = {"-f", "--format"}, defaultValue = "json") String format,
            @ShellOption(value = {"-o", "--output"}) String output,
            @ShellOption(value = {"-c", "--compress"}, defaultValue = "false") boolean compress) {

        return String.format("Exporting to %s in %s format (compressed: %s)",
            output, format, compress);
    }
}

Availability and Validation

Command Availability

@ShellComponent
public class AdminCommands {

    private boolean authenticated = false;

    @ShellMethod("Login to the system")
    public String login(String username, String password) {
        if (authService.authenticate(username, password)) {
            authenticated = true;
            return "Login successful";
        }
        return "Login failed";
    }

    @ShellMethod("Perform admin action")
    @ShellMethodAvailability("isAuthenticated")
    public String adminAction() {
        return "Admin action performed";
    }

    public Availability isAuthenticated() {
        return authenticated
            ? Availability.available()
            : Availability.unavailable("You must login first");
    }

    // Apply to multiple methods
    @ShellMethodAvailability({"adminAction", "deleteAll", "resetSystem"})
    public Availability requiresAdmin() {
        return currentUser.isAdmin()
            ? Availability.available()
            : Availability.unavailable("Admin privileges required");
    }
}

Input Validation

@ShellComponent
public class ValidatedCommands {

    @ShellMethod("Create user with validation")
    public String createUser(
            @ShellOption @Size(min = 3, max = 20) String username,
            @ShellOption @Email String email,
            @ShellOption @Min(18) @Max(120) int age) {

        return "User created: " + username;
    }
}

// Custom validator
@Component
public class FileExistsValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return String.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        String path = (String) target;
        if (!Files.exists(Path.of(path))) {
            errors.reject("file.notfound", "File does not exist: " + path);
        }
    }
}

Output Formatting

Tables

@ShellComponent
public class TableCommands {

    @ShellMethod("Show users in table format")
    public Table showUsers() {
        List<User> users = userService.findAll();

        TableModel model = new BeanListTableModel<>(users,
            new LinkedHashMap<>() {{
                put("id", "ID");
                put("username", "Username");
                put("email", "Email");
                put("role", "Role");
                put("createdAt", "Created");
            }});

        TableBuilder tableBuilder = new TableBuilder(model);
        tableBuilder.addFullBorder(BorderStyle.fancy_light);
        return tableBuilder.build();
    }

    // Custom table
    @ShellMethod("Show system info")
    public Table systemInfo() {
        String[][] data = {
            {"OS", System.getProperty("os.name")},
            {"Java", System.getProperty("java.version")},
            {"Memory", Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB"},
            {"Processors", String.valueOf(Runtime.getRuntime().availableProcessors())}
        };

        TableModel model = new ArrayTableModel(data);
        TableBuilder builder = new TableBuilder(model);
        builder.addHeaderBorder(BorderStyle.fancy_double);
        return builder.build();
    }
}

Colored Output

@ShellComponent
public class ColoredCommands {

    @ShellMethod("Show status with colors")
    public AttributedString status() {
        AttributedStringBuilder builder = new AttributedStringBuilder();

        builder.append("Status: ");
        builder.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
        builder.append("RUNNING");
        builder.style(AttributedStyle.DEFAULT);
        builder.append("\n");

        builder.append("Errors: ");
        builder.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold());
        builder.append("3");
        builder.style(AttributedStyle.DEFAULT);

        return builder.toAttributedString();
    }
}

Interactive Input

User Prompts

@ShellComponent
public class InteractiveCommands {

    private final LineReader lineReader;

    @ShellMethod("Interactive user creation")
    public String createUserInteractive() {
        String username = lineReader.readLine("Enter username: ");
        String email = lineReader.readLine("Enter email: ");
        String password = lineReader.readLine("Enter password: ", '*');

        String confirm = lineReader.readLine("Create user? (y/n): ");
        if ("y".equalsIgnoreCase(confirm)) {
            userService.create(username, email, password);
            return "User created successfully";
        }
        return "Cancelled";
    }
}

Progress Indicators

@ShellComponent
public class ProgressCommands {

    private final TerminalUI terminalUI;

    @ShellMethod("Import data with progress")
    public void importData(String file) throws Exception {
        List<String> lines = Files.readAllLines(Path.of(file));
        int total = lines.size();

        try (ProgressView progress = terminalUI.progressView()
                .spinner(Spinner.SIMPLE)
                .text("Importing data...")
                .max(total)
                .start()) {

            for (int i = 0; i < total; i++) {
                processLine(lines.get(i));
                progress.setValue(i + 1);
                progress.setText("Processing " + (i + 1) + "/" + total);
            }
        }
    }
}

Non-Interactive Mode

// Run single command
// java -jar myapp.jar hello --name John

// Run script
// java -jar myapp.jar @commands.txt

@SpringBootApplication
public class CliApplication implements CommandLineRunner {

    @Autowired
    private Shell shell;

    @Override
    public void run(String... args) throws Exception {
        if (args.length > 0 && args[0].equals("--batch")) {
            // Run in non-interactive mode
            shell.evaluate(() -> "process-batch --file data.csv");
            shell.evaluate(() -> "export --format json --output result.json");
            System.exit(0);
        }
        // Otherwise, start interactive shell
    }
}

Custom Prompt

@Component
public class CustomPromptProvider implements PromptProvider {

    private String currentContext = "default";

    @Override
    public AttributedString getPrompt() {
        return new AttributedString(
            String.format("myapp:%s> ", currentContext),
            AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN)
        );
    }

    public void setContext(String context) {
        this.currentContext = context;
    }
}

Exception Handling

@Component
public class CustomExceptionHandler implements CommandExceptionResolver {

    @Override
    public CommandHandlingResult resolve(Exception ex) {
        if (ex instanceof IllegalArgumentException) {
            return CommandHandlingResult.of(
                "Invalid argument: " + ex.getMessage() + "\n",
                1  // Exit code
            );
        }
        if (ex instanceof ResourceNotFoundException) {
            return CommandHandlingResult.of(
                "Error: " + ex.getMessage() + "\n",
                2
            );
        }
        // Let default handler process
        return null;
    }
}

Testing

@SpringBootTest
class CommandsTest {

    @Autowired
    private Shell shell;

    @Test
    void shouldSayHello() {
        Object result = shell.evaluate(() -> "hello --name John");
        assertThat(result).isEqualTo("Hello, John!");
    }

    @Test
    void shouldAddNumbers() {
        Object result = shell.evaluate(() -> "add 5 3");
        assertThat(result).isEqualTo(8);
    }

    @Test
    void shouldShowUnavailableForAdminCommands() {
        Object result = shell.evaluate(() -> "admin-action");
        assertThat(result.toString()).contains("must login first");
    }
}

Best Practices

| Do | Don't | |----|-------| | Group related commands | Create flat command structure | | Provide --help for all options | Leave options undocumented | | Use tables for structured data | Dump raw data | | Implement availability checks | Let commands fail cryptically | | Support both interactive and batch | Force one mode only |

Production Checklist

  • [ ] Commands grouped logically
  • [ ] All options documented (help)
  • [ ] Input validation implemented
  • [ ] Proper error handling
  • [ ] Progress indicators for long ops
  • [ ] Non-interactive mode supported
  • [ ] Exit codes defined
  • [ ] History enabled/configured
  • [ ] Security checks (authentication)
  • [ ] Tab completion working

When NOT to Use This Skill

  • Simple scripts - Use regular main method
  • Batch processing - Use spring-batch skill
  • Web APIs - Use spring-rest skill
  • GUI applications - Consider JavaFX

Anti-Patterns

| Anti-Pattern | Problem | Solution | |--------------|---------|----------| | Flat command structure | Hard to navigate | Group related commands | | Undocumented options | Poor UX | Add help to all options | | No availability checks | Cryptic failures | Implement @ShellMethodAvailability | | Raw data output | Unreadable | Use tables for structured data | | Interactive-only mode | Not scriptable | Support batch mode |

Quick Troubleshooting

| Problem | Diagnostic | Fix | |---------|------------|-----| | Command not found | Check annotation | Add @ShellComponent | | Option not recognized | Check naming | Verify @ShellOption config | | Validation not working | Check annotations | Add Bean Validation | | Command unavailable | Check availability | Verify availability method | | Tab completion missing | Check setup | Configure completion bean |

Reference Documentation