【问题标题】:Howto inject Picocli parsed parameters into Spring Bean definitions?如何将 Picocli 解析参数注入 Spring Bean 定义?
【发布时间】:2019-11-20 04:31:34
【问题描述】:

我正在尝试将 Picocli 与 Spring Boot 2.2 一起使用以将命令行参数传递给 Spring Bean,但不确定如何构造它。例如,我有以下@Command 从命令行指定连接用户名和密码,但是,想使用这些参数来定义一个 Bean:

@Component
@CommandLine.Command
public class ClearJdoCommand extends HelpAwarePicocliCommand {
    @CommandLine.Option(names={"-u", "--username"}, description = "Username to connect to MQ")
    String username;

    @CommandLine.Option(names={"-p", "--password"}, description = "Password to connect to MQ")
    String password;

    @Autowired
    JMSMessagePublisherBean jmsMessagePublisher;

    @Override
    public void run() {
        super.run();
        jmsMessagePublisher.publishMessage( "Test Message");
    }
}


@Configuration
public class Config {
    @Bean
    public InitialContext getJndiContext() throws NamingException {
        // Set up the namingContext for the JNDI lookup
        final Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
        env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
        env.put(Context.SECURITY_PRINCIPAL, username);
        env.put(Context.SECURITY_CREDENTIALS, password);
        return new InitialContext(env);
    }

    @Bean
    public JMSPublisherBean getJmsPublisher(InitialContext ctx){
        return new JMSPublisherBean(ctx);
    }
}

我在这里陷入了一个循环。我需要命令行用户名/密码来实例化我的 JMSPublisherBean,但这些仅在运行时可用,在启动时不可用。

我已经设法通过使用延迟初始化、将ClearJdoCommand bean 注入到配置 bean 并从 Spring 上下文中检索我的run() 中的 JMSPublisherBean 来解决这个问题,但这似乎是一个丑陋的 hack。此外,它迫使我所有的豆子都懒惰,这不是我的偏好。

是否有另一种/更好的方法来实现这一点?

【问题讨论】:

标签: spring-boot picocli


【解决方案1】:

第二个选项可能是使用纯 PicoCli(不是 PicoCli spring boot starter)并让它运行命令;命令不会是 Spring bean,只会用于验证参数。

在其call 方法中,Command 将创建SpringApplication,并使用属性填充它(通过setDefaultProperties 或使用JVM System.setProperty - 不同之处在于环境变量将覆盖默认属性,而系统属性具有更高的优先级)。

  @Override
  public Integer call() {
    var application = new SpringApplication(MySpringConfiguration.class);
    application.setBannerMode(Mode.OFF);
    System.setProperty("my.property.first", propertyFirst);
    System.setProperty("my.property.second", propertySecond);
    try (var context = application.run()) {
      var myBean = context.getBean(MyBean.class);
      myBean.run(propertyThird);
    }
    return 0;
  }

这样,PicoCli 将验证输入,提供帮助等,但您可以控制 Spring Boot 应用程序的配置。您甚至可以为不同的命令使用不同的 Spring 配置。我相信这种方法比将所有属性传递给 Spring 容器中的 CommandLineRunner 更自然

【讨论】:

    【解决方案2】:

    一个可能有用的想法是在 2 遍中解析命令行:

    1. 第一遍只是获取配置/初始化所需的信息
    2. 在第二遍中,我们选择其他选项并执行应用程序

    为了实现这一点,我将创建一个单独的类来“复制”配置所需的选项。此类将有一个 @Unmatched 字段用于剩余的 args,因此 picocli 会忽略它们。例如:

    class Security {
        @Option(names={"-u", "--username"})
        static String username;
    
        @Option(names={"-p", "--password"}, interactive = true, arity = "0..1")
        static String password;
    
        @Unmatched List<String> ignored;
    }
    

    在第一遍中,我们只想提取用户名/密码信息,我们还不想执行应用程序。为此,我们可以使用CommandLine.parseArgsCommandLine.populateCommand 方法。

    所以,我们的main 方法看起来像这样:

    public static void main(String[] args) throws Exception {
    
      // use either populateCommand or parseArgs
      Security security = CommandLine.populateCommand(new Security(), args); 
      if (security.username == null || security.password == null) {
          System.err.println("Missing required user name or password");
          new CommandLine(new ClearJdoCommand()).usage(System.err);
          System.exit(CommandLine.ExitCode.USAGE);
      }
    
      // remainder of your normal main method here, something like this?
      System.exit(SpringApplication.exit(SpringApplication.run(MySpringApp.class, args)));
    }
    

    我仍然会在ClearJdoCommand 类中保留(重复)使用和密码选项,以便应用程序可以在需要时打印一个很好的使用帮助消息。

    请注意,我在 Securitystatic 中创建了字段。 这是一种解决方法(hack?),它允许我们将信息传递给 getJndiContext 方法。

    @Bean
    public InitialContext getJndiContext() throws NamingException {
        // Set up the namingContext for the JNDI lookup
        final Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
        env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
        env.put(Context.SECURITY_PRINCIPAL, Security.username); // use info from 1st pass
        env.put(Context.SECURITY_CREDENTIALS, Security.password);
        return new InitialContext(env);
    }
    

    可能有更好的方法将信息传递给此方法。 有没有 Spring 专家愿意加入并向我们展示一个更好的选择?

    【讨论】:

      猜你喜欢
      • 2013-06-25
      • 1970-01-01
      • 1970-01-01
      • 2018-07-02
      • 1970-01-01
      • 2012-08-06
      • 2017-02-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多