1. Restart, reload і плутанина
Коли ви вивчаєте Spring Boot, ви постійно щось змінюєте: один метод у сервісі, один рядок у YAML, один заголовок на landing-сторінці. І майже завжди в голові звучить одне запитання: «Чому я це змінив, а в браузері, логах чи відповіді API все залишилося як було?». Це не лінь і не «руки не з того місця», а цілком нормальна ситуація, коли бракує моделі перезапуску.
Якщо спростити до побутової аналогії, початківець-розробник часто очікує, що Java-застосунок поводитиметься як документ у Google Docs: друкуєте — і «воно саме оновилося». Але Spring Boot — це не текстовий редактор. Це сервіс, у якого є життєвий цикл: старт, створення ApplicationContext, створення бінів, підняття embedded-сервера, обробка запитів. А DevTools — це «прискорювач», який допомагає проходити цей життєвий цикл частіше й швидше, але не скасовує його.
У цій лекції ми домовимося про терміни та поведінку. Restart — це коли ваш Spring-контекст справді створюється заново. Reload — це коли контекст не створюється заново, але ви все одно бачите оновлення результату, зазвичай тому, що змінився ресурс, який читається «на льоту». Якщо ви почнете розрізняти ці два режими, кількість загадкових ситуацій на кшталт «я вже 10 разів зберіг файл, чому нічого не змінилося?» різко зменшиться.
Три рівні оновлення
У розробці Boot-сервісу корисно тримати в голові не дві, а три різні «перезавантаження». Інакше легко потрапити в пастку: ви думаєте, що у вас був restart, а насправді відбулося лише оновлення сторінки в браузері, або навпаки. DevTools перебуває рівно посередині: він прискорює перезапуск застосунку, але не робить його «безсмертним».
Нижче — таблиця, яку варто подумки діставати щоразу, коли ви щось змінили й чекаєте на ефект:
| Що ви робите | Що реально оновлюється | Приклад у catalog-service | Що зазвичай видно |
|---|---|---|---|
| Reload (оновлення сторінки/ресурсу) | Контекст не створюється заново, застосунок продовжує працювати | Змінили static/index.html і натиснули оновити сторінку | Немає нових startup-логів, порт той самий, а в браузері просто інший HTML |
| DevTools restart | ApplicationContext створюється заново, біни створюються знову, сервер піднімається ще раз, але в межах того самого процесу JVM | Змінили Java-код сервісу або YAML у resources | У логах знову видно старт, знову виконуються раннери, @PostConstruct тощо |
| Повний перезапуск процесу (Stop/Run ще раз) | JVM-процес завершується, і стартує новий | Змінили build.gradle.kts, додали dependency або змінили версію | Усе стартує «з нуля»: новий процес, нова JVM, повний прогрів |
У цій таблиці є один важливий психологічний момент. DevTools restart відчувається як «майже магія», тому що він швидкий, але це все одно перезапуск Spring-застосунку. Якщо ви це приймете, вам буде простіше пояснювати собі дивні ефекти: чому повторно виконується StartupSummaryRunner, чому створюється заново in-memory репозиторій, чому деякі значення знову читаються з YAML.
2. Що робить DevTools restart у Spring Boot
Коли кажуть «DevTools перезапускає застосунок», хочеться уточнити: «а що саме він перезапускає?». Бо в голові новачка «застосунок» часто дорівнює або «tomcat», або «контролери». Насправді DevTools працює на рівні Spring ApplicationContext, а контекст — це «серце» Boot-застосунку, яке ми вже обговорювали в огляді Spring Core refresher.
Restart у DevTools — це приблизно такий сценарій: DevTools помічає важливу зміну, закриває поточний ApplicationContext (що призводить до @PreDestroy і зупинки інфраструктури), а потім створює новий контекст і запускає його так, ніби ви знову викликали SpringApplication.run(...). Усе це відбувається без «вбивства» самого JVM-процесу — тому швидше, ніж повноцінний Stop/Run.
Ось схематично, у якому порядку це зазвичай сприймається:
flowchart TD
%% Ви зберегли файл
A[Ви зберегли файл] --> B[Зміна потрапила до classpath]
%% DevTools реагує на зміни в classpath (зокрема на нові .class)
B --> C[DevTools запускає restart]
%% Контекст закривається коректно: викликаються destroy-callbackʼи і звільняються ресурси
C --> D[Закриття старого ApplicationContext]
%% Потім створюється новий контекст, як під час звичайного старту застосунку
D --> E[Створення нового ApplicationContext]
E --> F[Створення бінів і автоконфігурація]
F --> G[Підняття вбудованого сервера]
%% Після цього застосунок знову готовий приймати запити
G --> H[Застосунок знову готовий]
І тут важливо згадати про ваш catalog-service. У проєкті обов’язково є startup-код: наприклад, StartupSummaryRunner, сідер каталогу, можливо, якісь перевірки конфігурації. Під час кожного restart вони виконуватимуться заново, тому що контекст справді новий.
Щоб не бути голослівним, ось маленький фрагмент, який ви могли додати раніше або вже додали, і який дуже наочно показує «перезапуск контексту»:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component // Реєструємо runner як бін, щоб він виконувався під час старту контексту
public class StartupSummaryRunner implements ApplicationRunner {
// Логер зручно використовувати як «маячок» перезапуску
private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);
@Override
public void run(ApplicationArguments args) {
// Це повідомлення буде видно під час звичайного старту і під час DevTools restart
log.info("StartupSummaryRunner виконано");
}
}
Якщо ви змінюєте Java-код, і DevTools робить restart, то знову побачите це повідомлення в логах. Це дуже зручний «маячок»: ви відразу розумієте, що був саме restart, а не просто оновлення сторінки.
Ще один важливий нюанс для початківців. Restart означає, що весь in-memory стан, який живе в бінах, буде скинуто. Якщо ваш InMemoryCourseCatalogRepository зберігає список курсів у полі, то під час restart він створиться заново і знову заповниться. Для нашого навчального read-only сервісу це навіть добре: він має вміти стартувати чисто й передбачувано.
3. Reload із DevTools: ресурси vs Java
Слово «reload» у розробці люблять використовувати абияк, тому ми акуратно уточнимо, що саме маємо на увазі сьогодні. У межах нашої лекції та нашого курсу reload — це ситуація, коли ви змінюєте файл, але DevTools не створює контекст заново, і ви бачите нову поведінку або новий контент без повторного старту застосунку.
Найтиповіший приклад у catalog-service — статичні ресурси. У нас є landing page у src/main/resources/static/index.html. Цей файл віддається як звичайний статичний контент. І якщо DevTools налаштовано за замовчуванням, а в більшості випадків так і буде, зміна HTML зазвичай не має призводити до restart контексту. Ви змінюєте HTML, оновлюєте сторінку в браузері — і бачите нове. Контекст при цьому не створювався заново.
Простий приклад — і так, це той випадок, коли ви на п’ять секунд відчуваєте себе фронтендером:
<!-- src/main/resources/static/index.html -->
<!-- Статичний ресурс: зазвичай змінюється без restart контексту -->
<h1>Сервіс каталогу</h1>
<!-- Перевірте кеш браузера, якщо після reload текст не оновився -->
<p>Локальний режим</p>
Змінили <p>Локальний режим</p> на <p>Локальний режим (DevTools)</p>, зберегли, оновили сторінку. Якщо ви не бачите нового тексту, це найчастіше не «DevTools зламаний», а банальний кеш браузера, про який поговоримо в нюансах нижче.
Важливо не переплутати: reload не означає, що Spring «підхопив нові біни» або «перечитав конфігурацію». Контекст не створювався заново, отже всі біни як були, так і залишилися. Reload — це найчастіше про те, що нова версія файлу віддається клієнту під час наступного запиту.
Так, у Spring Boot DevTools є окрема можливість LiveReload — сервер, який може попросити браузер оновитися автоматично, — але в межах цієї лекції нам не потрібен цей зоопарк. Для нас достатньо простого й чесного механізму: змінили ресурс → оновили сторінку → побачили нове, без restart.
4. Два classloaderʼи в DevTools
Слово classloader звучить так, ніби зараз буде лекція на тему «як улаштована JVM на рівні космічних технологій». Спойлер: не буде. Але базове розуміння classloader усе-таки потрібне, інакше DevTools здаватиметься магічним гномом, який живе в IDE та ображається, якщо ви на нього дивитеся.
ClassLoader у JVM — це об’єкт, який відповідає за завантаження класів, тобто байткоду, в пам’ять. Можна думати про нього як про «бібліотекаря»: коли коду потрібен клас CourseCatalogService, бібліотекар шукає його в classpath, завантажує і каже JVM: «Ось, тримай».
DevTools використовує ідею «розділимо світ на стабільне і те, що змінюється». Стабільне — це залежності: Spring Framework, Boot, Jackson, Tomcat. Вони майже не змінюються, поки ви просто правите свій код. Те, що змінюється, — це ваш код: com.example.catalogservice.*.
Звідси й з’являється модель із двома classloaderʼами:
- base classloader завантажує «стабільні» речі, насамперед залежності,
- restart classloader завантажує ваш код застосунку, який змінюється постійно.
І виглядає це приблизно так:
flowchart LR
%% Базовий ClassLoader живе довше і зазвичай не створюється заново під час restart DevTools
Base["Базовий ClassLoader Spring / Tomcat / Jackson"] --> Restart["Restart ClassLoader com.example.catalogservice.*"]
%% Restart ClassLoader створюється заново повністю під час змін у коді застосунку
Тепер ключовий трюк DevTools. Коли ви змінюєте код, DevTools не намагається «переробити» вже завантажені класи, бо це була б справжня магія й біль. Він робить простіше: викидає restart classloader цілком і створює новий. Разом із ним зникають усі класи, завантажені цим classloaderʼом, а отже й увесь ваш застосунок як набір класів. Потім він піднімає контекст заново, але залежності залишаються завантаженими base classloaderʼом, і час на їх повторне завантаження не витрачається.
Ось чому DevTools restart зазвичай швидший за повний перезапуск процесу: ви не запускаєте нову JVM і не завантажуєте заново «пів всесвіту» у вигляді залежностей. Ви перескладаєте лише «свій шар».
Звідси випливає один дуже практичний висновок для початківців. Якщо ви додали залежність у build.gradle.kts (наприклад, новий starter), ви фактично змінили саме «base шар» — а DevTools restart його не переініціалізує коректно. У таких випадках зазвичай потрібен повний перезапуск процесу, тобто зупинити застосунок і запустити його знову, тому що змінилася фундаментальна структура classpath.
5. Приклади в catalog-service
Після всієї теорії хочеться чогось дуже практичного: «Окей, а в моєму проєкті що буде?». Гарна новина в тому, що у catalog-service дуже навчальний набір змін: Java-код, YAML-конфіг і статичний HTML. На них майже ідеально видно різницю між restart і reload.
Зробимо три типові сценарії й заздалегідь передбачимо реакцію.
Сценарій A: змінюємо Java-код (майже завжди restart).
Наприклад, у вас є сервіс, який використовує CatalogProperties, і ви змінюєте логіку або навіть просто повідомлення в логах. Щойно класи буде перекомпільовано і з’явиться новий .class у вихідному каталозі, DevTools побачить зміну classpath і ініціює restart.
Мініфрагмент, прив’язаний до нашої архітектури, — constructor injection, typed config:
import com.example.catalogservice.config.CatalogProperties;
import org.springframework.stereotype.Service;
@Service // Це Spring-бін: створиться заново під час DevTools restart
public class CourseCatalogService {
private final CatalogProperties properties;
// Constructor injection: залежності фіксуються під час створення біна
public CourseCatalogService(CatalogProperties properties) {
this.properties = properties;
}
public int featuredLimit() {
// Значення приходить з @ConfigurationProperties (зазвичай оновиться лише після restart)
return properties.maxFeaturedCount();
}
}
Якщо ви змінюєте метод featuredLimit() або будь-який інший Java-код, очікуйте restart: у логах знову буде старт, знову виконаються раннери, і лише після цього ви побачите нову поведінку під час запиту, наприклад до /api/catalog/featured.
Сценарій B: змінюємо YAML у src/main/resources (зазвичай restart).
Конфігурація в Boot читається на старті й перетворюється на Environment, а потім на @ConfigurationProperties. Це не динамічний «живий об’єкт», який сам змінюється під час редагування файлу. Тому, щоб застосувати зміни YAML, вам потрібен новий запуск контексту. DevTools допомагає зробити це швидше.
Типовий фрагмент:
# src/main/resources/application-local.yaml
# Конфіг профілю local: зміни застосуються після restart контексту
app:
catalog:
title: "Spring+ Catalog Local"
# Це значення «вшивається» в CatalogProperties під час старту застосунку
max-featured-count: 3
Змінюєте max-featured-count, і після restart ваш endpoint /api/catalog/featured почне віддавати іншу кількість карток. Але ключове слово тут — після restart, а не «прямо зараз».
Сценарій C: змінюємо статичний ресурс (зазвичай лише reload).
HTML, картинки, CSS у static — це той випадок, де контекст найчастіше не повинен перезапускатися. Ви просто оновлюєте сторінку й бачите новий результат. Startup-логів при цьому не буде.
Ці три сценарії дають вам базову навичку: перш ніж панікувати, ви запитуєте себе: «Я змінив код? конфіг? ресурс?». І вже за цим знаком приблизно розумієте, чи чекати restart.
6. Нюанси: компіляція, кеш, оточення
Про DevTools можна сказати багато хорошого, але одна річ про нього особливо важлива: він не читає ваших думок. Він реагує на фактичні зміни файлів, які потрапляють туди, куди він дивиться. А далі в гру вступають IDE, компіляція і кеш — три причини, чому у вас «не працює», хоча у викладача «зараз покажу — і все працює».
Найчастіша ситуація — ви змінили файл .java, зберегли, але DevTools не зробив restart. Причина зазвичай прозаїчна: у вихідному каталозі ще немає нового .class, бо код не було перекомпільовано або IDE налаштовано так, що вона компілює лише за явної дії. DevTools стежить за змінами класів і ресурсів на classpath, а не за вашим вихідним кодом як текстом. Тому «зберіг .java» ≠ «з’явився новий .class». Щойно компіляція відбудеться, restart почне поводитися передбачувано.
Друга вічна тема — кеш браузера. Навіть якщо DevTools правильно не рестартить застосунок під час зміни index.html, браузер може вперто показувати стару версію сторінки. DevTools зазвичай допомагає, відключаючи або послаблюючи кешування деяких ресурсів у режимі розробки, але браузер — створіння з характером. Якщо ви змінили HTML і не бачите змін, спробуйте hard reload, наприклад Ctrl+F5, і лише потім звинувачуйте Spring.
Третя ситуація — ви змінили щось «навколо» застосунку, наприклад build.gradle.kts, або підтягнули залежності, або змінили версію. Це вже не «restart classloader», а зміна classpath на рівні залежностей. У таких випадках DevTools restart може бути недостатнім: залежності завантажуються базовим classloaderʼом, і коректніше зробити повний перезапуск процесу. Це нормально: DevTools не обіцяв, що він магічно змінюватиме фундамент застосунку на льоту.
Якщо тримати ці три нюанси в голові, DevTools перестає бути випадковою магією і стає звичайним інженерним інструментом: «Я розумію, що він спостерігає, і розумію, чому він не спрацював».
7. Типові помилки під час роботи з restart/reload
Помилка № 1: очікування «гарячої заміни» Java-коду без restart.
Іноді здається, що DevTools має «вкрутити» зміни в уже працюючі об’єкти, ніби Java раптом стала JavaScript. На практиці DevTools робить чесний restart контексту. Якщо ви бачите, що після зміни коду нічого не змінилося, найчастіше проблема не в DevTools, а в тому, що не відбулося створення контексту заново або не було компіляції.
Помилка № 2: плутанина між «оновив сторінку» та «застосунок перезапустився».
Дуже легко обманутися: ви оновили сторінку, побачили новий HTML і думаєте, що «DevTools усе перезапустив». Але якщо в логах не було нового старту і ваш StartupSummaryRunner не відпрацював заново, значить контекст не створювався заново. Це важливо, тому що YAML-зміни і Java-зміни без restart не застосуються, скільки сторінку не оновлюй.
Помилка № 3: зберігання важливого стану в пам’яті й здивування, що його «скидає».
DevTools restart створює біни заново. Якщо ви зберігаєте «важливий» in-memory стан у полях сервісів або репозиторіїв, під час restart він зникне. Для нашого catalog-service це нормально, бо він read-only, але як звичка — небезпечно. Якщо вам потрібен стан «довше, ніж один контекст», отже це вже інше архітектурне завдання, і точно не завдання DevTools.
Помилка № 4: зміна YAML не в тому профілі й очікування ефекту.
Якщо ви запускаєте профіль local, а правите application-dev.yaml, жоден DevTools вас не врятує: ви чесно змінили файл, який у цьому запуску не використовується. Це не «DevTools не підхопив», це «застосунок ніколи не читав цей файл». Тому перше, що варто перевірити під час будь-яких конфігураційних сюрпризів, — активний профіль.
Помилка № 5: зміна залежностей і очікування, що DevTools «підхопить» їх як звичайний restart.
Додали starter, оновили Gradle, а застосунок продовжує поводитися так, ніби залежності немає? Це класика. DevTools restart не дорівнює повному перезапуску процесу. Коли змінюється classpath залежностей, майже завжди краще зупинити застосунок і запустити його знову, щоб базовий шар завантажився коректно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ