Привет, Хабр! Когда java -jar цинично игнорирует ваш -cp, хочется грустить, но спокойствие, сегодня рассмотрим, почему так происходит и как это обойти.
Откуда ноги растут: приоритет Class-Path’а в -jar
JVM при запуске с -jar делает две нетривиальные вещи:
Создаёт Application ClassLoader и кладёт в него: сам запущенный JAR, всё, что перечислено в Class-Path манифеста. Игнорирует всё, что вы пытались подсунуть через -cp или CLASSPATH.
Логика: гарантировать повторяемый запуск одной капсулы кода.
Пример:
# структура проекта src/ com/example/App.java libs/ commons-lang3-3.14.0.jar # компиляция javac -d out src/com/example/App.java # манифест без Class-Path echo "Main-Class: com.example.App" > MANIFEST.MF # сборка jar cfm app.jar MANIFEST.MF -C out . # «Ложная надежда»: пробуем передать -cp java -cp libs/commons-lang3-3.14.0.jar -jar app.jar # получаем NoClassDefFoundError – зависимость не видна
Что именно должно быть в MANIFEST.MF
Минимальный набор для самостоятельного JAR»а:
Manifest-Version: 1.0 Main-Class: com.example.App Class-Path: libs/commons-lang3-3.14.0.jar libs/guava-33.0.0.jar
Пути относительные к расположению JAR»а. Разделитель — пробел, а не запятая. Переносы — только CRLF или LF + пробел в начале продолжения строки. Максимум 72 байта в строке — придётся разбивать. Если библиотек много — лучше переключаться на uber‑JAR.
Автоматическая генерация в Gradle
jar { manifest { attributes( 'Main-Class': 'com.example.App', 'Class-Path': configurations.runtimeClasspath .collect { "libs/${it.name}" } .join(' ') ) } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } }
Не только пишем Class-Path, но и кладём зависимости в libs/ рядом с app.jar.
Собираем JAR с встроенным classpath
Gradle Shadow Plugin
plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' } shadowJar { archiveClassifier.set('') minimize() // срежет неиспользуемые классы }
Запускаем:
./gradlew shadowJar java -jar build/libs/app.jar # работает без внешних lib’ов
Shadow перезаписывает MANIFEST.MF — Class-Path исчезает. Все классы зависимостей пакуются внутрь. Возможность shade»ить пакеты, чтобы избежать конфликтов версий.
Maven Shade
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.App</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>
Альтернатива № 1: Main-Class + shell-обёртка
Когда хотите держать JAR чистым, а зависимости — в libs/:
Bash-launcher (run.sh)
#!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" java \ -cp "$SCRIPT_DIR/libs/*:$SCRIPT_DIR/app.jar" \ com.example.App "$@"
"$@" прокидывает аргументы дальше. На Windows аналогичный .cmd с set CLASSPATH= и ; вместо :.
Но минус: два артефакта вместо одного.
Альтернатива № 2: Jigsaw + jlink
С Java 9+ можно пойти дальше и вообще отказаться от класса‑паса:
# module-info.java module com.example.app { requires org.apache.commons.lang3; } # сборка jlink \ --module-path mods:$(jdeps --print-module-path libs/*.jar | tr ':' '\n') \ --add-modules com.example.app \ --output dist ./dist/bin/com.example.app
Получаем самодостаточный runtime, лишний код отрезан, класс‑паса нет. Весит столько, сколько нужно приложению, а не целый JDK.
Профилактика NoClassDefFoundError
Мгновенный рентген запущенной JVM
# Показать, какие JAR'ы реально проглотил AppClassLoader jcmd $(pgrep -f app.jar) VM.classloaders | less
Команда (jcmd ... VM.classloaders) доступна с JDK 11+. Видно иерархию лоудеров, от Bootstrap до пользовательских, плюс список JAR»ов.
Летучий аудит: -verbose:class
Временно перезапускаем сервис с дополнительным флагом:
java -jar -verbose:class app.jar | grep "com.google.common.base.Preconditions"
JVM печатает каждую загрузку класса и JAR‑источник. Ловим момент, когда нужный класс ищется, но не находится. Сохраняйте вывод в файл и фильтруйте grep.
jdeps против слепых зон
Когда не уверены, в каком JAR‑файле должен лежать класс:
# покажет транзитивные зависимости jdeps --recursive --multi-release 17 app.jar | less
Флаг --multi-release важен для multi‑release JAR»ов. Если в выводе нет нужного модуля/пакета — значит, его правда забыли уложить в Class-Path или uber‑JAR.
Правильный Docker-слой
Контейнеры часто прячут проблему:
# Анализируем лёгкий runtime, собранный jlink'ом FROM eclipse-temurin:17-jre COPY dist/ /opt/app/ ENTRYPOINT ["/opt/app/bin/com.example.app"]
Если JAR‑ы кладутся в ${APP_HOME}/libs, проверьте, что COPY действительно подтягивает libs/*. Для multi‑stage‑build удобно держать артефакты в /build и копировать ровно то, что надо:
COPY --from=builder /build/app.jar /opt/app/app.jar COPY --from=builder /build/libs /opt/app/li
Когда uber-JAR или jlink — не ваш выбор
Частые обновления и микро‑патчи. Если вы выкатываете сервис десятками мелких версий в день, собирать и тащить 50-мегабайтный uber‑JAR или целый jlink‑runtime ради фикса из пары классов экономически бессмысленно. Инкрементные деплой‑стратегии (JRebel, Spring DevTools, hot‑swap в Kubernetes) теряют прелесть, потому что каждый пуш превращается в полный ребилд > перекладка жирного артефакта.
Динамические плагины и runtime‑скрипты. Приложения, которые по ходу жизни подтягивают сторонние JAR‑ы (DSL‑engine, плагинная архитектура, Apache Beam JobServer), требуют гибкого classpath»а. Uber‑JAR запечатывает вселенную, а jlink строит кастомный JRE без возможности --add-modules на лету. В таких случаях shell‑лаунчер с аккуратным -cp "$LIBS/*:$EXT/*" остаётся единственным адекватным вариантом.
Если у вас полтора JAR»а зависимостей — прописывайте их в манифесте. Если десятки — соберите uber‑JAR и спите спокойно. Нужен fine‑grained контроль и дистрибутив ≤ 40 MB? Пора познакомиться с jlink. А когда инфраструктура требует — пишите shell‑лаунчер, добавьте health‑check и резвитесь с JVM флагами.
Если вы сталкивались с неожиданными тормозами Hibernate из-за неправильных JPQL-запросов, рекомендую посетить открытый урок 19 июня — на нём подробно разберете, как избежать типичных ошибок и повысить производительность в несколько раз. Это практический урок с конкретными приёмами оптимизации — от выявления антипаттернов до эффективного использования JOIN FETCH и кэширования.
Если тема интересна — записывайтесь на странице курса "Java Developer. Professional".
Готовы проверить свои знания Java? Пройдите короткое вступительное тестирование и узнайте, насколько уверенно вы разбираетесь в теме.
