JavaRush /Курси /Spring Boot /Події життєвого циклу Boot: started, ready, failed

Події життєвого циклу Boot: started, ready, failed

Spring Boot
Рівень 6, Лекція 2
Відкрита

1. Завдання подій життєвого циклу в Boot

Якщо сприймати запуск застосунку як одну точку — «запустив, і воно стартувало», — події здаються дивною сутністю зі світу enterprise-чаклунства. Але щойно ви хоча б кілька разів потрапляєте в ситуацію «ніби стартує, та щось не так», ви починаєте цінувати будь-який зрозумілий поділ процесу на етапи. Це як аптечка в машині: поки не потрібна, здається зайвою.

Spring Boot живе не у вакуумі. Він проходить багато кроків: готує оточення, створює контекст, піднімає інфраструктуру, запускає вбудований сервер, якщо ви працюєте у веб-режимі, виконує ваші runners, і лише потім можна чесно сказати: «застосунок готовий». Ці кроки важливі не заради філософії, а заради практики. Коли ви бачите «готово», ви хочете розуміти, після чого саме настав цей стан.

Події життєвого циклу — це проста ідея: Boot у ключові моменти «стріляє» сигналом, тобто об’єктом-подією, і ви можете на нього підписатися. Уявіть собі театр. Іде вистава «Запуск застосунку». На сцені з’являються таблички: «Акт 1: контекст піднято», «Акт 2: runners відпрацювали», «Акт 3: усе впало, і всі розійшлися». Події — це такі таблички для коду. Вони допомагають перестати гадати, на якому ви етапі сюжету.

2. Події started, ready і failed

У Spring Boot є ще багато подій, зокрема й ранніх етапів, про які зручно знати в більш просунутих сценаріях. Але для нашого рівня і для сьогоднішньої задачі достатньо трьох найприкладніших і найлегше пояснюваних подій: ApplicationStartedEvent, ApplicationReadyEvent і ApplicationFailedEvent. Вони чудово розкладають старт на зрозумілі «до/після» і не вимагають глибокого занурення у внутрішності фреймворка.

Щоб не тримати в голові все одразу, давайте зведемо їх у таблицю. У ній важливо не зубрити формулювання, а зрозуміти зміст: що вже зроблено на цьому етапі, а що ще ні.

Подія Коли відбувається Що вже відбулося Що ще не відбулося Чим корисна
ApplicationStartedEvent Після оновлення (refresh) контексту ApplicationContext створено й оновлено, біни вже існують Ваші CommandLineRunner / ApplicationRunner ще не відпрацювали Зрозуміти «контекст піднято», поставити ранні маркери, провести легку діагностику
ApplicationReadyEvent Після виконання runners Стартова логіка runners завершилася Нічого «обов’язкового» не залишилося, застосунок вважається готовим Вивести «ми готові», запустити дуже легку постініціалізацію, подати сигнал «готово» для діагностики
ApplicationFailedEvent Коли старт завершився винятком Boot упіймав помилку старту й оформив її як подію Застосунок не піднявся й зазвичай завершується Отримати «останній шанс» зафіксувати причину падіння зрозумілим повідомленням

Ключовий момент лекції — у прямому сенсі ключовий: started і ready — це різні стани. У початківців часто є внутрішнє очікування: «якщо контекст піднято, значить усе готово». Але Boot спеціально показує межу: після started ви ще можете виконувати важливі одноразові дії через runners. І лише після них застосунок вважається ready.

3. Таймлайн старту

Коли в голові немає часової карти запуску, легко переплутати ролі механізмів: почати вкладати «головний стартовий код» у слухачі подій або, навпаки, намагатися все робити в main(). Зараз ми зберемо просту часову схему, щоб було зрозуміло, хто за ким іде та чого можна очікувати на кожному етапі.

Нижче — спрощена діаграма. Вона не намагається показати всі внутрішні стадії Boot, а демонструє лише те, що нам потрібно сьогодні: факт появи ApplicationContext, момент публікації ApplicationStartedEvent, далі runners, а потім ApplicationReadyEvent. Окремо позначено, що у разі винятку десь дорогою з’являється ApplicationFailedEvent.

flowchart TD
    A["main()"] --> B["SpringApplication.run(...)"]
    B --> C["Створення та оновлення ApplicationContext"]
    C --> D["Публікація ApplicationStartedEvent"]
    D --> E["Виконання ApplicationRunner / CommandLineRunner"]
    E --> F["Публікація ApplicationReadyEvent"]
    B -->|Виняток на старті| X["ApplicationFailedEvent"]
    C -->|Виняток на старті| X
    E -->|Виняток у runner| X

Тут є дуже практичний наслідок: listeners, тобто слухачі подій, і runners стоять дуже близько до фіналу запуску. Це добре, бо контекст уже піднято, залежності можна інжектити, а сервіси та репозиторії доступні в межах поточного каркаса. Але це й небезпечно: якщо ви випадково зробите в listener важку роботу, ви подовжите запуск або взагалі перетворите його на непередбачувану річ.

І ще один нюанс: за замовчуванням події обробляються синхронно. Тобто Boot публікує подію, слухачі виконуються в тому самому потоці, і доки вони не закінчать, далі за таймлайном застосунок не рушить. На рівні відчуттів це як «корок на старті»: один невдалий listener може затримати всіх.

4. Підписка на події через @EventListener

Слухачі подій у Spring звучать страшніше, ніж є насправді. На практиці це звичайний Spring-бін, усередині якого є метод, позначений @EventListener. Цей метод приймає параметром об’єкт події. Коли Boot публікує подію, Spring знаходить відповідні методи й викликає їх. Усе. Жодних шаманських танців навколо main(), жодного ручного «підпишіться на шину подій».

Почнемо з найпростішого й корисного: виведемо маркери на ApplicationStartedEvent і ApplicationReadyEvent. Тут System.out.println нам потрібен лише як простий ліхтарик, щоб наочно побачити порядок.

Зверніть увагу на два моменти. По-перше, клас має бути Spring-managed, тобто перебувати в зоні component scan і мати @Component. По-друге, методам не потрібно нічого повертати — вони просто реагують на подію.


import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Робимо клас Spring-біном, щоб Spring міг викликати обробники подій
public class StartupPhaseListener {

    @EventListener
    public void onStarted(ApplicationStartedEvent e) {
        // Спрацює, коли контекст уже піднято, але runners ще не запускалися
        System.out.println("[event] started"); // [event] started
    }

    @EventListener
    public void onReady(ApplicationReadyEvent e) {
        // Спрацює після виконання всіх runners — чесний маркер «застосунок готовий»
        System.out.println("[event] ready");       // [event] ready
    }
}

Це найкоротша вступна спроба. Поруч із наступним, більш явним order-demo, такий клас зазвичай не тримають: далі ми просто розкладемо той самий порядок за окремими ролями, щоб таймлайн було легше читати.

Це маленький, але важливий практичний крок: ви починаєте бачити стадії запуску саме в термінах Boot, а не лише за «потоком логів», який іноді нагадує водоспад. І, що приємно, це працює навіть у наймінімальнішому catalog-service, де ще немає веб-шару.

Тепер важливе правило, яке варто проговорити тут, поки ви не написали собі «StartupListener на 300 рядків». Listener — це не місце для важкої логіки. Він добре підходить для спостереження: маркерів, легкої діагностики, фіксування таймінгу. Але не для довгої роботи. Якщо хочеться зробити явну стартову дію, для цього в сьогоднішньому дні вже є runners.

5. Listener і Runner: ролі та порядок

На старті Boot дає два типи механізмів, і їх часто плутають: runner і listener. Здалеку вони схожі: і те, і те виконується на старті, і там, і там можна щось зробити. Але ролі в них різні. Runner — це місце, де ви робите одноразову стартову дію: коротко, явно, з можливістю інжектити залежності. Listener — це місце, де ви спостерігаєте життєвий цикл і реагуєте на переходи між фазами.

Найшвидший спосіб відчути різницю — поставити маркери й подивитися на порядок виводу. Щоб цей порядок було видно зовсім явно, тимчасово замінимо попередній спрощений listener на більш розкладений demo-набір. Ідея та сама, просто тепер кожна роль живе окремо, і таймлайн читається легше.

Зробимо три маленькі компоненти: один слухає started, другий — runner, третій слухає ready. У результаті ви побачите: started → runner → ready.

Слухач started:


import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Цей клас має бути біном, інакше метод-обробник ніхто не викличе
public class StartedMarker {

    @EventListener
    public void onStarted(ApplicationStartedEvent e) {
        // Маркер для перевірки: цей вивід має бути ДО runners
        System.out.println("[event] started (before runners)"); // [event] started (before runners)
    }
}

Маркер-runner:


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

@Component // Runner теж є Spring-біном і запускається на старті
public class MarkerRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // Тут місце для одноразової стартової роботи, яка має бути короткою й передбачуваною
        System.out.println("[runner] executed"); // [runner] executed
    }
}

Слухач ready:


import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Спостерігаємо момент, коли застосунок уже «встав на ноги»
public class ReadyMarker {

    @EventListener
    public void onReady(ApplicationReadyEvent e) {
        // Маркер для перевірки: цей вивід має бути ПІСЛЯ runners
        System.out.println("[event] ready (after runners)"); // [event] ready (after runners)
    }
}

Якщо запустити застосунок, то серед іншого ви побачите ці рядки саме в такому відносному порядку:


[event] started (before runners)
[runner] executed
[event] ready (after runners)

Чому це важливо? Тому що це знімає вічний біль новачка: «а де мені друкувати “готово”?». Якщо вам потрібне чесне «готово» після того, як ваша стартова логіка вже відпрацювала, то ApplicationReadyEvent — чесніший маркер, ніж ApplicationStartedEvent. А якщо вам потрібно зробити явну дію старту, то runner — добре місце. Listener — це ліхтар і датчик, runner — інструмент і дія.

І ще одна тонкість, про яку варто пам’ятати, щоб не дивуватися: якщо у вас кілька listeners на одну подію, Spring викличе їх усіх. Порядок виклику між кількома слухачами — окрема тема, і на початку курсу краще не будувати логіку, яка залежить від порядку виклику слухачів. Слухачі мають бути максимально незалежними й безпечними.

6. ApplicationFailedEvent і падіння під час запуску

Коли застосунок падає під час запуску, початківець зазвичай бачить величезний stack trace і відчуває два почуття: «чому він такий довгий» і «чому він такий страшний». Насправді найчастіше вам потрібна одна річ: зрозуміла причина падіння і місце, де цю причину можна акуратно зафіксувати. ApplicationFailedEvent — це якраз сигнал: «старт не завершився успішно».

Зробимо невеликий listener, який друкує тип винятку та його повідомлення. Це окремий діагностичний інструмент, а не обов’язковий сусід усіх маркерних компонентів вище. У production ви б логували це нормально, але зараз наша мета — побачити, що подія реально відбувається і що з неї можна дістати корисну інформацію.


import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Спробуємо упіймати й красиво підсвітити падіння під час запуску
public class StartupFailedListener {

    @EventListener
    public void onFailed(ApplicationFailedEvent e) {
        // Це «останній шанс» вивести щось зрозуміле до завершення застосунку
        System.out.println("[event] failed: " + e.getException().getMessage()); // [event] failed: ...
    }
}

Тут є важливе життєве зауваження. ApplicationFailedEvent — це last-chance подія, але вона все одно залежить від того, наскільки далеко встиг дійти Boot. Якщо застосунок падає зовсім рано, ще до підняття контексту, listener, який живе як @Component всередині контексту, може просто не встигнути з’явитися. На нашому поточному етапі, коли помилки частіше проявляються ближче до фіналу запуску, наприклад у runner або вже після створення бінів, цей механізм дуже корисний. Але не варто сприймати його як залізну гарантію, що ви перехопите будь-яке падіння у світі.

І ще одна пастка для новачка: іноді хочеться «перехопити помилку й продовжити». У контексті запуску це майже завжди погана ідея. Якщо Boot упав під час запуску, краще, щоб він упав чесно, ніж щоб ви отримали «напівживий застосунок», який ніби працює, але насправді вже зламаний усередині. Listener на failed — це місце, щоб зафіксувати й пояснити проблему, а не щоб зображати героїзм.

7. Типові помилки в подіях життєвого циклу

Помилка № 1: вважати ApplicationStartedEvent синонімом «застосунок готовий».
Це дуже поширена плутанина: студент побачив слово started і зробив висновок «ну раз стартувало — значить готово». Насправді started означає, що контекст уже піднято, але runners ще не відпрацювали. Якщо ви хочете виводити підсумковий маркер «ми готові» саме після вашої стартової логіки, більш доречним сигналом буде ApplicationReadyEvent.

Помилка № 2: перетворювати listener на прихований runner і виконувати в ньому важку роботу.
Listener виглядає як зручна точка: «ой, тут же можна написати код». І ось тут легко почати робити в @EventListener усе підряд: довгі обчислення, читання великих файлів, підготовку чогось важливого. Проблема в тому, що події обробляються синхронно, і ви буквально заморожуєте просування запуску далі. Якщо це дія, тобто реальна стартова задача, їй місце в runner, і вона має бути короткою й передбачуваною.

Помилка № 3: дублювати логіку в runner і в listener на ready.
Іноді розробник пише runner, який робить «стартову зведену інформацію», і паралельно пише listener на ApplicationReadyEvent, який робить «стартову зведену інформацію». Потім починається вічне життя багів: зведення друкується двічі, повідомлення роз’їжджаються, ніхто не розуміє, де «справжня правда». Хороший стиль на початку курсу — вибрати одне місце для однієї дії: runner для дії, listener для спостереження.

Помилка № 4: намагатися прив’язати бізнес-логіку до порядку виклику кількох listeners.
Навіть якщо у вас зараз усе «випадково працює», це дуже крихкий стиль. Слухачі мають бути максимально незалежними й не розраховувати, що «спочатку викличеться мій, потім твій». На старті курсу краще тримати listeners простими: друк маркера, легка діагностика, виведення зрозумілого повідомлення. Складні залежності між слухачами — майже завжди сигнал, що архітектура попливла.

Помилка № 5: очікувати, що ApplicationFailedEvent спрацює завжди й у всіх випадках.
Ця подія корисна, але вона не чарівна пастка для будь-яких падінь у Всесвіті. Якщо застосунок падає надто рано, коли контекст ще не створився, ваш @Component-listener може просто не існувати. Тому ApplicationFailedEvent — хороший інструмент, але не єдина опора: читайте повідомлення винятку, дивіться на верхівку stack trace і не бійтеся нормальної діагностики, а не лише «магічних перехоплень».

1
Задача
Spring Boot, 6 рівень, 2 лекція
Недоступна
Порядок фаз `started -> runner -> ready`
Порядок фаз `started -> runner -> ready`
1
Задача
Spring Boot, 6 рівень, 2 лекція
Недоступна
Повідомлення про `ApplicationFailedEvent`
Повідомлення про `ApplicationFailedEvent`
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ