1. DevTools — турбо-режим локальной разработки
DevTools легко воспринимать как обязательную часть Spring Boot: включил — и всё “живее”, быстрее, веселее. Но это примерно как привыкнуть ездить по городу только по навигатору и потом внезапно обнаружить, что вы не знаете, где север. DevTools — помощник, который ускоряет локальную разработку, но не должен подменять понимание того, как приложение обычно стартует и работает.
Если смотреть на catalog-service как на учебный шаблон, DevTools — это “съёмный ускоритель”. Он не меняет архитектуру домена, не делает ваши @ConfigurationProperties правильнее и не чинит сломанный wiring. Он просто помогает чаще получать результат: поправили controller — почти сразу увидели новый JSON, поправили index.html — быстро обновили страницу, поправили YAML — контекст пересоздался и подтянул новое значение.
И вот здесь скрыта ловушка: DevTools добавляет в картину ещё один слой поведения. Вы начинаете жить в режиме “авто-рестартов”, и в какой-то момент у вас появляется два разных мира: “как работает у меня локально” и “как работает без DevTools”. И если вы не умеете эти два мира сравнивать, вы рискуете диагностировать баги гаданием на кофейной гуще (а кофе, как известно, тоже имеет свойство заканчиваться).
Обычный запуск и сравнение поведения
Самая практичная причина иногда отключить DevTools — не “потому что он плохой”, а потому что вам нужно честное сравнение. Если приложение ведёт себя странно, очень полезно увидеть: это странность вашего кода/конфига или странность режима DevTools. Это как с наушниками с шумоподавлением: удобно, но иногда надо снять и понять, что вообще происходит вокруг.
В DevTools-режиме JVM процесс обычно не перезапускается. Перезапускается application context, пересоздаётся часть загрузки классов, пересобираются бины, поднимается встроенный сервер, выполняются startup runners — но сам процесс остаётся тем же. И вот это “сам процесс тот же” иногда влияет на восприятие. Например, если какая-то библиотека или инфраструктурный кусок держит статическое состояние в base classloader’е, оно может переживать рестарты. В обычном “полном” stop/start процесса такого не будет — всё начнётся с нуля.
Ещё одна причина — воспроизводимость. Вы хотите, чтобы коллега (или вы сами через неделю) мог запустить проект без хитрых локальных условий и получить то же поведение. DevTools должен ускорять, но не превращать запуск в квест “а у тебя DevTools точно включён? а exclude не слишком широкий? а IDE не держит старый процесс?”.
И да, в catalog-service DevTools подключён как developmentOnly, то есть он не должен становиться частью “обычной жизни” приложения как артефакта. Это заранее защищает от ситуации “случайно утащили DevTools в не-локальную среду и получили сюрпризы”.
2. Отключение DevTools
По умолчанию local-режим у нас остаётся простым: DevTools подключён как developmentOnly, настройки живут в application-local.yaml, restart включён. Мягкое и жёсткое отключение — это временные диагностические режимы, а не новое штатное состояние проекта.
Три уровня отключения
Когда говорят “отключи DevTools”, начинающие часто делают одно и то же: выкидывают зависимость из Gradle. Иногда это нормально, но чаще — это слишком грубо. В реальности есть несколько уровней отключения, и они решают разные задачи. Удобно держать в голове следующую карту.
| Уровень | Как делаем | Что получаем | Когда это уместно |
|---|---|---|---|
| 1) Убрать DevTools с classpath | Удалить spring-boot-devtools из зависимостей или не включать developmentOnly | Вообще никакой DevTools-логики, один “обычный” запуск | Когда хотите полностью исключить влияние DevTools или готовите проект как минимальный шаблон |
| 2) Отключить restart как реакцию на изменения | spring.devtools.restart.enabled: false в application-local.yaml | DevTools на classpath есть, но file-watcher не будет дёргать рестарт | Когда DevTools мешает автоперезапуском (например, вы в середине большого рефакторинга) |
| 3) Полностью выключить restart-support ещё до старта | System property до SpringApplication.run(...) (или -D...) | DevTools не будет поднимать restart-механику на старте | Когда подозреваете classloader-эффекты и хотите максимально “обычный” рантайм без DevTools-слоёв |
Звучит похоже, но разница существенная. Вариант №2 — это чаще всего “временно перестать дёргаться от каждого сохранения файла”. Вариант №3 — это уже “мне надо, чтобы старт был максимально чистый, без участия restart-механики вообще”.
Мягкое отключение авто-рестарта
Когда вы устали от бесконечных рестартов (особенно если вы правите много файлов подряд), хочется кнопочку “замри”. И такая кнопочка есть: spring.devtools.restart.enabled=false. Это удобный, конфигурационный способ сказать DevTools: “не следи за изменениями и не пересоздавай контекст автоматически”. Важно лишь понимать, что именно вы отключаете и что при этом остаётся включённым.
Вот самый прямой вариант для локального профиля:
# src/main/resources/application-local.yaml
# Локальный профиль: DevTools остаётся на classpath, но перестаёт автоматически рестартить контекст
spring:
devtools:
restart:
enabled: false # Отключаем авто-рестарт при сохранении файлов
Это решение хорошее тем, что оно остаётся в рамках нашей общей модели: всё, что относится к локальному поведению, мы стараемся держать в application-local.yaml. Мы не разбрасываем dev-only настройки по общему application.yaml, чтобы потом не удивляться: “почему у меня в dev/production что-то странное”.
Но есть важный нюанс из сегодняшних тезисов дня: даже при таком отключении restart classloader поддержка всё ещё может быть инициализирована (то есть вы отключили “автоматическую реакцию”, но сам механизм может быть поднят). Поэтому если ваша проблема выглядит как “что-то странное с классами и classloader’ами”, мягкого отключения может не хватить — тогда нужен более жёсткий вариант.
И ещё один момент: этот способ отключения идеален как “режим тишины”, но он не заменяет обычный stop/start. Если вы поменяли что-то большое и хотите гарантированно начать “с чистого листа”, DevTools тут просто не обязателен — иногда честнее перезапустить приложение вручную. Закончили проверку — верните enabled обратно или просто уберите этот ключ, чтобы local-режим снова оставался быстрым по умолчанию.
Полное отключение до старта приложения
Бывают ситуации, когда вы хотите, чтобы приложение стартовало так, будто DevTools вообще не существует. Самый надёжный способ — выставить системное свойство до запуска контекста. Внутри кода это выглядит так (да, это именно тот пример, который иногда делают на время диагностики):
package com.example.catalogservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Важно: задаём свойство ДО SpringApplication.run(...),
// иначе restart-механика могла уже успеть инициализироваться
System.setProperty("spring.devtools.restart.enabled", "false");
// Обычно аккуратнее передавать это как JVM-аргумент (-Dspring.devtools.restart.enabled=false),
// а строку в main() держать только как временный диагностический приём
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
Смысл здесь не в том, чтобы навсегда оставить такую строку в main() (обычно это как раз плохая идея для постоянного состояния проекта), а в том, чтобы однозначно выключить restart-support ещё до того, как Spring Boot начнёт собирать приложение. Это похоже на “запуск в безопасном режиме”: вы исключаете один слой поведения и проверяете, что изменилось.
На практике то же самое можно сделать через JVM аргумент -Dspring.devtools.restart.enabled=false при запуске, и это даже аккуратнее, потому что не требует правки кода. Но в рамках учебного проекта полезно один раз увидеть, что “до run(...)” действительно имеет значение: часть инфраструктурных решений принимается очень рано, и иногда важно попасть именно в этот ранний момент.
Главная мысль: если вы пытаетесь расследовать поведение, которое похоже на classloader-магии, то выключать DevTools “после старта” — поздно. Нужен вариант, который действует сразу. Проверили гипотезу — убрали эту строку или JVM-аргумент и вернулись к обычному local-режиму. Оставлять такой тумблер в main() на постоянной основе — почти гарантированный будущий сюрприз.
3. Когда DevTools мешает: симптомы и диагностика
DevTools чаще всего работает идеально и просто экономит время. Но когда он начинает мешать, ощущения у новичка обычно одинаковые: “я ничего не понимаю, оно само перезапускается, а баг то появляется, то исчезает”. Чтобы не превращать диагностику в шаманство, полезно знать несколько характерных симптомов и иметь простой сценарий проверки.
Один из самых показательных симптомов — когда проблема исчезает после полного stop/start, но упорно воспроизводится или “плавает” при devtools-рестартах. Это прямой намёк, что где-то есть состояние, которое переживает рестарты. В нашем учебном catalog-service мы стараемся не создавать такую ситуацию, но в реальной жизни это бывает, например, из-за статических кэшей в библиотеках, из-за фоновых потоков, из-за каких-то объектов, которые удерживаются в base classloader’е.
Второй симптом — когда в исключениях начинают мелькать слова, похожие на RestartClassLoader. Например, вы можете увидеть что-то вроде ClassCastException, где формально “класс не может быть приведён к этому же классу”. Звучит как анекдот, но это классический эффект двух загрузчиков: для JVM “одинаковое имя класса” не означает “одинаковый класс”, если его загрузили разные classloader’ы. В таком случае DevTools становится подозреваемым номер один.
Третий симптом более житейский: вы в отладчике поставили брейкпоинт, а после очередного рестарта он как будто “перестал срабатывать”, или вы видите, что IDE держит старую картину классов. Это не всегда DevTools, но DevTools часто участвует в этом танце, потому что классы реально “переезжают” в новый restart classloader.
Четвёртый симптом — когда вы меняете конфиг, ожидаете, что он применился, но приложение ведёт себя так, будто конфиг старый. Здесь DevTools может быть виноват косвенно: например, вы исключили нужный путь из рестарта или, наоборот, ожидаете рестарт от файла, за которым DevTools не следит. Тогда проблема решается не “выключить DevTools навсегда”, а “привести политику restart в порядок”. Но первый шаг диагностики всё равно полезен: выключить restart и посмотреть, что изменится.
Чтобы не держать всё это в голове как набор “страшилок”, удобно смотреть на диагностику как на маленький алгоритм. Его можно даже изобразить как блок-схему:
flowchart TD
%% Быстрая проверка: отделяем баги кода/конфига от эффектов рестартов DevTools
A["Странное поведение после изменений"] --> B{"Воспроизводится после полного stop/start?"}
B -- "Да" --> C["DevTools вряд ли причина: ищем в коде/конфиге"]
B -- "Нет" --> D{"DevTools включён?"}
D -- "Нет" --> C
D -- "Да" --> E["Отключаем restart (enabled=false) и проверяем"]
E --> F{"Проблема исчезла?"}
F -- "Да" --> G["Это эффект рестартов/classloader: временно работаем без рестарта или выключаем restart-support полностью"]
F -- "Нет" --> C
Обратите внимание на важную методическую деталь: DevTools здесь не “козёл отпущения”, а просто один из факторов среды. Мы не начинаем с “удалить зависимость”. Мы сначала честно проверяем, влияет ли она вообще.
И последнее. Не путайте “DevTools мешает” с “DevTools неудобен”. Если вам просто не нравится, что сервис рестартит слишком часто — это решается настройками exclude/additional-exclude и привычкой сохранять файлы осмысленно. Если же вы видите эффекты, похожие на разъехавшиеся классы, странные исключения или исчезающие баги — тогда да, DevTools лучше на время отключить и отладить поведение в максимально обычном режиме.
4. Стратегия DevTools в catalog-service
В нашем курсе catalog-service — не просто “проект для галочки”, а будущий шаблон. Поэтому DevTools мы используем так, чтобы он не оставлял после себя следов в архитектуре и не превращал конфигурацию в “лес локальных заклинаний”. Стратегия простая: DevTools подключён как developmentOnly, в application-local.yaml живут только понятные devtools-настройки, а restart по умолчанию включён. Все отключения и тумблеры живут ровно столько, сколько нужна диагностика.
Если вы хотите, чтобы DevTools был всегда включён в локальном профиле, это выглядит максимально прозрачно: у вас есть application-local.yaml, и там видно, что рестарт разрешён. Если вы хотите временно выключить рестарт — вы меняете ровно одно свойство в локальном профиле (или передаёте системное свойство перед запуском), и это изменение легко объяснить словами. Главное — не превращать DevTools-настройки в “шум”: чем больше там магических паттернов, тем чаще вы будете думать, что приложение “само по себе странное”, хотя на самом деле вы просто забыли, какие правила вы ему задали.
Хорошая привычка в таком проекте — периодически запускать сервис в режиме, максимально похожем на обычный: без авто-рестартов. Не ради наказания, а ради уверенности: вы проверяете, что ваш wiring и конфигурация не зависят от “особого режима”. Это особенно полезно для config-first проекта: вам важно видеть, что @ConfigurationProperties стабильно биндится и валидируется, и что изменения конфигов не дают “фантомных” эффектов.
И ещё одно: если вам приходится добавить в main() временный System.setProperty(...) ради диагностики — воспринимайте это как временную лупу, а не как часть дизайна приложения. В нормальном состоянии шаблона CatalogServiceApplication должен быть чистым и не содержать “локальных тумблеров”. Локальные тумблеры у нас уже есть — они называются profiles и YAML.
5. Типичные ошибки DevTools
Когда DevTools только появляется в проекте, он часто вызывает две противоположные реакции: одни начинают верить, что “теперь всегда будет быстро и правильно”, другие в первом же непонятном случае кричат “всё, удаляю DevTools”. Оба подхода приводят к одинаковому результату: вы теряете управляемость.
Ошибка №1: отключать DevTools в общем application.yaml, а потом удивляться, что локально всё стало медленнее.
Это очень частая история: кто-то один раз выключил рестарт, чтобы спокойно порефакторить, и оставил настройку в общей конфигурации. Через день другой человек (или вы сами) запускаете проект и получаете “почему оно не обновляется?”. DevTools-настройки почти всегда должны жить в application-local.yaml, чтобы не затрагивать базовое поведение.
Ошибка №2: путать “выключить автоперезапуск” и “полностью убрать restart-механику”.
spring.devtools.restart.enabled=false выключает автоматический рестарт как реакцию на изменения, но при этом DevTools остаётся на classpath и часть его инфраструктуры может всё равно участвовать в старте. Если вы боретесь с classloader-эффектами, вам нужен вариант “до SpringApplication.run(...)” (или полный отказ от зависимости), иначе вы будете лечить симптомы, а не причину.
Ошибка №3: диагностировать баги только в режиме DevTools и считать это «реальным» поведением приложения.
DevTools — это специальный режим. Если вы видите странность, которую никак не можете объяснить, сравнение с обычным запуском — очень сильный шаг. Иногда оказывается, что проблема вообще не в DevTools, а в том, что вы изменили конфиг, но ожидали “reload” там, где нужен “restart”, или исключили нужный путь. Но пока вы не сравнили режимы, у вас нет точки опоры.
Ошибка №4: сразу удалять зависимость из Gradle вместо временного отключения restart.
Удаление зависимости — это как выключить электричество во всём доме, потому что вас раздражает лампочка в коридоре. Иногда нужно, но чаще это слишком грубо. Обычно разумнее сначала отключить рестарт в application-local.yaml, а если стало лучше — уже решать, нужен ли полный отказ от DevTools на время.
Ошибка №5: превращать DevTools-настройки в “секретную магию”, которую никто не понимает.
Когда в application-local.yaml появляется десяток паттернов exclude/additional-paths, и никто уже не помнит, что они означают, DevTools перестаёт быть ускорителем и становится источником случайности. В учебном и шаблонном проекте важно, чтобы любую настройку можно было объяснить словами: “это не рестартит, потому что статический ресурс” или “за этим каталогом следим, потому что он внешний”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ