1. Build‑ошибки: окружение и Spring
Большинство сбоев сборки связаны не с «магией Spring», а с тем, что окружение и граф зависимостей перестали быть согласованными. Когда проект перестаёт собираться, мозг начинающего разработчика обычно резко разворачивается в сторону «я, наверное, ничего не понимаю» или «Spring опять всё сломал». Это нормальная реакция: сборка выглядит как чёрный ящик, особенно когда половина сообщений на английском, а другая половина похожа на заклинания.
Полезно сначала отделить «где упало» от «почему упало». У Gradle и Spring Boot есть довольно чёткие этапы: скачать зависимости, собрать classpath, скомпилировать, прогнать тесты. Если вы понимаете, на каком этапе случилась авария, вы уже выиграли половину битвы.
flowchart TD
A["Вы меняете build.gradle.kts"] --> B["Resolve dependencies
(скачать/согласовать версии)"]
B --> C["Compile
(javac + toolchain)"]
C --> D["Test
(если есть)"]
D --> E["Run tasks
(bootRun и т.д.)"]
Вот простая таблица, которая помогает «приземлить» ошибку, прежде чем вы начнёте менять всё подряд:
| Где сломалось | Как обычно выглядит | Что это чаще всего означает |
|---|---|---|
| Dependency resolution | Could not resolve..., |
проблема с репозиторием, версией, кэшем или конфликтом |
| Compilation | invalid source release, |
проблема с Java, toolchain или несовпадением JDK |
IDE показывает красное, а зелёный |
«IDE ругается, но сборка проходит» | рассинхрон IDE и Gradle, индексы/кэши |
| Внезапно «появилось много всего» | стало больше зависимостей, автоконфигураций | добавили не тот starter или дублирующую зависимость |
И главное психологическое правило этого дня: build‑ошибка не доказывает, что код плохой. Она доказывает, что модель сборки — версии, classpath, toolchain — больше не соответствует ожиданиям.
2. Java mismatch в проекте
Java mismatch — это как ситуация, когда вы пришли на тренировку по плаванию в лыжных ботинках. Формально обувь есть, но с водой не дружит. В Spring Boot-проекте Java «встречается» в нескольких местах: какая Java стоит у вас в системе, какой JDK использует Gradle, какой JDK использует IDE и какую версию просит toolchain. Если эти четыре «Java» перестали совпадать, начинается концерт с ошибками.
Самый неприятный момент: вы можете честно установить JDK, но Gradle продолжит использовать другой. Или IDE будет запускать проект одной Java, а Gradle — собирать другой. Поэтому лечить mismatch лучше не «переустановкой всего подряд», а короткой проверкой фактов.
Какие «Java» участвуют в жизни проекта
Сначала — быстрое «картографирование». Вот откуда берётся версия Java в типичном проекте вроде нашего catalog-service.
| Что именно | За что отвечает | Где задаётся | Как проверить |
|---|---|---|---|
| System Java (java в терминале) | что запустится, если вы набрали java ... | PATH / JAVA_HOME | java -version |
| Gradle JVM | на какой JVM работает Gradle daemon | настройки Gradle/IDE | ./gradlew -version |
| Toolchain languageVersion | какой JDK нужен для компиляции | build.gradle.kts | java { toolchain { ... } } |
| IDE Project SDK | подсветка, компиляция/запуск в IDE | настройки IDE | в IDE‑настройках (и косвенно через ошибки) |
И здесь появляется важная инженерная идея: toolchain — это способ сделать сборку воспроизводимой, даже если у студентов разные JDK в системе. В идеале проект говорит: «мне нужна Java 25», а Gradle умеет это обеспечить.
Симптомы mismatch и как они читаются
Есть несколько классических сообщений, которые встречаются очень часто. Приятный бонус: по ним почти всегда можно угадать, что именно не так.
Если вы видите что-то вроде:
Unsupported class file major version 69
Это означает: где-то запускается или анализируется байткод, скомпилированный более новой Java, чем та, которая пытается его прочитать. Для Java 25 «major version 69» — нормальная цифра, а вот для более старой JVM это уже «я таких букв не знаю».
Если вы видите что-то вроде:
error: invalid source release: 25
Это означает: компилятор javac, который сейчас реально используется, слишком старый и не умеет компилировать под указанный релиз. Тут часто виноваты не Spring и не Gradle, а то, что сборка идёт не тем JDK, которым вы думали.
Мини‑проверка «что у меня реально используется»
Первый шаг — не чинить, а посмотреть.
java -version
# openjdk version "25" 2025-09-16
./gradlew -version
# JVM: 25 (Vendor ...)
Не нужно идеального совпадения строк до запятой, но версия должна быть согласованной с тем, что вы заложили в проект.
И вот базовая настройка toolchain — в нашем курсе мы держим современную Java и хотим, чтобы сборка была стабильной:
java {
toolchain {
// Проект явно просит JDK 25 для компиляции, независимо от того,
// на какой JVM крутится сам Gradle daemon.
languageVersion = JavaLanguageVersion.of(25)
}
}
Если в проекте стоит 25, а ./gradlew -version показывает, что Gradle работает на 17, это не конец света. Gradle может работать на одной JVM, а компилировать другой через toolchain. Проблема начинается тогда, когда toolchain не может быть найден или скачан, либо IDE упорно компилирует «своей» Java мимо Gradle.
Что делать, если mismatch проявился прямо сейчас
Здесь важно не «устраивать ремонты во всей квартире», а сделать минимальные шаги: убедиться, что вы запускаете сборку через ./gradlew, проверить, что toolchain задан, и после изменения toolchain иногда полезно перезапустить Gradle daemon, чтобы он не продолжал жить прошлой жизнью.
./gradlew --stop
./gradlew clean build
--stop — это не магия, а просьба: «дорогой Gradle, давай начнём с чистого листа, без старой JVM-сессии». Особенно это помогает, когда вы только что поменяли JDK в IDE и ждёте, что Gradle внезапно «поймёт намёк».
3. Дубли зависимостей в Gradle
Дубли в build.gradle.kts — это как два одинаковых ключа на одной связке: дверь вы откроете, но каждый раз будете думать, какой из них «правильный». Проблема в том, что Gradle часто не падает от дубля, поэтому ошибка не кричит, а тихо ухудшает читаемость и усложняет диагностику. А в начале обучения читаемость — это половина понимания.
Начнём с самого прямого и самого грустного варианта: одна и та же зависимость объявлена дважды. Да, так бывает. Особенно когда «я что-то добавлял вчера ночью, не помню что».
dependencies {
// Один и тот же starter объявлен два раза — сборка часто пройдёт,
// но вы сами потом будете ловить «а кто это добавил?».
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
}
Gradle, скорее всего, не устроит истерику. Но вы устроите её себе — через неделю, когда будете искать «а кто принёс Tomcat?» и увидите один и тот же starter два раза, после чего начнёте подозревать вселенский заговор.
Есть и более хитрые дубли: когда вы не копируете строку, а добавляете похожую по смыслу зависимость. Например, подключили starter, а рядом «на всякий случай» ещё низкоуровневый модуль, который уже приехал транзитивно. Сборка может пройти, но граф станет шумнее, а ваша уверенность — меньше.
Как это диагностировать без лишней философии? Обычно хватает двух действий: глазами пройти dependencies { ... }, а затем посмотреть дерево зависимостей по нужной конфигурации. Не весь лес целиком, а хотя бы тот слой, который влияет на запуск.
./gradlew dependencies --configuration runtimeClasspath
# ... большой вывод, ищем повторяющиеся ветки и странные зависимости
Если хотите точечно понять, почему у вас вообще есть вот эта библиотека, у Gradle есть команда-лупа dependencyInsight. Это уже чуть более инструментальный режим, но он часто экономит время.
./gradlew dependencyInsight --dependency spring-webmvc --configuration runtimeClasspath
# покажет, кто именно притянул spring-webmvc
Очень важная мысль: дубли — это не только про сборку, это ещё и договорённость с будущим собой. build.gradle.kts — это документ. Если он шумный и повторяется, вы теряете способность быстро отвечать на вопрос «что у проекта на classpath и почему».
4. Ошибка выбора Spring Boot starter
Starter — это не «одна библиотека», а вход в целый сценарий. Поэтому неверно выбранный starter — это как перепутать дверь в подъезде и зайти к соседям: вроде тоже квартира, но тапочки не ваши, кот смотрит с осуждением, и вообще всё не так. Самая частая ошибка новичка — выбрать starter по похожему слову, а не по смыслу, а потом удивляться, что проект стал «другим».
Классический пример: человек хочет обычный servlet‑подход, то есть MVC, но добавляет reactive starter, потому что слова «web» и «web» выглядят одинаково убедительно.
dependencies {
// Включает reactive stack (WebFlux) — это другой сценарий и другой набор транзитивных зависимостей.
implementation("org.springframework.boot:spring-boot-starter-webflux")
}
Чаще всего build при этом даже не падает. Но dependency tree меняется, classpath меняется, а дальше начинают вылезать «странные» симптомы: другие зависимости, другой серверный стек, другие подсистемы. Даже если вы пока не дошли до темы web-слоя, сам граф зависимостей уже показывает, что вы подключили другой «набор мебели».
Правильная версия для нашего курса — то есть servlet MVC baseline — выглядит так:
dependencies {
// Классический servlet MVC стек для большинства учебных примеров в курсе.
implementation("org.springframework.boot:spring-boot-starter-webmvc")
}
И ещё один распространённый сценарий: новичок добавляет зависимости, которые в нашем проекте осознанно запрещены на этом этапе, потому что они тащат в проект соседние курсы. Например, можно случайно подключить JPA или Security «на будущее» и получить гору транзитивных библиотек плюс новую автоконфигурацию, которую вы ещё не готовы осмыслять.
Вместо того чтобы гадать «что случилось», делайте одну простую вещь: после добавления starter’а смотрите дерево зависимостей и задавайте себе вопрос «какой сценарий я только что включил?». Даже короткий фрагмент дерева часто уже всё объясняет:
runtimeClasspath
\--- org.springframework.boot:spring-boot-starter-webflux
\--- ... (reactive web stack transitives)
Самая надёжная «починка» неправильного starter’а — не exclude и не ручные версии, а честный откат к осмысленному набору прямых зависимостей. Если цель курса — Boot-baseline без ухода в реактивность, безопасность, базы данных и инфраструктуру, то лучший фикс — вернуть build к этому baseline.
5. Кэш Gradle и рассинхрон IDE
Кэш — это как память у человека: иногда спасает, иногда мешает жить, а иногда помнит то, чего уже давно нет. В Gradle есть кэши зависимостей, кэши сборки, Gradle daemon, плюс кэши и индексы IDE. Поэтому вполне возможна «мистическая» ситуация: вы всё поправили, а ошибка продолжает появляться. Обычно это не мистика, а просто кто-то — Gradle или IDE — продолжает жить в прошлой версии реальности.
Начнём с главного: доверяем Gradle Wrapper, а не ощущениям IDE. Если IDE подсвечивает красным, но ./gradlew build зелёный, это неприятно, но уже полезно: проблема не в коде, а в синхронизации и индексах.
Если же ./gradlew build тоже падает, но вы уверены, что зависимость «должна скачаться» и «вчера скачивалась», имеет смысл сделать мягкое обновление зависимостей.
./gradlew --refresh-dependencies
./gradlew build
--refresh-dependencies говорит Gradle: «представь, что ты чуть более забывчивый, чем обычно, и перепроверь артефакты». Это часто лечит ситуации с битым скачиванием, временными проблемами репозиториев и странностями после переключения версий.
Если вы меняли Java/toolchain, добавляли плагины или делали что-то, что влияет на внутреннее состояние Gradle, полезно остановить daemon. Это не обязательно делать каждый день, но как диагностический шаг — абсолютно нормально.
./gradlew --stop
./gradlew clean build
clean нужен не как ритуал «на всякий случай», а как способ убрать старые build-артефакты, если вы подозреваете, что проект собирается из старого мусора. В большинстве случаев Gradle и сам достаточно умный, но в учебном процессе проще иметь ясный reset-шаг, чем спорить с кэшем на философском уровне.
А вот что точно стоит делать только как последнюю артиллерию, — это удалять .gradle директорию, сносить кэши IDE, переустанавливать JDK и читать заговоры. Эти шаги иногда помогают, но они не учат диагностике. Наша цель — не «заставить заработать любой ценой», а понимать, почему сломалось.
6. Маршрут диагностики build‑проблем
Когда всё горит, очень хочется сделать «огнетушитель из всего»: поменять Java, добавить версию, убрать версию, накидать exclude, обновить Gradle, снести кэш, перезагрузить компьютер и ещё пару раз крикнуть на монитор. Проблема в том, что после такого вы уже не знаете, что именно помогло, и не сможете повторить исправление в следующий раз.
Поэтому нам нужен жёсткий маршрут. Он не самый романтичный, зато очень практичный: сообщение об ошибке → toolchain/Java → dependency tree → последние изменения → кэш/IDE.
flowchart TD
A["Ошибка сборки/старта"] --> B["Читаем сообщение
не пропуская первую причину"]
B --> C["Проверяем Java/toolchain
java -version, ./gradlew -version"]
C --> D["Смотрим последнее изменение build.gradle.kts
что добавили/убрали?"]
D --> E["Смотрим dependency tree
./gradlew dependencies (runtimeClasspath)"]
E --> F["Только если нужно: refresh/stop/clean
--refresh-dependencies, --stop"]
F --> G["Проверяем снова одной командой
./gradlew build"]
Чтобы маршрут был совсем приземлённым, вот маленькая таблица «что делать руками» на каждом шаге, без ухода в лишнюю теорию:
| Шаг | Цель | Минимальная команда/действие |
|---|---|---|
| Прочитать ошибку | понять, это resolve/compile/test | просто дочитать до первой причины, не до BUILD FAILED |
| Проверить Java | увидеть факты, а не предположения | java -version, |
| Проверить toolchain | понять, что проект просит | посмотреть java { toolchain { ... } } |
| Проверить dependency graph | увидеть реальный classpath | ./gradlew dependencies --configuration runtimeClasspath |
| Обновить/сбросить состояние | убрать проблему кэша/daemon | ./gradlew --refresh-dependencies, |
| Подтвердить фикс | убедиться, что реально работает | ./gradlew build |
Заметьте важную дисциплину: после каждого шага вы либо получили новую информацию, либо сделали одно изменение. Это и отделяет диагностику от шаманства.
7. Мини‑истории из жизни catalog-service
Иногда легче запомнить не «правила», а узнаваемые ситуации. Ниже — несколько коротких мини-сюжетов, которые регулярно происходят с учебными Boot-проектами и почти всегда сводятся к одному и тому же: нужно вернуть согласованность между окружением и dependency model.
«Я поставил Java 25, но Gradle всё равно ругается на Java 17»
Симптом обычно такой: java -version показывает одно, а Gradle при сборке ведёт себя так, будто ничего не менялось. Здесь чаще всего виноват Gradle daemon, который продолжает жить на старой JVM, или IDE, которая запускает Gradle «своей» Java.
Фикс обычно начинается с проверки:
./gradlew -version
# JVM: 17 ... (вот и причина)
И затем — аккуратный reset daemon:
./gradlew --stop
./gradlew clean build
После этого вы снова проверяете ./gradlew -version. Не потому что «так надо», а потому что это подтверждает: вы действительно переключились.
«Я добавил starter, и проект перестал скачивать зависимости»
Очень типичная ошибка — указать ручную версию там, где она не нужна, и ещё и ошибиться в ней. Пример специально утрированный, но в жизни встречается постоянно, особенно когда копируют из случайного ответа в интернете.
dependencies {
// Так делать обычно не нужно: версию starter'а в Boot-проектах
// управляет сам Spring Boot через dependency management.
implementation("org.springframework.boot:spring-boot-starter-webmvc:0.0.1")
}
Gradle честно пытается найти артефакт 0.0.1 и честно не находит. Решение скучное и правильное: убрать версию и вернуться в managed model.
dependencies {
// Версию подтянет Spring Boot (BOM/платформа), а вы фиксируете только намерение: «нужен MVC».
implementation("org.springframework.boot:spring-boot-starter-webmvc")
}
Смысл в том, что Spring Boot уже управляет версиями starter’ов через свой платформенный слой. Если вы начинаете подсовывать случайные версии, вы просто выходите из зоны предсказуемости.
«Вроде всё работает, но build.gradle.kts стал как простыня — и я ничего не понимаю»
Обычно это результат дублей и «досыпаний» низкоуровневых модулей. Самая простая форма — прямой дубль:
dependencies {
// Дубль зависимости: сборка может пройти, но читабельность build.gradle.kts станет больно страдать.
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Сборка может и не упасть, но читабельность падает сразу. В учебном проекте это особенно опасно, потому что вы теряете способность видеть намерение. Лечение банальное: оставить одну строку и, если уже хочется по-взрослому, добавить комментарий, зачем она нужна.
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test") // тестовый стек Boot
}
Да, комментарий в build.gradle.kts — это не стыдно. Стыдно через месяц не помнить, почему у вас семнадцать зависимостей «на всякий случай».
8. Типичные ошибки при диагностике build‑проблем и зависимостей
Перед тем как мы закончим уровень, важно проговорить несколько «граблей», на которые наступают почти все. Эти ошибки не про незнание Spring Boot, а про стиль мышления в сборке: либо вы действуете системно, либо вы случайно победили проблему и не понимаете как.
Ошибка №1: начинать со случайного ручного переопределения версии.
Когда что-то не собирается, рука тянется «поставить версию явно». Проблема в том, что в Boot-проекте версии уже согласованы, и случайное переопределение чаще ломает совместимость, чем лечит её. Если очень хочется править версии, сначала посмотрите dependency tree и убедитесь, что понимаете, какая именно библиотека конфликтует и почему.
Ошибка №2: верить IDE больше, чем ./gradlew.
IDE умеет многое, но она не является источником истины для сборки. Источник истины — Gradle Wrapper. Если IDE кричит красным, а ./gradlew build зелёный, лечим синхронизацию и индексы, а не проект. Если ./gradlew build падает, лечим именно то, что он говорит, а не то, что кажется в интерфейсе.
Ошибка №3: менять сразу всё подряд.
Самая частая картина: «я поменял JDK, обновил Gradle, добавил starter, исключил зависимость, и теперь не знаю, что из этого помогло или сломало». Такой подход почти гарантирует, что вы повторите проблему позже. Гораздо сильнее дисциплина «одно изменение → одна проверка». Да, медленнее на две минуты. Зато быстрее на два часа.
Ошибка №4: использовать exclude как пылесос “убрать лишнее из дерева”.
exclude — это хирургия, а не уборка в комнате. Если вы исключаете транзитивную зависимость, вы должны уметь объяснить: что именно исключили, почему оно мешало и чем заменили. Иначе вы создаёте проект, который случайно работает — пока не перестанет.
Ошибка №5: выбирать starter по названию, а не по сценарию.
Слова web, webmvc, webflux выглядят похоже, но означают разные сценарии. Starter — это включение целого слоя зависимости и будущего поведения. Поэтому выбор начинается с вопроса «какой сценарий нужен проекту», а не «что похоже на то, что я видел в интернете».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ