1. Межа рестарту та зайві перезапуски
Коли DevTools увімкнено, він намагається бути корисним: бачить зміну — реагує. Але телепатією він не володіє: не знає, що ви лише виправили текст у README.md, а не змінювали бізнес-логіку. Тому застосунок може перезапускатися надто часто. Саме тут і потрібне керування шляхами: воно допомагає зробити так, щоб перезапуск відбувався лише тоді, коли він справді потрібен.
Уявіть, що ваш catalog-service — це кафе. Restart — це ніби закрити кафе, вигнати всіх гостей, знову увімкнути кавомашину, заново скласти меню, повісити вивіску й відкрити двері. Іноді це виправдано, якщо ви змінили меню або кухню. Але якщо ви просто поправили описку на табличці «Вхід», закривати кафе на п’ять хвилин — дуже дивна бізнес-модель.
Технічно проблема виглядає так: DevTools стежить за змінами у файлах, що потрапляють у «зону уваги» (зазвичай це classpath проєкту). Будь-яка зміна там потенційно може спричинити restart. Наша мета — зробити межу передбачуваною: зрозуміти, які зміни викликають restart, які дають reload, а які взагалі можна ігнорувати.
2. Типові зміни DevTools
Розуміння типових правил DevTools — це як знати правила дорожнього руху. Без них теж можна… але тоді ви постійно дивуєтеся, чому навколо всі сигналять. DevTools насамперед орієнтується на зміни в classpath: туди потрапляють скомпільовані класи та ресурси (src/main/resources), які IDE/Gradle копіюють у вихідний каталог збирання. Саме такі зміни найчастіше й вважаються достатньо важливими.
Давайте прив’яжемо це до реальності нашого проєкту. У catalog-service є три основні групи змін. Перша — Java-код: контролери, сервіси, репозиторій, конфігураційні класи. Друга — конфігурація (application.yaml, application-local.yaml, catalog-data.yaml тощо). Третя — ресурси, які не впливають на біни напряму, наприклад static/index.html. І є ще четверта, небажана категорія: файли, що лежать у ресурсах, але не використовуються застосунком — документація, заготовки, .http-файли. Вони можуть випадково запускати рестарт і псувати вам життя.
Нижче — коротка карта для орієнтиру, не абсолютна істина, а практична підказка.
| Що ви змінили | Де зазвичай лежить | Чи має це викликати restart? | Чому |
|---|---|---|---|
| *.java | src/main/java → build/classes | Майже завжди так | Змінюються wiring, біни, логіка |
| application-*.yaml, catalog-data.yaml | src/main/resources → build/resources | Зазвичай так | Зв’язування конфігурації відбувається під час запуску |
| static/index.html | src/main/resources/static | Зазвичай ні; тут хочеться reload | Це «вітрина», а не «двигун» |
| docs/** | src/main/resources/docs | Ні | Не впливає на застосунок, але може запустити рестарт |
Саме через останній рядок ми й вчимося керувати шляхами: інакше DevTools чесно робить restart на зміни, які взагалі не повинні змінювати поведінку сервісу.
У DevTools для цього є три різні ручки керування restart. Їх не треба механічно складати всі одразу в один application-local.yaml: у звичайному локальному режимі вони можуть і не знадобитися. Налаштування обирають під конкретне джерело зайвих перезапусків або під зовнішній каталог, за яким потрібно стежити.
3. spring.devtools.restart.exclude: повний контроль
Іноді хочеться сказати DevTools: «Любий, я сам вирішу, що важливо, а що ні». Для цього й існує spring.devtools.restart.exclude. Але в цієї опції є характер: вона не додає нові виключення, а заміщує список цілком. Тобто ви берете на себе роль людини, яка тепер повністю відповідає за правила перезапуску. Це нормально — якщо ви готові потім пояснити собі, чому все поводиться інакше.
У навчальному проєкті exclude найчастіше потрібен не для того, щоб «зробити по-своєму», а для демонстрації механіки. У реальному житті новачок часто використовує exclude як молоток: «рестартів занадто багато — виключу все». Потім дивується, чому він змінює код, а застосунок ніби не реагує. У цей момент зазвичай і народжуються легенди про «глючний Spring».
Мінімальний приклад — суто як ілюстрація механіки — може виглядати так:
# src/main/resources/application-local.yaml
spring:
devtools:
restart:
# Виключаємо з restart те, що зазвичай хочеться оновлювати без повторного створення контексту.
# Важливо: exclude заміщує типові виключення DevTools, а не доповнює їх.
exclude: "static/**,public/**"
Тут ми прямо кажемо: зміни в static/** і public/** не мають викликати рестарт. Але є важливий нюанс: якщо раніше DevTools уже виключав щось інше, наприклад інші папки ресурсів, то за такого підходу ви можете це випадково загубити, і поведінка зміниться.
Тому exclude краще сприймати як режим «я налаштовую правила цілком». Він корисний, коли ви точно знаєте, що робите, або коли хочете зробити поведінку максимально передбачуваною й однаковою для всієї команди — і готові це підтримувати.
4. spring.devtools.restart.additional-exclude
Якщо exclude — це «знести старий дім і побудувати новий», то additional-exclude — це «додати ще одну полицю й не чіпати несучі стіни». У більшості навчальних і невеликих проєктів саме це й потрібно: DevTools уже має розумні значення за замовчуванням, а ви додаєте кілька виключень під свій репозиторій, щоб не ловити зайві перезапуски.
У catalog-service типова ситуація звучить так: «У мене в проєкті є папка з документацією або файлами запитів, і я не хочу, щоб редагування цих файлів перезапускало застосунок». Наприклад, ви зберігаєте навчальні матеріали в src/main/resources/docs/ — не найкращий варіант з архітектурного погляду, але в житті таке буває.
Тоді логічніше зробити так:
# src/main/resources/application-local.yaml
spring:
devtools:
restart:
# Додаємо власні виключення, не чіпаючи типові правила DevTools.
# Тут ми захищаємося від зайвих рестартів через документацію та .http-файли.
additional-exclude: "docs/**,**/*.http"
Сенс простий: типові виключення DevTools залишаються, а ви додаєте ще два свої. І тут з’являється важлива інженерна риса: ви можете пояснити, чому ці виключення додані. Це завжди добрий тест на адекватність налаштування.
У результаті ваш цикл зворотного зв’язку стає рівнішим: ви можете спокійно редагувати документацію та приклади запитів, не спостерігаючи кожні 20 секунд перезапуск вбудованого Tomcat, який уже починає підозрювати, що його тримають у заручниках.
5. Шаблони шляхів для exclude та additional-exclude
Шляхи в exclude та additional-exclude — це маленька мова шаблонів. І, як у будь-якій маленькій мові, найбільше проблем приносить не складність, а самовпевненість. Здається: «Ну я написав * — значить, усе». А потім раптово виключається не те, що ви очікували, і DevTools поводиться, як кіт: дивиться в очі й удає, що нічого не сталося.
На практиці в середовищі Boot найчастіше працює логіка в стилі Ant із шаблонами. Ідея проста: ви описуєте маску файлів, зміни в яких не мають викликати restart. У конфігу зазвичай пишуть кілька масок через кому. Важливо не забувати, що пробіли в рядку іноді стають частиною значення, тому краще писати акуратно, без «красивих» пробілів.
Нижче — мінітаблиця смислів, щоб мозок не намагався щоразу винаходити велосипед:
| Шаблон | Інтуїтивний зміст | Практичний приклад |
|---|---|---|
| static/** | усе всередині static/ на будь-якій глибині | static/index.html, static/css/app.css |
| docs/** | усе в папці документації | docs/guide.md, docs/img/logo.png |
| **/*.http | усі файли .http будь-де | http/catalog-service.http (якщо він на classpath) |
| public/** | усе в public/ | аналогічно до static/ |
З цим варто бути обережними: якщо ви напишете занадто широкий шаблон, наприклад **/* або **/*.yaml, ви можете випадково вимкнути restart для важливих речей на кшталт конфігурації або класів. Далі доведеться налагоджувати не застосунок, а власне налаштування DevTools. Це доволі мета-досвід, але зазвичай він трапляється не за планом і радості не додає.
6. spring.devtools.restart.additional-paths: зовнішні папки
До цього моменту ми говорили переважно про те, що лежить усередині проєкту й потрапляє на classpath. Але наш catalog-service уже живе у світі externalized configuration: ми можемо читати конфіг не лише з src/main/resources, а й із зовнішніх файлів, наприклад із ./config/ поруч із jar/проєктом. І тут DevTools без підказки може не помітити зміну: файл змінили, а restart не відбувається, бо DevTools його не відстежує.
spring.devtools.restart.additional-paths — це як сказати DevTools: «Ось ще одна зона, за якою потрібно наглядати». Це особливо природно в нашому курсі, тому що ви вже знаєте spring.config.import, spring.config.additional-location і взагалі ідею зовнішніх конфігів.
Приклад налаштування виглядає просто:
# src/main/resources/application-local.yaml
spring:
devtools:
restart:
# Говоримо DevTools стежити за зовнішньою папкою (поруч із проєктом/запуском).
# Якщо в ній зміниться файл, DevTools зможе ініціювати restart.
additional-paths: "./config"
Тепер, якщо ви редагуєте файл у ./config, DevTools може побачити зміну й ініціювати restart. А restart нам і потрібен, бо конфігурація (через @ConfigurationProperties) схоплюється під час запуску контексту. Без перезапуску ви зазвичай не побачите нових значень і подумаєте, що YAML вас ігнорує, хоча насправді ви просто не перезапустили контекст.
Якщо весь конфіг живе всередині проєкту й вам немає за чим стежити у зовнішньому каталозі, additional-paths тут просто не потрібен. Це окрема ручка для конкретного сценарію, а не обов’язкове продовження налаштувань exclude.
Щоб не залишати тему у вакуумі, зведімо все в одну зрозумілу картину саме для catalog-service. За ТЗ проєкту в нас допустимий зовнішній конфіг, наприклад ./config/catalog-extra.yaml. Щоб застосунок узагалі читав такий файл із довільною назвою, його простіше явно імпортувати через уже знайомий spring.config.import. А щоб DevTools реагував на зміни в цій же папці, додаємо additional-paths.
У локальному профілі це може виглядати так — коротко і по суті:
# src/main/resources/application-local.yaml
spring:
config:
# Підключаємо зовнішній файл як джерело конфігурації.
# Для довільної назви файла тут зручніше використовувати import.
import: "optional:file:./config/catalog-extra.yaml"
devtools:
restart:
# Підключаємо спостереження DevTools за цією ж папкою (сам факт відстеження змін).
additional-paths: "./config"
А сам зовнішній файл може перевизначати частину наших налаштувань app.catalog.*, наприклад заголовок (згадуємо: CatalogProperties — типізована модель конфігурації):
# ./config/catalog-extra.yaml
app:
catalog:
# Приклад значення, яке зручно змінювати без повторного збирання проєкту.
# Але щоб воно застосувалося, усе одно знадобиться restart контексту.
title: "Catalog Service (зовнішня конфігурація)"
Чому ця зв’язка важлива методично? Ми не просто пришвидшуємо перезапуск. Ми робимо так, щоб ваша конфігураційна модель з попереднього модуля (profiles, imports, external locations, @ConfigurationProperties) нормально жила в dev-режимі. Інакше виникає кумедна — і дуже поширена — ситуація: конфіг ви вже винесли назовні, але змінювати його незручно, бо «щось не застосовується». DevTools якраз допомагає зробити цей сценарій приємним, але лише якщо ви правильно описали межі спостереження.
7. Правило: коли потрібен restart
У реальній розробці вам рідко потрібно пам’ятати назви всіх властивостей. Важливіше швидко відповісти на запитання: «Ця зміна має знову створювати контекст чи ні?». Якщо правило занадто складне, ви все одно почнете діяти навмання, а DevTools виглядатиме як нестабільна магія. Тому корисно тримати в голові дуже простий алгоритм.
Нижче — мінісхема ухвалення рішення. Вона не про те, що DevTools фактично зробить у кожному випадку, а про те, як вам думати і як обирати налаштування exclude / additional-exclude / additional-paths.
flowchart TD
A["Файл змінився"] --> B{"Чи впливає це на біни / wiring / звʼязування конфігурації?"}
B -- "Так" --> C["Потрібен restart: контекст слід створити заново"]
B -- "Ні" --> D{"Це статичний ресурс, який просто віддається назовні?"}
D -- "Так" --> E["Краще без restart: достатньо оновлення ресурсу"]
D -- "Ні" --> F["Ймовірно, це шум — його краще виключити з restart"]
Якщо перекласти схему людською мовою, вийде просте правило. Змінили код або конфіг, від якого залежить створення бінів і поведінка @ConfigurationProperties, — очікуємо restart і не боремося з ним. Змінили «вітрину» — HTML у static — хочемо оновлення без рестарту. Змінили файл, який узагалі не бере участі в роботі застосунку, — виключаємо його, щоб він не запускав перезапуск.
Це правило не лише економить час. Воно робить DevTools зрозумілим: ви не «налаштовуєте магію», а описуєте межі того, що справді важливо для повторного створення контексту.
8. Типові помилки під час роботи з DevTools
Коли початківець уперше отримує DevTools, він зазвичай проходить три стадії: захват, роздратування, «гаразд, давай налаштуємо». І саме на стадії налаштування найчастіше й народжуються помилки, які потім сприймаються як «DevTools глючить». На практиці майже завжди глючить не DevTools, а наша власна політика шляхів.
Помилка №1: використовувати exclude, коли достатньо additional-exclude.
Це виглядає невинно: ви просто хочете додати виключення, а берете й перезаписуєте весь список. Після цього DevTools раптово починає рестартити на зміни там, де раніше не рестартив, або навпаки — перестає реагувати на важливі ресурси. Якщо мета — «додати кілька папок», майже завжди правильніше additional-exclude, бо він не ламає типові правила.
Помилка №2: писати занадто широкі шаблони на емоціях.
Іноді людина бачить, що застосунок занадто часто рестартить, і додає щось на кшталт **/*.yaml або взагалі **/*, щоб «заспокоїти» DevTools. Через хвилину вона змінює application-local.yaml, а застосунок не змінює поведінку, бо рестарту більше немає. Виникає відчуття, що Spring «не читає конфіг». Насправді Spring читає, просто ви вимкнули собі механізм, який мав застосувати зміну.
Помилка №3: додавати additional-paths, але забувати, що застосунок має читати ці файли.
additional-paths — це спостереження, а не магічне підключення конфігурації. Якщо DevTools стежить за ./config, але сам файл не підключений через spring.config.import або інший відповідний механізм, то зміна може викликати restart, але на поведінці застосунку це ніяк не позначиться: файл просто не бере участі у збиранні конфігурації. І навпаки, якщо застосунок читає зовнішній конфіг, але DevTools за ним не стежить, ви отримуватимете «чому не застосувалося?» аж до першого ручного рестарту.
Помилка №4: виключити з restart те, що справді впливає на контекст.
Іноді хочеться, щоб catalog-data.yaml оновлювався «як статичний файл» без рестарту. Але в нашому курсі каталог курсів завантажується через типізовану конфігурацію (@ConfigurationProperties), а отже зміни в YAML мають приводити до повторного створення контексту або принаймні до рестарту механізму binding. Якщо ви виключите цей файл із restart, ви створите собі хибне очікування: «змінив YAML — і все застосувалося».
Помилка №5: зберігати налаштування DevTools не в local профілі, а в загальному application.yaml.
Так ви робите локальні зручності глобальними правилами. Потім вмикаєте інший профіль, віддаєте проєкт товаришу, запускаєте в іншому режимі — і раптом DevTools впливає на поведінку там, де він узагалі не потрібен. DevTools — це інструмент для локальної розробки, і його політика шляхів має бути локальною та явно прив’язаною до local профілю, щоб базова конфігурація проєкту залишалася чистою та переносною.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ