JavaRush /Курсы /Spring Boot /Layered jar: четыре слоя Spring Boot

Layered jar: четыре слоя Spring Boot

Spring Boot
25 уровень , 1 лекция
Открыта

1. Структура Spring Boot executable jar

Когда мы говорим jar, мозг Java-разработчика часто автоматически рисует картинку: META-INF, манифест, несколько .class — и погнали. Но Spring Boot jar — это чуть более хитрая «матрёшка»: он должен уметь запускаться как приложение, при этом тащить внутри целый набор библиотек, и делать это без внешнего сервера приложений. Поэтому первое, что нам нужно сегодня — спокойная, понятная модель физической структуры Boot-jar, чтобы layered jar дальше не выглядел как магический контейнер контейнеров.

Одного факта «jar запускается» уже мало. Теперь важно увидеть, как этот jar устроен физически, иначе разговор о слоях повиснет в воздухе.

Начнём с простой мысли: jar — это zip-архив. То есть его можно «посмотреть» стандартными инструментами, и в этом нет никакого кощунства.

Соберём jar (если он уже собран — команда просто будет быстрой):

# Полная пересборка артефакта (jar будет в build/libs)
./gradlew clean bootJar

# Берём именно executable jar из bootJar, а не *-plain.jar
JAR="$(find build/libs -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"

# Проверяем, что jar действительно появился и смотрим размер
ls -lh "$JAR"

Теперь заглянем внутрь. Не надо распаковывать весь архив и устраивать хаос на диске — достаточно просто вывести список файлов:

# Смотрим первые 20 записей внутри jar, чтобы понять «скелет» архива
jar tf "$JAR" | head -n 20

Вы увидите, что в executable jar есть два больших «мира»:

  1. Код запуска (Spring Boot loader) — классы, которые умеют стартовать приложение и загружать вложенные jar-зависимости.
  2. Содержимое приложения — ваш код, ваши ресурсы и ваши зависимости.

В упрощённом виде это выглядит так:

catalog-service.jar
├─ META-INF/
├─ org/springframework/boot/loader/...
└─ BOOT-INF/
   ├─ classes/     <-- ваш код + ресурсы (application.yaml, static, и т.д.)
   └─ lib/         <-- зависимости (другие jar внутри jar)

Почти всегда удобно отдельно «подсветить» ключевые места:

# Ваши классы и ресурсы приложения
jar tf "$JAR" | grep '^BOOT-INF/classes' | head

# Подключённые зависимости (вложенные jar)
jar tf "$JAR" | grep '^BOOT-INF/lib' | head

# Spring Boot loader (то, что делает jar исполняемым)
jar tf "$JAR" | grep '^org/springframework/boot/loader' | head

Что лежит в BOOT-INF/classes

Это ваше приложение в самом прямом смысле. Там будут:

— скомпилированные классы (например, com/example/catalogservice/...);
— ресурсы из src/main/resources/ (например, application.yaml, catalog-data.yaml, статические файлы static/index.html и т.п.).

Если вы меняете Java-код контроллера или правите YAML — вы меняете именно эту часть.

Что лежит в BOOT-INF/lib

Это зависимости: Spring Framework, Spring Boot, Jackson, Logback, Micrometer и всё остальное, что прилетело через starter’ы и transitive dependencies.

И это важный психологический момент: когда вы делаете bootJar, вы получаете артефакт, который можно передать другому человеку (или системе) — и он будет самодостаточным, потому что библиотеки лежат прямо внутри.

Что лежит в org/springframework/boot/loader/...

Это boot loader — механизм, благодаря которому java -jar работает именно «по-бутовски». Он читает манифест, понимает, какой main-класс запускать, и создаёт classpath так, чтобы JVM увидела ваши классы и вложенные зависимости.

И вот здесь появляется важная связка: layered jar будет выделять boot loader в отдельный слой, потому что по частоте изменений он живёт своей жизнью (обычно меняется только при обновлении версии Boot).

2. Layered jar и индекс layers.idx

До этого момента jar можно было воспринимать как чемодан: открыл — всё внутри, закрыл — пошёл. Но для container-friendly мышления чемодан неудобен: если вы поменяли одну футболку, вам приходится «пересобирать» весь чемодан заново. Layered jar — это как чемодан с секциями и ярлычками: вещи всё ещё в одном чемодане, но теперь можно однозначно сказать, что где лежит, и что меняется чаще. Поэтому в этой части мы вводим layered jar как тот же executable jar, но с добавленной мета-информацией о слоях.

Что такое layered jar в человеческих терминах

Layered jar — это executable jar Spring Boot, в котором присутствует описание слоёв: какие файлы относятся к «зависимостям», какие — к «коду приложения», какие — к «лоадеру» и т.д.

Ключевой файл, который нас интересует: layers.idx. Это индекс слоёв внутри jar. Он не делает приложение «быстрее само по себе», но делает структуру артефакта явной для инструментов, которые умеют эту структуру использовать.

Как проверить layered jar и когда нужна явная настройка

В текущем Boot baseline layered jar обычно уже приходит вместе с обычным bootJar, поэтому первым делом лучше не включать его «на всякий случай», а просто проверить, что внутри действительно есть индекс слоёв:

# Ищем индекс слоёв внутри jar
jar tf "$JAR" | grep 'layers.idx'

Чаще всего вы увидите путь вроде:

BOOT-INF/layers.idx

Явная настройка имеет смысл, когда вы хотите намеренно зафиксировать это поведение в build-файле или дальше кастомизировать слои. Например, так можно явно оставить layered packaging на задаче bootJar:

tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    // Optional: явно фиксируем layered packaging на задаче `bootJar`
    layered()
}

Такую настройку удобно держать в build-файле, если вы сознательно хотите зафиксировать layered packaging или позже перейти к кастомизации слоёв. Но базовый вопрос сначала всегда один: есть ли у jar индекс слоёв и совпадает ли он с тем, как вы понимаете структуру сервиса.

Как выглядит идея layers.idx

Формат layers.idx можно воспринимать как «оглавление» или «карту упаковки». Он говорит: вот у нас есть слой dependencies, вот application, и т.д.

Схематично (упрощённо) мысль такая:

layers.idx
├─ dependencies          -> часть BOOT-INF/lib (release зависимости)
├─ spring-boot-loader    -> loader классы в корне jar
├─ snapshot-dependencies -> часть BOOT-INF/lib (SNAPSHOT зависимости)
└─ application           -> BOOT-INF/classes + ресурсы приложения

И здесь важно не перепутать: слои — это логическая группировка, а не обязательно «одна папка = один слой». Да, в дефолтной схеме многое совпадает с папками, но это совпадение — приятный бонус, а не смысл механизма.

3. Четыре стандартных слоя Spring Boot

Сейчас у нас уже есть две модели одного и того же jar: физическая (папки и файлы) и логическая (слои). Дальше мы делаем самое главное в лекции: разбираем четыре стандартных слоя, которые Spring Boot использует по умолчанию. Наша цель — чтобы названия dependencies и application перестали быть просто словами, а стали инженерными «ящиками в голове»: что туда попадает, почему так, и как это связано с тем, что вы реально меняете в проекте.

Ниже удобная таблица, которую стоит буквально перечитать два раза: первый — чтобы понять, второй — чтобы мозг перестал бояться.

Слой Что это по смыслу Что там обычно лежит Как часто меняется Пример в catalog-service
dependencies «Стабильные библиотеки» BOOT-INF/lib/*.jar (release) редко Spring, Jackson, Logback, Micrometer
spring-boot-loader «Механизм запуска jar» org/springframework/boot/loader/** очень редко меняется при смене версии Boot
snapshot-dependencies «Нестабильные зависимости» BOOT-INF/lib/*SNAPSHOT*.jar чаще (если есть) часто вообще пустой слой
application «Ваше приложение» BOOT-INF/classes/** + ресурсы приложения часто классы контроллеров, YAML, static/index.html

dependencies: «камни фундамента»

Этот слой почти всегда самый тяжёлый по размеру (в килобайтах/мегабайтах), потому что туда попадают ваши библиотеки. И это хорошо: библиотеки обычно занимают больше места, чем ваш код, особенно в маленьком учебном сервисе.

Туда попадают зависимости, которые у вас в проекте фиксированы как нормальные релизы. В типичном Boot-сервисе это значит: вы можете десять раз менять контроллеры и сервисы, но зависимости останутся теми же, пока вы не обновите build.gradle.kts.

Практически это означает: если бы мы говорили про упаковку в контейнерный образ, этот слой хотелось бы кэшировать максимально агрессивно. Но даже без контейнеров полезно мыслить так: пока я не трогаю зависимости, эта часть артефакта стабильна.

spring-boot-loader: «то, что делает jar исполняемым»

Очень частая ошибка начинающих — думать, что Spring Boot loader «это часть моего приложения». На самом деле это часть упаковки.

Это те классы, которые позволяют java -jar запустить ваше приложение и корректно собрать classpath из того, что лежит внутри BOOT-INF/lib. Если сильно упростить, то loader — это «мини-движок», который запускает ваш код, но сам вашим кодом не является.

Почему он выделен в отдельный слой? Потому что он меняется не тогда, когда вы правите CourseCatalogController, а тогда, когда вы обновляете версию Spring Boot. И это редкое событие по сравнению с ежедневными правками приложения.

snapshot-dependencies: «зона повышенной турбулентности»

SNAPSHOT-зависимости — это зависимости, которые потенциально могут меняться без смены версии в привычном смысле (условно: «сегодняшний снапшот» и «завтрашний снапшот»).

В учебных проектах и в нормальной production-жизни SNAPSHOT обычно стараются избегать. Поэтому честный сценарий для catalog-service: слой может быть пустым. И это нормально. Он существует не потому, что «обязательно должен быть заполнен», а потому что Spring Boot предлагает понятную структуру на случай, если снапшоты всё же есть.

С инженерной точки зрения это очень логично: снапшоты обновляются чаще, значит их выгодно отделять от стабильных релизных библиотек, чтобы не «портить кэш» всего слоя dependencies.

application: «ваш код и всё, что вы считаете частью приложения»

Это слой, который вы меняете чаще всего. Причём «код» здесь — не только .class файлы.

В application обычно попадают:

— ваши скомпилированные классы;
application.yaml, application-*.yaml, catalog-data.yaml;
— статические файлы (static/index.html);
— иногда вспомогательные индекс-файлы вроде layers.idx (да, сам индекс слоёв часто живёт рядом, потому что он «привязан» к конкретной сборке приложения).

Если вы поменяли что-то в src/main/java или src/main/resources, то почти наверняка вы изменили именно этот слой.

4. Директории и слои: как это связано

Ловушка здесь очень человеческая: вы посмотрели jar, увидели папку BOOT-INF/lib и решили, что «это и есть слой dependencies». В большинстве случаев оно похоже на правду, но layered jar как раз существует, чтобы работать точнее, чем наша интуиция. В этой части мы аккуратно разводим два понятия: физическое расположение файлов в архиве и логическая группировка в слои, потому что путаница между ними дальше приводит к странным выводам и ошибкам.

Физическая структура jar говорит «где лежит файл». Логическая структура слоёв отвечает на другой вопрос: «к какой группе изменений относится файл и как его должен видеть packaging-механизм».

Посмотрим на несколько характерных соответствий:

Где лежит файл в jar Пример В какой слой обычно попадает Почему так
BOOT-INF/classes/** com/example/.../*.class, application.yaml application это ваш код и ресурсы
BOOT-INF/lib/*.jar spring-webmvc-*.jar dependencies релизная библиотека
BOOT-INF/lib/*SNAPSHOT*.jar some-lib-1.2.3-SNAPSHOT.jar snapshot-dependencies зависимость меняется чаще
org/springframework/boot/loader/** JarLauncher.class spring-boot-loader механизм запуска

Обратите внимание на ключевой момент: одна и та же директория BOOT-INF/lib может «раскладываться» на два слоя. Именно поэтому на уровне мышления «директория ≠ слой» — это не занудство, а полезная дисциплина.

Если вы хотите почувствовать это руками, можно сделать очень маленькое наблюдение: посмотрите на содержимое BOOT-INF/lib и просто прикиньте, есть ли среди jar-ников что-то со словом SNAPSHOT. В большинстве случаев — нет, и тогда слой snapshot-dependencies будет пустой. Но если бы он был, физически файл жил бы в той же папке, а логически — в другом слое.

Ещё один тонкий момент: слой spring-boot-loader физически лежит в корне jar (в виде пакета org/...), а не в BOOT-INF. Это непривычно, если вы мысленно считаете BOOT-INF «домом приложения». Но исторически так и задумано: loader должен быть доступен сразу при запуске jar.

Чтобы не потеряться, полезно держать такую схему (она очень «детская», и именно поэтому работает):

flowchart TD
    J["catalog-service.jar"] --> L1["spring-boot-loader корень jar"]
    J --> BI["BOOT-INF"]
    BI --> C["classes application"]
    BI --> LIB["lib dependencies + snapshot-dependencies"]

5. Изменения проекта и слои jar

Когда вы только учитесь, хочется, чтобы сборка была просто кнопкой «собрать». Но как только вы живёте с проектом хотя бы неделю, сборка превращается в ежедневную рутину: вы собираете, запускаете, передаёте, упаковываете, снова собираете. И тут внезапно выясняется, что самое дорогое в жизни — не кофе, а повторно пересобирать то, что не менялось. Именно поэтому layered jar — не «про красоту», а про экономию времени и ясность: он привязывает тип изменения к части артефакта.

Давайте прямо сопоставим обычные действия разработчика и то, какие слои меняются.

Что вы сделали Что изменилось в jar Какой слой чаще всего меняется
Поменяли Java-код (контроллер/сервис) .class файлы application
Поменяли application.yaml или catalog-data.yaml ресурсы в BOOT-INF/classes application
Добавили/обновили зависимость в build.gradle.kts jar-ники в BOOT-INF/lib dependencies или
snapshot-dependencies
Обновили версию Spring Boot изменился loader + зависимости spring-boot-loader и
dependencies
Подключили SNAPSHOT-зависимость новый jar в BOOT-INF/lib snapshot-dependencies

И вот здесь становится понятен смысл четырёх слоёв: они разделяют артефакт по «ритму жизни». Самый нервный слой — application (мы в нём живём каждый день). Самые спокойные — dependencies и spring-boot-loader. snapshot-dependencies — специально выделенная «зона беспокойства», если у вас в проекте есть снапшоты.

Если вы держите это в голове, layered jar перестаёт быть «ещё одним термином из Boot», и становится удобным способом ответить на практический вопрос: почему мой артефакт меняется так, как меняется, и какие части можно считать стабильными.

6. Типичные ошибки при layered jar

В этой теме чаще всего ошибаются не потому, что люди «плохие», а потому, что jar выглядит как один файл, и мозг очень хочет считать его монолитом. Layered jar ломает эту привычку: физически файл один, но логически он разделён на части, и это нужно принять как новую инженерную оптику.

Ошибка №1: считать, что BOOT-INF/lib целиком равен слою dependencies.
Папка действительно почти полностью совпадает со слоем зависимостей, но layered jar мыслит точнее: внутри BOOT-INF/lib могут оказаться SNAPSHOT-зависимости, и они логически попадут в snapshot-dependencies. Если этот момент упустить, дальше вы начинаете неверно объяснять, почему «что-то пересобралось» или «почему слой пустой».

Ошибка №2: воспринимать spring-boot-loader как «часть моего приложения» и пытаться его “оптимизировать”.
Boot loader — это инфраструктурный механизм запуска, а не ваш бизнес-код. Он лежит в jar ровно для того, чтобы java -jar умел собрать classpath из вложенных jar-ников. Попытки «убрать лишнее» здесь обычно заканчиваются тем, что артефакт перестаёт быть executable, и вы получаете очень грустный ClassNotFoundException.

Ошибка №3: паниковать из-за пустого snapshot-dependencies.
Очень многие проекты вообще не используют SNAPSHOT-зависимости (и правильно делают), поэтому слой snapshot-dependencies может быть пустым. Это не баг и не «недосборка», это просто следствие того, что вы используете релизные версии библиотек.

Ошибка №4: путать «слой» и «директорию» и делать выводы по имени папки.
Layered jar — это не «ещё одна папка в архиве». Слои описываются индексом layers.idx, и слой может включать файлы из разных мест, а одна папка может быть разделена на несколько слоёв. Если держаться только физической структуры, вы теряете половину смысла layered packaging.

Ошибка №5: думать, что layered jar требует менять код приложения.
В этой теме вообще не нужно переписывать контроллеры, сервисы или репозитории. Мы работаем с упаковкой и структурой артефакта, а не с архитектурой catalog-service. Если вы вдруг ловите себя на мысли «надо бы переделать сервис, чтобы jar стал layered», это хороший момент остановиться и напомнить себе: layered jar — это задача build/packaging слоя, а не прикладного кода.

1
Задача
Spring Boot, 25 уровень, 1 лекция
Недоступна
Явное включение layered jar и сохранение `layers.idx`
Явное включение layered jar и сохранение `layers.idx`
1
Задача
Spring Boot, 25 уровень, 1 лекция
Недоступна
Карта физических директорий и логических слоёв
Карта физических директорий и логических слоёв
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ