Почему java -jar игнорирует твой -cp и как это обойти

от автора

Привет, Хабр!

Когда 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? Пройдите короткое вступительное тестирование и узнайте, насколько уверенно вы разбираетесь в теме.


ссылка на оригинал статьи https://habr.com/ru/articles/911992/