Spring BootMiddleTechnical

Что такое CommandLineRunner и ApplicationRunner и когда их использовать?

CommandLineRunner и ApplicationRunner — функциональные интерфейсы, чьи run()-методы вызываются после полной инициализации ApplicationContext. CommandLineRunner получает String[], ApplicationRunner — ApplicationArguments с именованными опциями. Оба блокируют завершение старта до возврата из run().

Контракт и порядок вызова

После того как SpringApplication.run() поднял ApplicationContext и вызвал ApplicationContext.refresh(), он обходит все бины типа CommandLineRunner и ApplicationRunner и вызывает их run()-методы. Порядок между несколькими runner-ами задаётся аннотацией @Order или интерфейсом Ordered.

CommandLineRunner — сырые аргументы

@Component
@Order(1)
public class DataSeeder implements CommandLineRunner {

    private final UserRepository userRepo;

    public DataSeeder(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    public void run(String... args) throws Exception {
        // args = ["--env=prod", "migrate"] — сырые строки из командной строки
        if (userRepo.count() == 0) {
            userRepo.save(new User("admin", "admin@example.com"));
            System.out.println("Seed data inserted");
        }
    }
}

ApplicationRunner — разобранные аргументы

@Component
@Order(2)
public class MigrationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // запуск: java -jar app.jar --mode=migrate --dry-run
        boolean dryRun = args.containsOption("dry-run");
        List<String> modes = args.getOptionValues("mode"); // ["migrate"]
        List<String> nonOption = args.getNonOptionArgs();  // позиционные аргументы

        System.out.printf("Mode: %s, dryRun: %s%n", modes, dryRun);

        if (!dryRun) {
            // реальная миграция
        }
    }
}

Использование в тестах

@SpringBootTest
class DataSeederTest {

    @Autowired
    private UserRepository userRepo;

    @Test
    void seedRuns_whenRepoEmpty() {
        // DataSeeder уже запустился при старте контекста
        assertThat(userRepo.count()).isGreaterThan(0);
    }
}

// Если нужно запустить runner вручную в тесте:
@SpringBootTest
class MigrationRunnerTest {

    @Autowired
    private MigrationRunner runner;

    @Test
    void dryRunDoesNotMutate() throws Exception {
        ApplicationArguments args = new DefaultApplicationArguments("--dry-run");
        runner.run(args); // не бросает исключений, ничего не меняет
    }
}

Типичные сценарии применения

  • Наполнение БД начальными данными (seed)
  • Проверка конфигурации при старте (fail-fast)
  • Запуск однократных миграций данных
  • Прогрев кэша перед принятием трафика
  • CLI-режим: приложение выполняет задачу и завершается

Подводные камни

  • Блокировка readiness: runner блокирует финальный этап старта. Долгий runner задержит сигнал готовности в Kubernetes (readiness probe), что приведёт к таймауту и перезапуску пода. Тяжёлые операции запускайте в отдельном потоке или через @Async.
  • Исключение в run() = падение приложения: непойманное исключение из runner завершит приложение с ненулевым exit code. Оборачивайте в try/catch или используйте retry-логику.
  • Тест-контекст запускает runner-ы: в @SpringBootTest все runner-ы выполняются при каждом поднятии контекста. Если runner делает side-effect (insert в БД), тест может упасть при повторном запуске. Используйте @MockBean для изоляции или проверяйте идемпотентность.
  • CommandLineRunner не разбирает опции: --key=value придёт как одна строка, а не как пара ключ/значение. Для параметризованных CLI используйте ApplicationRunner.
  • Несколько runner-ов без @Order: порядок выполнения недетерминирован, если @Order не задан. В зависимых runner-ах это приведёт к случайным ошибкам.
  • Транзакции в runner: runner-методы не оборачиваются в транзакцию автоматически. Добавьте @Transactional на метод run() или вызывайте транзакционный сервис.
  • Не подходит для периодических задач: runner выполняется один раз при старте. Для повторяющихся задач используйте @Scheduled или внешний планировщик.
  • Профили и условная активация: если runner нужен только в определённой среде, комбинируйте с @Profile("dev") или @ConditionalOnProperty — иначе seed-данные попадут в продакшн.

Common mistakes

  • Путать термин «commandline application runner» с соседним механизмом Spring Boot.
  • Не называть границу lifecycle, transaction, thread или request для «commandline application runner».
  • Игнорировать production-эффекты «commandline application runner»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «commandline application runner» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «commandline application runner».
  • Уточнить, какие настройки или API меняют «commandline application runner» в реальном сервисе.

Sources

Related topics