3

Currently, I have a component that implements CommandLineRunner and parses the command line arguments with Commons CLI.

java -jar app.jar --host 123.123.222 --port 8080

There is also another component, Requester, which depends on (a subset of) those arguments.

@Component
public class Requester
{

  // host and port need to be configured once
  private final String host;
  private final int port;

  public Requester(String host, int port)
  {
    this.host = host;
    this.port = port;
  }

  public boolean doRequest(String name) throws Exception
  {
    String url = "http://" + host + ":" + port + "/?command=" + name;

    URL obj = new URL(url);
    HttpURLConnection connection = (HttpURLConnection) obj.openConnection();
    int responseCode = connection.getResponseCode();
    return 200 == responseCode;
  }

}

What can I do to autowire a configured Requester into future components? What is the Spring way to create parameterized, singleton beans?


One solution would be to have every component, that has any dependency on the program arguments, implement CommandLineRunner. This way it could parse the program arguments itself, but that is a highly redundant approach. There must be a better solution.

mike
  • 4,929
  • 4
  • 40
  • 80
  • What are the parameters in your case? – NiVeR Nov 26 '18 at 19:30
  • The arguments are host and port. – mike Nov 26 '18 at 19:31
  • You can make the fields mutable, and set them from your CommandLineRunner. Or you can parse the command-line arguments *before* starting the Spring application, and use an `@Bean` which creates the requester using the parsed arguments. – JB Nizet Nov 26 '18 at 19:35
  • @JBNizet mutable and static, you mean? Or is there a step missing? Like registering the configured object as bean? Can you please elaborate. – mike Nov 26 '18 at 19:41
  • No, not static. The CommandLineRunner can have the Requester as its dependencies (i.e. have an Autowired field of type Requester), and store the parsed arguments in the requester. – JB Nizet Nov 26 '18 at 19:45
  • Ah, via setter. Now I get it. Is there another way without having to initialize the Requester after instantiation? – mike Nov 26 '18 at 19:50
  • Or you can parse the command-line arguments before starting the Spring application, and use an `@Bean` method which creates the requester using the parsed arguments – JB Nizet Nov 26 '18 at 19:52
  • I don't see why this discussion goes on..@mike have you tried the answer? – NiVeR Nov 26 '18 at 19:55
  • NiVeR The current answer does not allow the simultaneous use of short and long option names, i.e. `--host` and `-h`, because of the tight coupling to the names. – mike Nov 26 '18 at 20:09
  • @mike This wasn't even mentioned in the question. Anyway, in each possible solution you will most likely need to handle that manually. – NiVeR Nov 26 '18 at 20:13
  • @NiVeR I didn't know that answers could rely on concrete naming of the program arguments. I'd have no problem with manually registering the preconfigured instance of `Requester. I just wanted ease of use later on and simply 'autowire' the component when needed. – mike Nov 26 '18 at 20:26
  • @mike You can autowire the `Requester` without problems. Maybe I am not getting something – NiVeR Nov 26 '18 at 20:31
  • I want the CLI component to parse the command line, extract host/port and then create a `new Requester(host, port)` and register it to Spring (somehow), in order for future components to just define `@Autowire private Requester requester`. I hope this shows what I want. – mike Nov 26 '18 at 20:35
  • @mike The annotation `@Component` (as used in my answer) does exaclty that. Creates/registers a bean of type `Requester` in the spring context which you can inject by `@Autowired private Requester requester`. – NiVeR Nov 26 '18 at 20:38
  • Yes, but host/port are determined at runtime (by my CLI component which also supports short options, i.e. --host and -h). – mike Nov 26 '18 at 20:43
  • I've added an own answer. – mike Nov 27 '18 at 17:16

3 Answers3

7

Have you checked the annotation @Value? It allows you to inject values from properties files at run time. You should be using that one each time you need to inject some external value from resources. For instance your code can be:

@Component
public class Requester
{
  @Value("${host}")
  private final String host;
  @Value("${port}")
  private final int port;

  public boolean doRequest(String name) throws Exception
  {
    String url = "http://" + host + ":" + port + "/?command=" + name;

    URL obj = new URL(url);
    HttpURLConnection connection = (HttpURLConnection) obj.openConnection();
    int responseCode = connection.getResponseCode();
    return 200 == responseCode;
  } 
}

In your application.properties file:

...
host = myhost
port = 1234
...

If you want to pass the parameters as command line arguments you can simply invoke the command as you did:

java -jar app.jar --host 123.123.222 --port 8080

and these parameters will override the ones from the property files since they are given higher priority as can be seen in the documentation.

NiVeR
  • 9,644
  • 4
  • 30
  • 35
  • I've clarified the question to show that it is a command line application. I don't want to configure it via property files. Still, thank you for your answer :-) – mike Nov 26 '18 at 19:39
  • Thanks for the code update. This is valuable information, since I'm a Spring novice! – mike Nov 26 '18 at 19:42
  • As long as you are not inside the scope of a `BeanPostProcessor` or `BeanFactoryPostProcessor` type you should be fine (as per the documentation). – NiVeR Nov 26 '18 at 19:45
  • @Savior Check your facts first: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-command-line-args – NiVeR Nov 26 '18 at 19:54
  • @NiVeR Maybe you should include that information in the answer. – mike Nov 26 '18 at 20:06
1

Use post @PostConstruct to deal with that.
Lets assume that this is your CommandLineRunner implementation :

@Component
public class CLIArgs implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        // Parse the args, and for each arg set system property
        System.setProperty("ArgName", "value");
    }
}

Then create Getters for all the args you want, And the the Requester will look like that:

@Component
public class Requester {
    @Autowired
    private Environment env; // This is a spring component

    // host and port need to be configured once
    private String host;
    private int port;

    @PostConstruct
    public void init () {
        this.host = env.getProperty("host");
        this.port = Integer.parseInt(env.getProperty("port"));
    }

    public boolean doRequest(String name) throws Exception {
        String url = "http://" + host + ":" + port + "/?command=" + name;

        URL obj = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) obj.openConnection();
        int responseCode = connection.getResponseCode();
        return 200 == responseCode;
    }
}

The @PostConstruct will happen when the component is created

Daniel Taub
  • 5,133
  • 7
  • 42
  • 72
  • 1
    Thanks! Is there a possibility to remove the coupling to `CLIArgs`? Maybe a way where the `Requester` instance is created in `CLIArgs` and then somehow registered to the Spring environment? – mike Nov 26 '18 at 20:14
1

Inspired by JB Nizet's comment, I now parse the command line arguments and register the beans manually before I start the Spring application.

I identified two ways: provide the c'tor arguments and let Spring create the beans or provide a supplier function to Spring.

Now it's possible to declare @Autowire private Requester requester; in other parts of the application.


In order for this solution to work the @Component annotation needs to be removed from Requester, because otherwise problems may occur when Spring can't provide the necessary constructor arguments.

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        // parse args here
        String host = getHost();
        int port = getPort();

        SpringApplication application = new SpringApplication(Application.class);

        // provide c'tor arguments and let Spring create the instance
        BeanDefinitionCustomizer bdc = bd -> {
            // working with bd.getPropertyValues() uses setter instead of c'tor
            ConstructorArgumentValues values = bd.getConstructorArgumentValues();
            values.addIndexedArgumentValue(0, host);
            values.addIndexedArgumentValue(1, port);
        };
        application.addInitializers((GenericApplicationContext ctx) -> ctx.registerBean(Requester.class, bdc));

        // or create the instance yourself
        Requester requester = new Requester(host, port);
        application.addInitializers((GenericApplicationContext ctx) -> ctx.registerBean(Requester.class, () -> requester));
        application.run(args);
    }

}
mike
  • 4,929
  • 4
  • 40
  • 80
  • I've accepted my own answer since it does not introduce new dependencies to `Requester`, it also does not restrict the command line parameters (long and short options are both possible). – mike Nov 29 '18 at 22:17