12

I have created spring boot application with spring cloud task which should executes a few commands(tasks). Each task/command is shorted-lived task, and all tasks are start from command line, do some short ETL job and finish execution.

There is one spring boot jar which contain all the commands/tasks. Each task is CommandLineRunner, and I like to decide which tasks (one or more) will be executed based on the params from command line. What is the best practice to do so? I don't like to have dirty code which ask "if else" or something like this.

Shay
  • 121
  • 1
  • 1
  • 6
  • 2
    If you have multiple main classes in the jar file, then you can issue `java -classpath myapp.jar com.example.Task1` on the command line instead of `java -jar myapp.jar`. No if-else anywhere in sight. Why the hate for if-else though? Program's got to branch some times. – Barend Jun 11 '17 at 07:27
  • 1
    Thanks, I'm not sure that spring boot allow using multiple main classes. Using hardcoded if-else its harder to maintain then inject components – Shay Jun 12 '17 at 08:01

5 Answers5

12

You can also make your CommandLineRunner implementations @Component and @ConditionalOnExpression("${someproperty:false}")

then have multiple profiles, that set someproperty to true to include those CommandLineRunners in the Context.

@Component
@Slf4j
@ConditionalOnExpression("${myRunnerEnabled:false}")
public class MyRunner implements CommandLineRunner {
    @Override
    public void run(String ... args) throws Exception {
        log.info("this ran");
    }
}

and in the yml application-myrunner.yml

myRunnerEnabled: true
@SpringBootApplication
public class SpringMain {
    public static void main(String ... args) {
        SpringApplication.run(SpringMain.class, args);
    }
}
user2043566
  • 133
  • 2
  • 6
  • This is an elegant solution when an application as well as another dependent artifact both have implemented CommandLineRunner. Without this solution, both the implementation classes get executed. – Srikanta Oct 30 '19 at 14:45
10

Spring Boot runs all the CommandLineRunner or ApplicationRunner beans from the application context. You cannot select one by any args.

So basically you have two possibiities:

  1. You have different CommandLineRunner implementations and in each you check the arguments to determine if this special CommandLineRunner should run.
  2. You implement only one CommandLineRunner which acts as a dispatcher. Code might look something like this:

This is the new Interface that your runners will implement:

public interface MyCommandLineRunner {
    void run(String... strings) throws Exception;
}

You then define implementations and identify them with a name:

@Component("one")
public class MyCommandLineRunnerOne implements MyCommandLineRunner {
    private static final Logger log = LoggerFactory.getLogger(MyCommandLineRunnerOne.class);

    @Override
    public void run(String... strings) throws Exception {
        log.info("running");
    }
}

and

@Component("two")
public class MyCommandLineRunnerTwo implements MyCommandLineRunner {
    private static final Logger log = LoggerFactory.getLogger(MyCommandLineRunnerTwo.class);
    @Override
    public void run(String... strings) throws Exception {
        log.info("running");
    }
}

Then in your single CommandLineRunner implementation you get hold of the application context and resolve the required bean by name, my example uses just the first argument, and call it's MyCommandLineRunner.run()method:

@Component
public class CommandLineRunnerImpl implements CommandLineRunner, ApplicationContextAware {
    private ApplicationContext applicationContext;


    @Override
    public void run(String... strings) throws Exception {
        if (strings.length < 1) {
            throw new IllegalArgumentException("no args given");
        }

        String name = strings[0];
        final MyCommandLineRunner myCommandLineRunner = applicationContext.getBean(name, MyCommandLineRunner.class);
        myCommandLineRunner.run(strings);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
P.J.Meisch
  • 18,013
  • 6
  • 50
  • 66
  • We are trying to implement it with spring cli's command CliCommand annotation. It seems that each command is like "main" which already parse the arguments fro command line – Shay Jun 12 '17 at 08:03
  • so this is something different than the Spring Boot CommandLine Runner; sorry, never have used that – P.J.Meisch Jun 12 '17 at 09:31
  • 3
    Neither approach is remotely reasonable.... Just going to the 3 story high switch statement, ... I hate to say it, but it maintains better. Dependency injection, sure 100%, once you pass beyond that though... – J. M. Becker Oct 01 '18 at 03:32
3

Strangely there is not a built-in mechanism to select a set of CommandLineRunner. By default all of them are executed.

I have reused - maybe improperly - the Profile mechanism, that is I have annotated each CommandLineRunner with @org.springframework.context.annotation.Profile("mycommand"), and I select the one I want to execute with the System property -Dspring.profiles.active=mycommand

For more information on the Profile mechanism, please refer to https://www.baeldung.com/spring-profiles

Pierluigi Vernetto
  • 1,954
  • 1
  • 25
  • 27
2

Similar to this answer https://stackoverflow.com/a/44482525/986160 but using injection and less configuration.

Having a similar requirement, what has worked for me is to have one CommandLineApps class implementing CommandLineRunner, then inject all my other command line runners as @Component and use the first argument to delegate to one of the injected runners. Please note that the injected ones should not extend CommandLineRunner and not be annotated as @SpringBootAppplication.

Important: be careful though if you put the call in a crontab one call will destroy the previous one if it is not done.

Here is an example:

@SpringBootApplication
public class CommandLineApps implements CommandLineRunner {

    @Autowired
    private FetchSmsStatusCmd fetchSmsStatusCmd;

    @Autowired
    private OffersFolderSyncCmd offersFolderSyncCmd;

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(CommandLineApps.class,  args);
        ctx.close();
    }

    @Override
    public void run(String... args) {

        if (args.length == 0) {
            return;
        }

        List<String> restOfArgs = Arrays.asList(args).subList(1, args.length);

        switch (args[0]) {
            case "fetch-sms-status":
                fetchSmsStatusCmd.run(restOfArgs.toArray(new String[restOfArgs.size()]));
                break;
            case "offers-folder-sync":
                offersFolderSyncCmd.run(restOfArgs.toArray(new String[restOfArgs.size()]));
                break;
        }
    }
}
@Component
public class FetchSmsStatusCmd {

    [...] @Autowired dependencies    

    public void run(String[] args) {

        if (args.length != 1) {
            logger.error("Wrong number of arguments");
            return;
        }
        [...]
    }
 }
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
1

You can have multiple CommandLineRunner in single file or application. Which one needs to be executed ( calling "run" method) decided by the one and only - Order (@Order annotation). If you need to execute multiple CommandLineRunner, write them, pass the value to Order annotation.

@Bean
    @Order(value = 1)
    public CommandLineRunner demo1(CustomerRepository customerRepository){
        return (args) ->{
            customerRepository.save(new Customer("Viji", "Veerappan"));
            customerRepository.save(new Customer("Dhinesh", "Veerappan"));
            customerRepository.save(new Customer("Senbagavalli", "Veerappan"));
        };
    }



@Order(value = 2)
    public CommandLineRunner demo(CustomerRepository customerRepository){
        return (args) ->{

            // Save all the customers
            customerRepository.save(new Customer("Mahith", "Saravanan"));
            customerRepository.save(new Customer("Pooshi", "Saravanan"));
            customerRepository.save(new Customer("Dharma", "Saravanan"));
            customerRepository.save(new Customer("Mookayee", "Veerayandi"));
            customerRepository.save(new Customer("Chellammal", "Kandasamy"));

            //fetch all customer
            log.info("Fetching all the customers by findAll()");
            log.info("----------------------------------------");
            for(Customer customer : customerRepository.findAll()){
                log.info(customer.toString());
            }
            log.info("");

            //fetch one customer by Id
            log.info("Fetch one customer Id by findById(1L)");
            log.info("----------------------------------------");
            log.info(customerRepository.findById(1L).toString());
            log.info("");

            //fetch by last name
            log.info("Fetch all customers that have lastname = Saravanan");
            log.info("---------------------------------------------------");
            for(Customer customer: customerRepository.findByLastName("Saravanan")){
                log.info(customer.toString());
            }
            /*customerRepository.findByLastName("Saravanan").forEach( saravanan ->{
                saravanan.toString();
            });*/
        };
    }
D.S.
  • 137
  • 5