7

不久前我偶然发现了这个问题,解释了如何在使用单个命令从命令行调用 Spring Shell 应用程序后退出该应用程序。但是,使用 Spring Boot 在 2.0.0 中对此进行测试,似乎不再是使用命令参数调用 JAR 将执行该命令然后退出的情况。shell 只是正常启动而不执行提供的命令。仍然可以这样做吗?如果没有,是否可以将 JAR 执行中的参数传递给 Spring Shell,然后在执行后触发退出?

例如,假设我有一个命令,import它有几个选项。它可以像这样在 shell 中运行:

$ java -jar my-app.jar

> import -f /path/to/file.txt --overwrite
Successfully imported 'file.txt'

> exit

但是为了构建一个可以利用这个功能的脚本,能够简单地执行和退出会很好:

$ java -jar my-app.jar import -f /path/to/file.txt --overwrite
Successfully imported 'file.txt'
4

6 回答 6

4

使用@my-script 运行它,如下所示:

java -jar my-app.jar @my-script

其中 my-script 是包含您的命令的文件:

import -f /path/to/file.txt --overwrite
于 2020-06-16T19:37:24.287 回答
3

补充一点,我发现了另一种方法,它不会让您选择以交互模式运行,但使用上面的配置文件,您当然可以交换配置。请注意,我使用的是 lombok 和 jool(以防万一有人复制粘贴并遇到有趣的问题!)

入口

@SpringBootApplication
public class Righter {

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

    @Bean
    public ApplicationRunner shellRunner(Shell shell) {
        return new NonInteractiveShellRunner(shell);
    }

应用程序运行器:

@Order(0)
public class NonInteractiveShellRunner implements ApplicationRunner{

    private final Shell shell;

    public NonInteractiveShellRunner(Shell shell) {
        this.shell = shell;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        shell.run(new CommandInputProvider(args.getSourceArgs()));
    }

    public static class PredefinedInputProvider implements InputProvider{

        private final Input input;
        private boolean commandExecuted = false;

        public PredefinedInputProvider(String[] args) {
            this.input = new PredefinedInput(args);
        }

        @Override
        public Input readInput() {
            if (!commandExecuted){
                commandExecuted=true;
                return input;
            }
            return new PredefinedInput(new String[]{"exit"});
        }

        @AllArgsConstructor
        private static class PredefinedInput implements Input{

            private final String[] args;

            @Override
            public String rawText() {
                return Seq.of(args).toString(" ");
            }

            @Override
            public List<String> words(){
                return Arrays.asList(args);
            }
        }

    }

}
于 2017-11-24T10:35:25.927 回答
3

一种在不排除交互模式和脚本模式的情况下添加单个命令运行模式的方法(在 spring-shell-starter::2.0.0.RELEASE 上测试)。

类比ScriptShellApplicationRunner创建一个runner。

// Runs before ScriptShellApplicationRunner and InteractiveShellApplicationRunner
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 200)
public class SingleCommandApplicationRunner implements ApplicationRunner {

    private final Parser parser;
    private final Shell shell;
    private final ConfigurableEnvironment environment;
    private final Set<String> allCommandNames;

    public SingleCommandApplicationRunner(
            Parser parser,
            Shell shell,
            ConfigurableEnvironment environment,
            Set<CustomCommand> customCommands
    ) {
        this.parser = parser;
        this.shell = shell;
        this.environment = environment;
        this.allCommandNames = buildAllCommandNames(customCommands);
    }

    private Set<String> buildAllCommandNames(Collection<CustomCommand> customCommands) {
        final Set<String> result = new HashSet<>();
        customCommands.stream().map(CustomCommand::keys).flatMap(Collection::stream).forEach(result::add);
        // default spring shell commands
        result.addAll(asList("clear", "exit", "quit", "help", "script", "stacktrace"));
        return result;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        final boolean singleCommand = haveCommand(args.getSourceArgs());
        if (singleCommand) {
            InteractiveShellApplicationRunner.disable(environment);
            final String fullArgs = join(" ", args.getSourceArgs());
            try (Reader reader = new StringReader(fullArgs);
                 FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
                shell.run(inputProvider);
            }
        }
    }

    private boolean haveCommand(String... args) {
        for (String arg : args) {
            if (allCommandNames.contains(arg)) {
                return true;
            }
        }
        return false;
    }

}

将跑步者注册为 bean。

@Configuration
class ContextConfiguration {

    @Autowired
    private Shell shell;

    @Bean
    SingleCommandApplicationRunner singleCommandApplicationRunner(
            Parser parser,
            ConfigurableEnvironment environment,
            Set<CustomCommand> customCommands
    ) {
        return new SingleCommandApplicationRunner(parser, shell, environment, customCommands);
    }

}

为了使运行器仅在发送命令时启动,我们创建了一个接口。

public interface CustomCommand {

    Collection<String> keys();

}

在每个命令中实现 CustomCommand 接口。

@ShellComponent
@RequiredArgsConstructor
class MyCommand implements CustomCommand {

    private static final String KEY = "my-command";

    @Override
    public Collection<String> keys() {
        return singletonList(KEY);
    }

    @ShellMethod(key = KEY, value = "My custom command.")
    public AttributedString version() {
        return "Hello, single command mode!";
    }

}

完毕!

以交互模式运行:

java -jar myApp.jar

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// shell:>my-command
// Hello, single command mode!

从文件 script.txt 运行脚本(包含文本“my-command”):

java -jar myApp.jar @script.txt

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// Hello, single command mode!

以单命令模式运行:

java -jar myApp.jar my-command

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// Hello, single command mode!
于 2021-01-14T16:44:54.020 回答
2

除了亚历克斯的答案,这是NonInteractiveApplicationRunner我制作的更简单的版本。

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100)
class NonInteractiveApplicationRunner implements ApplicationRunner {

    private final Shell shell;
    private final ConfigurableEnvironment environment;

    public NonInteractiveApplicationRunner(Shell shell, ConfigurableEnvironment environment) {
        this.shell = shell;
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        if (args.getSourceArgs().length > 0) {
            InteractiveShellApplicationRunner.disable(environment);
            var input = String.join(" ", args.getSourceArgs());
            shell.evaluate(() -> input);
            shell.evaluate(() -> "exit");
        }
    }
}

使用@Component,我们不需要添加 bean 方法。此外,使用shell.evaluate()方法看起来要简单得多shell.run(...)

于 2020-04-22T07:28:45.013 回答
1

我找到了一个不错的解决方法。我没有创建一个ApplicationRunner模仿 v1 行为(这很棘手,因为JLineInputProvider它是一个私有类),而是创建了一个基于活动 Spring 配置文件的可选加载。我使用 JCommander 定义 CLI 参数,允许我对交互式 shell 和一次性执行具有相同的命令。运行不带参数的 Spring Boot JAR 会触发交互式 shell。使用参数运行它会触发一次性执行。

@Parameters
public class ImportParameters {

  @Parameter(names = { "-f", "--file" }, required = true, description = "Data file")
  private File file;

  @Parameter(names = { "-t", "--type" }, required = true, description = "Data type")
  private DataType dataType;

  @Parameter(names = { "-o", "--overwrite" }, description = "Flag to overwrite file if it exists")
  private Boolean overwrite = false;

  /* getters and setters */
}

public class ImportCommandExecutor {

  public void run(ImportParameters params) throws Exception {
    // import logic goes here
  }

}

/* Handles interactive shell command execution */
@ShellComponent
public class JLineInputExecutor {

  // All command executors are injected here
  @Autowired private ImportCommandExecutor importExecutor;
  ...

  @ShellMethod(key = "import", value = "Imports the a file of a specified type.")
  public String importCommand(@ShellOption(optOut = true) ImportParameters params) throws Exception {
    importCommandExecutor.run(params);
  }

  ...

}

/* Handles one-off command execution */
public class JCommanderInputExecutor implements ApplicationRunner {

  // All command executors are injected here
  @Autowired private ImportCommandExecutor importExecutor;
  ...

  @Override
  public void run(ApplicationArguments args) throws Exception {

    // Create all of the JCommander argument handler objects
    BaseParameters baseParameters = new BaseParameters();
    ImportParameters importParameters = new ImportParameters();
    ...

    JCommander jc = newBuilder().
      .acceptUnknownOptions(true)
      .addObject(baseParameters)
      .addCommand("import", importParameters)
      ...
      .build();

    jc.parse(args);
    String mainCommand = jc.getParsedCommand();

    if ("import".equals(mainCommand)){
      importExecutor.run(importParameters);
    } else if (...) {
      ...
    }  

  }
}

@Configuration
@Profile({"CLI"})
public class CommandLineInterfaceConfiguration {

  // All of my command executors are defined as beans here, as well as other required configurations for both modes of execution 
  @Bean
  public ImportCommandExecutor importExecutor (){
    return new ImportCommandExecutor();
  }
  ...

}

@Configuration
@Profile({"SINGLE_COMMAND"})
public class SingleCommandConfiguration {

  @Bean
  public JCommanderInputExecutor commandLineInputExecutor(){
    return new JCommanderInputExecutor();
  }

}

@SpringBootApplication
public class Application {

  public static void main(String[] args) throws IOException {
    String[] profiles = getActiveProfiles(args);
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Application.class);
    builder.bannerMode((Mode.LOG));
    builder.web(false);
    builder.profiles(profiles);
    System.out.println(String.format("Command line arguments: %s  Profiles: %s",
        Arrays.asList(args), Arrays.asList(profiles)));
    builder.run(args);
  }

  private static String[] getActiveProfiles(String[] args){
    return Arrays.asList(args).contains("-X") ? new String[]{"CLI", "SINGLE_COMMAND"} : new String[]{"CLI"};
  }

}

所以现在我可以通过简单地运行我的可执行 JAR 来触发交互式客户端:

java -jar app.jar
> import -f /path/to/file.txt -t GENE -o
> quit()

或者,如果我在命令行上传递“-X”参数,应用程序将执行然后退出:

java -jar app.jar -X import -f /path/to/file.txt -t GENE -o
于 2017-10-25T13:24:05.217 回答
0

在 linux 中也是这样工作的:

echo "import -f /path/to/file.txt --overwrite" | java -jar my-app.jar

尽管它以失败告终;它仅在命令成功执行后发生。

于 2021-12-06T14:55:02.420 回答