1. Restart, reload и путаница
Когда вы учитесь Spring Boot, вы постоянно что-то меняете: один метод в сервисе, одну строчку в YAML, один заголовок на landing page. И почти всегда у вас в голове звучит один вопрос: «Почему я это поменял, а в браузере/логах/ответе API всё осталось как было?». Это не лень и не “руки не из того места”, это нормальная проблема отсутствия модели перезапуска.
Если упростить до бытовой аналогии, то начинающий разработчик часто ожидает, что Java‑приложение будет вести себя как документ в Google Docs: печатаешь — и «оно само обновилось». Но Spring Boot — это не текстовый редактор. Это сервис, у которого есть жизненный цикл: старт, создание ApplicationContext, создание бинов, поднятие embedded‑сервера, обработка запросов. А DevTools — это “ускоритель”, который помогает проходить этот жизненный цикл чаще и быстрее, но не отменяет его.
В этой лекции мы договоримся о терминах и поведении. Restart — это когда ваш Spring‑контекст реально пересоздаётся. Reload — это когда контекст не пересоздаётся, но вы всё равно видите обновление результата (обычно потому что поменялся ресурс, который читается «на лету»). И если вы начнёте различать эти два режима, у вас резко уменьшится число загадочных ситуаций вида «я уже 10 раз сохранил файл, почему ничего не изменилось?».
Три уровня обновления
В разработке Boot‑сервиса полезно держать в голове не две, а три разные “перезагрузки”. Иначе легко попасть в ловушку: вы думаете, что у вас был restart, а на деле был только reload браузера (или наоборот). DevTools находится ровно посередине: он ускоряет перезапуск приложения, но не делает его «бессмертным».
Ниже — таблица, которую стоит мысленно доставать каждый раз, когда вы что-то поменяли и ждёте эффект:
| Что вы делаете | Что реально обновляется | Пример в catalog-service | Что обычно видно |
|---|---|---|---|
| Reload (обновление страницы/ресурса) | Контекст не пересоздаётся, приложение продолжает работать | Поменяли static/index.html, нажали обновить страницу | Нет новых startup‑логов, порт тот же, просто в браузере другой HTML |
| DevTools restart | ApplicationContext пересоздаётся, бины пересоздаются, сервер поднимается заново (в рамках того же процесса JVM) | Поменяли Java‑код сервиса или YAML в resources | В логах снова видно старт, снова выполняются runners, @PostConstruct и т.д. |
| Полный перезапуск процесса (Stop/Run заново) | Умирает JVM‑процесс и стартует новый | Поменяли build.gradle.kts, добавили dependency / поменяли версию | Всё стартует «с нуля»: новый процесс, новая JVM, полный прогрев |
В этой таблице есть один важный психологический момент. DevTools restart ощущается как “почти магия”, потому что он быстрый, но это всё равно перезапуск Spring‑приложения. Если вы это примете, вам станет проще объяснять себе странные эффекты: почему повторно выполняется StartupSummaryRunner, почему пересоздаётся in-memory репозиторий, почему некоторые значения читаются заново из YAML.
2. Что делает DevTools restart в Spring Boot
Когда говорят «DevTools перезапускает приложение», хочется уточнить: “а что именно он перезапускает?”. Потому что в голове новичка “приложение” часто равно “томкат” или “контроллеры”. На самом деле 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[Поднятие embedded-сервера]
%% После этого приложение снова готово принимать запросы
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 executed");
}
}
Если вы меняете 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>Catalog Service</h1>
<!-- Проверьте кэш браузера, если после reload текст не обновился -->
<p>Local mode</p>
Поменяли <p>Local mode</p> на <p>Local mode (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
%% Base ClassLoader живёт дольше и обычно не пересоздаётся при DevTools restart
Base["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 перезапуск его не переинициализирует корректно. В таких случаях обычно нужен полный перезапуск процесса (остановить приложение и запустить снова), потому что изменилась фундаментальная структура 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: в логах снова будет старт, снова выполнятся runners, и только после этого вы увидите новое поведение при запросе, например, к /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 обычно помогает, отключая или ослабляя кэширование некоторых ресурсов в dev‑режиме, но браузер — существо с характером. Если вы поменяли 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 зависимостей, почти всегда лучше остановить приложение и запустить заново, чтобы базовый слой загрузился корректно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ