JavaRush /Курсы /Spring Boot /Стартовая логика: runners

Стартовая логика: runners

Spring Boot
6 уровень , 0 лекция
Открыта

1. Размещение стартового кода

Каждый разработчик рано или поздно сталкивается с желанием сделать «маленькую штуку на старте». Например, вывести краткую сводку, проверить, что приложение запустилось в правильном режиме, или подготовить кэш в памяти. В консольных Java-приложениях обычно рука тянется написать это прямо в main(String...args). В Spring Boot это быстро превращается в неудобный и плохо поддерживаемый стиль: main() становится свалкой, а логика старта начинает конфликтовать с тем, как Boot управляет жизненным циклом и зависимостями.

Давайте честно: main() в Boot-проекте — это больше «кнопка включения», чем «место, где живёт бизнес-логика». Если вы начнёте писать в main() реальный код старта, то очень быстро получите три типовые проблемы.

Первая проблема в том, что код в main() почти всегда начинает создавать зависимости вручную. Вы либо пишете new (и теряете DI), либо пытаетесь «как-то достать бин» из контекста (и уже пахнет странным). Вторая проблема в том, что main() плохо тестируется и плохо читается: он должен быть простым, иначе любой новый разработчик будет читать ваш старт как детектив. Третья проблема в том, что main() выполняется до того, как приложение нормально собралось: многие вещи (особенно то, что является Spring-managed) там ещё не в том состоянии, когда вы можете ими удобно пользоваться.

Если вам нужен образ, то main() — это не кухня и не бар, это выключатель света у входа. Нажать — ок. Готовить борщ на выключателе — уже странно.

2. Runner как хук старта

Чтобы не превращать main() в «пульт управления всем», Spring Boot даёт очень понятный и безопасный механизм: runners. Runner — это Spring-managed компонент (то есть обычный бин), который Boot вызывает один раз при старте приложения, когда контекст уже собран и зависимости уже можно нормально внедрять. Это как “последний шаг перед тем, как сказать: приложение готово”.

Важно уловить идею: runner — не «альтернативный main». Это именно hook (крючок) в жизненном цикле, куда удобно положить короткую стартовую логику. По ощущениям это похоже на “после того, как всё поднялось — сделай маленькую и понятную стартовую работу”.

Визуально последовательность можно представлять примерно так:

flowchart TD
    A[JVM запускается] --> B["main(String... args)"]
    B --> C["SpringApplication.run(...)"]
    C --> D[Создание ApplicationContext]
    D --> E[Создание бинов и внедрение зависимостей]
    E --> F["Вызов runners (1 раз)"]
    F --> G[Приложение считается готовым]

Ключевой момент здесь в том, что runner запускается после того, как Spring собрал граф зависимостей. Значит, runner можно писать как нормальный Spring-компонент с constructor injection: он может получить сервисы, репозитории, настройки — всё, что существует в контексте. Но (и это важно) runner должен оставаться коротким и предсказуемым: это часть старта, а старт — место, где всё должно быть максимально понятно, иначе вы будете «дебажить запуск» вместо разработки.

В Spring Boot есть два близких интерфейса, которые решают эту задачу: CommandLineRunner и ApplicationRunner. Сейчас мы аккуратно разведём их по ролям.

3. CommandLineRunner: String... args

CommandLineRunner — это самый простой и прямолинейный startup hook. Он нужен в ситуациях, когда вам либо вообще не важны аргументы запуска, либо вам достаточно получить их как сырой массив строк, без «умного разложения». Для начинающего разработчика это часто лучший старт: проще сигнатура, меньше новых понятий, легче поймать, что runner действительно запускается в правильном месте.

Ниже будут несколько маленьких вариантов одного и того же startup hook. Их не нужно держать все одновременно в catalog-service: это учебные probes, чтобы почувствовать механику и выбрать подходящую форму.

Минимальный пример в catalog-service

Представим, что мы хотим на старте просто увидеть: приложение дошло до runner-фазы и сколько аргументов ему передали. Создадим компонент (бин), который реализует CommandLineRunner.

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class RawArgsRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // Boot вызывает этот метод один раз при старте, когда контекст уже поднят
        System.out.println("Raw args count = " + args.length); // Raw args count = 0
    }
}

Обратите внимание на две вещи. Во‑первых, это обычный Spring-компонент (@Component), то есть его не нужно вручную создавать. Во‑вторых, run(...) получает String... args — те самые аргументы, которые JVM передаёт в main(String[]args), а Boot дальше прокидывает внутрь запуска.

Если вы запустите приложение без аргументов, получите 0. Если добавите пару аргументов (например, в IDE в поле Program arguments), увидите другое число. Мы сейчас не будем превращать приложение в CLI-комбайн, нам важно почувствовать механизм.

Альтернатива: объявить runner через @Bean

Иногда runner настолько маленький, что отдельный класс кажется «слишком торжественным». Можно зарегистрировать его как бин в конфигурационном классе через @Bean. Это тот же механизм, просто другой способ регистрации.

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StartupConfiguration {

    @Bean
    CommandLineRunner helloRunner() {
        // Удобный вариант для совсем короткой стартовой логики без отдельного класса
        return args -> System.out.println("Hello from runner"); // Hello from runner
    }
}

Это не «лучше» и не «хуже» — это вопрос удобства. Для учебного проекта чаще проще начать с отдельного класса: он легче читается и проще расширяется. Но полезно знать, что @Bean-вариант существует, и он вполне рабочий. Если выбираете этот вариант, предыдущий класс обычно просто не нужен.

4. ApplicationRunner: ApplicationArguments

ApplicationRunner решает ту же задачу, что и CommandLineRunner: дать вам место для одноразовой логики старта. Разница в том, что вместо сырых строк он получает ApplicationArguments — объект, который умеет хранить аргументы запуска и даёт более удобный API для чтения. Это особенно полезно, когда вы хотите отличать «флаги» от «позиционных значений», и не писать свой ручной парсер на десять if и два split("=") (что обычно заканчивается слезами и странными багами).

Здесь нам не нужен весь API ApplicationArguments. Достаточно увидеть сам факт: runner может получать не только String..., но и более структурированный объект.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class SourceArgsRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // getSourceArgs() — это «сырой» исходный массив строк, который пришёл в приложение при запуске
        int count = args.getSourceArgs().length;
        System.out.println("Source args count = " + count); // Source args count = 0
    }
}

Этот пример тоже обычно заменяет предыдущий probe, если вам нужен именно ApplicationArguments, а не живёт рядом с ним ради коллекции runner’ов.

Этот пример пока специально «почти такой же», как в CommandLineRunner. Он нужен, чтобы вы увидели: механизм тот же, точка старта та же, но входные данные уже не просто массив строк. Для реального чтения опций у ApplicationArguments есть containsOption(...), getOptionValues(...) и getNonOptionArgs(), но здесь нам важнее закрепить саму развилку: есть два интерфейса, и различие между ними в форме аргументов.

Ещё один важный момент: ApplicationRunner обычно выигрывает у CommandLineRunner даже тогда, когда вы не собираетесь читать аргументы сразу. Просто потому что в реальном проекте «а давайте добавим одну маленькую опцию запуска» — это типичная просьба, и если вы уже на ApplicationRunner, то вы к ней готовы.

5. Выбор runner-интерфейса

Выбор между этими двумя механизмами чаще всего не философский. Он очень практичный: насколько вам важны аргументы и насколько вы хотите читать их «человечески». Внутри одного проекта их можно смешивать, но обычно здоровее придерживаться одной логики: для одной задачи — один runner, и лучше чтобы он был читаемым, а не «универсальным комбайном».

Критерий
CommandLineRunner
ApplicationRunner
Сигнатура run(String... args) run(ApplicationArguments args)
Входные данные сырые строки объект с API для чтения аргументов
Когда удобно «мне вообще не важны аргументы» или «хватает массива строк» «хочу читать аргументы без ручного парсинга»
Типичный уровень комфорта в проекте стартовый, самый простой обычно удобнее в долгую

Теперь — несколько “жизненных” сценариев, в которых runners реально полезны (без уходов в будущие темы). Runner хорошо подходит, когда вы хотите сделать короткое и предсказуемое действие на старте. Например, вывести в консоль «мы дошли до runner-фазы» — это простая проверка в процессе обучения. Или сделать небольшую проверку инварианта, от которой зависит смысл работы сервиса — например, убедиться, что каталог не пустой.

А вот в роли “я на старте запускаю большой процесс на 30 секунд, который ходит по сети и читает файлы в пяти местах” runner уже выглядит сомнительно. Не потому что «нельзя», а потому что вы ухудшаете и усложняете старт. Сегодня мы хотим наоборот: сделать старт наблюдаемым и понятным.

Короткое правило, которое помогает не ошибиться: если вы описываете runner словами “на старте приложение делает вот эту одну вещь и затем продолжает жить”, то вы на правильном пути. Если вы ловите себя на мысли “runner — это почти отдельная подсистема приложения”, значит вы уже перебрали.

Стиль runner-логики в catalog-service

Когда вы пишете runner в учебном проекте, очень легко случайно сделать из него «коробку, куда складываем всё непонятное». Вроде бы логика старта — значит, давайте туда инициализацию, проверки, печать параметров, “быстрые фиксы”, и ещё чуть-чуть, и… всё, у вас появился класс на 300 строк, который ломает запуск от одного лишнего пробела. Поэтому сейчас зафиксируем стиль, который мы будем держать в catalog-service.

Во-первых, runner должен быть явно назван. Даже если он временный. RawArgsRunner — нормально для учебного эксперимента. Для проектного кода лучше что-то вроде StartupSummaryRunner, CatalogSeederRunner, StartupChecksRunner — так вы читаемостью покупаете себе спокойствие. Во-вторых, держите runner в логичном месте. Для нашего проекта это пакет com.example.catalogservice.catalog.bootstrap: там будут жить “одноразовые” вещи, которые участвуют в старте (bootstrap — это как раз «поднять и подготовить к жизни»).

В-третьих, runner должен делать одну вещь. Если вы хотите и печатать сводку, и проверять критичные условия, и что-то ещё — это повод либо разделить на два компонента, либо (чаще) пересмотреть, не прячете ли вы настоящую архитектурную проблему под ковёр “стартового кода”. Здоровый стиль здесь простой: короткий runner, аккуратные проверки и никакой “второй фазы приложения” на старте.

И наконец, очень практичный нюанс: runner — это бин, поэтому его конструктор должен быть скучным. Конструктор — только для зависимостей. Никакой логики уровня “а давайте тут сразу что-то посчитаем/прочитаем/запустим” в конструкторе быть не должно. Любое действие — в run(...), потому что именно оно явно говорит: “вот это стартовая логика”.

6. Типичные ошибки при работе с runners

Первые ошибки с runners обычно происходят не из-за «плохого программиста», а из-за старых привычек: консольные приложения, ручное управление объектами, желание быстро “проверить одну штуку” и забыть. Но Boot как платформа очень чувствителен к тому, где вы разместили код — поэтому лучше эти грабли заметить заранее.

Ошибка №1: писать стартовую логику в main() и руками создавать зависимости.
Так вы очень быстро превращаете Boot-приложение в смесь “немного Spring” и “немного самодельного DI”. При этом появляются странные дубли: часть объектов создаёт Spring, часть вы создаёте сами. Правильнее оставить main() максимально коротким и выносить одноразовую стартовую логику в runner, который является Spring-бином и может получать зависимости через конструктор.

Ошибка №2: дублировать одно и то же поведение в нескольких runners «на всякий случай».
Иногда появляется два компонента, которые оба печатают аргументы запуска, оба выводят “started”, оба делают похожие проверки. Это создаёт шум и путаницу: вы больше не знаете, какой из них “главный”, и почему сообщения в консоли повторяются. Старайтесь держать один runner на одну понятную задачу, а временные учебные runners удалять или объединять.

Ошибка №3: прятать стартовую работу в конструкторе бина.
Конструктор вызывается в момент создания бина, и если вы туда добавите логику, она будет выполняться “по пути”, без явного места в жизненном цикле. Это усложняет отладку: вы видите ошибку старта, но непонятно, в какой фазе она случилась. Пусть конструктор только принимает зависимости, а реальная стартовая логика живёт в run(...).

Ошибка №4: делать runner слишком длинным и непредсказуемым.
Runner должен быть коротким, потому что он стоит на критическом пути старта. Если внутри начинается “подключимся к этому”, “прочитаем то”, “посчитаем вот это”, вы получите медленный и хрупкий запуск. Особенно неприятно, когда запуск иногда быстрый, а иногда медленный — такие проблемы потом очень тяжело ловить. Лучше несколько маленьких, ясных действий, чем один «стартап-монстр».

Ошибка №5: относиться к runner как к месту для “фоновых задач”.
Runner выполняется один раз при старте и в том же потоке, который завершает startup sequence. Это не “планировщик” и не “фоновый воркер”. Если вам нужно что-то периодическое или асинхронное — это вообще другой разговор (и явно не тема сегодняшнего дня). Здесь мы держим фокус: runners — это про одноразовую стартовую логику и раннюю диагностику, а не про “жизнь приложения после старта”.

1
Задача
Spring Boot, 6 уровень, 0 лекция
Недоступна
Счётчик аргументов через CommandLineRunner
Счётчик аргументов через CommandLineRunner
1
Задача
Spring Boot, 6 уровень, 0 лекция
Недоступна
Стартовое сообщение через ApplicationRunner
Стартовое сообщение через ApplicationRunner
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ