1. Граница рестарта и лишние перезапуски
Когда DevTools включён, он старается быть хорошим: видит изменения — реагирует. Но у него нет телепатии: он не знает, что вы только правили текст в README.md, а не меняли код бизнес-логики. Поэтому он может перезапускать приложение “слишком старательно”, и именно здесь появляется нужда в управлении путями — чтобы перезапуск происходил только тогда, когда он действительно нужен.
Представьте, что ваш catalog-service — это кафе. Restart — это закрыть кафе, выгнать всех гостей, заново включить кофемашину, пересобрать меню, снова повесить вывеску и открыть двери. Иногда это оправдано (вы поменяли меню или кухню), но если вы просто поправили опечатку в табличке “Вход”, закрывать кафе на пять минут — очень странная бизнес-модель.
Технически проблема выглядит так: DevTools следит за изменениями в файлах, которые попадают в “зону внимания” (обычно это classpath проекта). Любое изменение там потенциально может вызвать restart. Наша цель — сделать границу предсказуемой: какие изменения вызывают restart, какие дают reload, а какие вообще игнорируются.
2. Дефолтные изменения DevTools
Понимание дефолтной политики DevTools — это как понимание правил дорожного движения. Можно и без него… но тогда вы постоянно удивляетесь, почему все вокруг сигналят. DevTools в первую очередь ориентируется на изменения в classpath: туда попадают скомпилированные классы и ресурсы (src/main/resources), которые IDE/Gradle копируют в build-вывод. Именно эти изменения чаще всего и считаются “достаточно важными”.
Давайте привяжем это к реальности нашего проекта. В 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 | Обычно да | Binding конфигурации происходит при старте |
| static/index.html | src/main/resources/static | Обычно нет (хочется reload) | Это “витрина”, а не “двигатель” |
| docs/** | src/main/resources/docs | Нет | Не влияет на приложение, но может триггерить рестарт |
Вот из-за последней строки мы и учимся управлять путями: иначе DevTools честно делает restart на изменения, которые вообще не должны менять поведение сервиса.
У DevTools для этого есть три разные ручки управления restart. Их не надо механически складывать все сразу в один application-local.yaml: обычный local-режим может обходиться вообще без них, а настройку выбирают под конкретный источник лишних перезапусков или под внешний каталог, за которым надо следить.
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 уже исключал что-то ещё (например, другие resource-папки), то при таком подходе вы могли это “случайно забыть”, и поведение изменится.
Поэтому 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 остаются, а вы добавляете ещё два своих. И вот здесь появляется важное инженерное качество: вы можете объяснить, почему эти исключения добавлены. Это всегда хороший тест на адекватность настройки.
В результате ваш feedback loop становится ровнее: вы можете спокойно редактировать документацию и sample requests, не наблюдая каждые 20 секунд перезапуск embedded Tomcat (который уже начинает подозревать, что его держат в заложниках).
5. Паттерны путей для exclude и additional-exclude
Пути в exclude и additional-exclude — это маленький язык шаблонов. И, как в любом маленьком языке, больше всего боли приносит не сложность, а самоуверенность. Кажется: “ну я написал * — значит всё”. А потом внезапно исключается не то, что вы ожидали, и DevTools ведёт себя как кот: смотрит в глаза и делает вид, что ничего не произошло.
На практике в Boot-окружении чаще всего встречается ant-style логика с шаблонами. Идея простая: вы описываете “маску” файлов, изменения в которых не должны вызывать 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 — typed config model):
# ./config/catalog-extra.yaml
app:
catalog:
# Пример значения, которое удобно менять без пересборки проекта.
# Но чтобы оно применилось, всё равно потребуется restart контекста.
title: "Catalog Service (external config)"
Почему эта связка важна методически. Мы не просто “ускоряем рестарт”. Мы делаем так, чтобы ваша конфигурационная модель из предыдущего модуля (profiles, imports, external locations, @ConfigurationProperties) нормально жила в dev-режиме. Иначе получается забавная (и очень распространённая) ситуация: конфиг вы уже вынесли наружу, но менять его неудобно, потому что “что-то не применяется”. DevTools как раз помогает сделать этот сценарий приятным, но только если вы правильно описали границы наблюдения.
7. Правило: когда нужен restart
В реальной разработке вам редко нужно помнить названия всех свойств. Вам нужно быстро ответить на вопрос: “Это изменение должно пересоздавать контекст или нет?”. Если правило слишком сложное, вы всё равно начнёте действовать наугад, а DevTools будет выглядеть как “нестабильная магия”. Поэтому полезно держать в голове очень простой алгоритм.
Ниже — мини-схема принятия решения. Она не про то, что DevTools фактически сделает в каждом случае, а про то, как вам думать и как выбирать настройки exclude / additional-exclude / additional-paths.
flowchart TD
A["Файл изменился"] --> B{"Влияет на бины / wiring / binding конфигурации?"}
B -- "Да" --> C["Нужен restart (пересоздать context)"]
B -- "Нет" --> D{"Это статический ресурс, который просто отдаётся наружу?"}
D -- "Да" --> E["Лучше без restart (reload/refresh ресурсов)"]
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 обновлялся “как статический файл” без рестарта. Но в нашем курсе каталог курсов загружается через typed configuration (@ConfigurationProperties), а значит изменения в YAML должны приводить к пересозданию контекста (или как минимум к рестарту механизма binding). Если вы исключите этот файл из restart, вы создадите себе ложное ожидание “поменял YAML — и всё применилось”.
Ошибка №5: хранить devtools-настройки не в local профиле, а в общем application.yaml.
Так вы делаете локальные удобства глобальными правилами. Потом включаете другой профиль, отдаёте проект товарищу, запускаете в другом режиме — и внезапно DevTools влияет на поведение там, где он вообще не нужен. DevTools — это инструмент для локальной разработки, и его политика путей должна быть локальной и явно привязанной к local профилю, чтобы базовая конфигурация проекта оставалась чистой и переносимой.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ