W3docs

Java Classpath

Как задать classpath при компиляции и запуске Java-программ, чтобы JVM находила нужные классы и зависимости.

Компилятор знает, в каких пакетах искать, благодаря classpath — набору мест, где Java ищет файлы классов. Когда вы запускаете java MyApp и получаете ClassNotFoundException, причина почти всегда в classpath: JVM не нашла файл .class там, где ожидала. Понимание того, как формируется classpath, превращает «просто не запускается» в конкретную, устранимую проблему.

Что такое classpath

Classpath — это упорядоченный список мест, в которых JVM ищет файлы .class. Каждое место является одним из следующих:

  • Директория — воспринимается как корень дерева пакетов. JVM ищет в ней файл com/example/Foo.class.
  • JAR-файл — просматривается так, словно его внутренняя структура директорий является обычной директорией.
  • Маска вида lib/* — соответствует всем файлам .jar в lib/ (не рекурсивно и не отдельным файлам .class).

Когда вы обращаетесь к com.example.Foo, JVM проходит classpath по порядку и использует первое совпадение. Если два места содержат один и тот же класс, побеждает тот, что стоит раньше в classpath — частая причина ситуации «я обновил JAR, но старый код всё равно запускается».

Задание classpath

Существуют три способа указать JVM содержимое classpath, по убыванию предпочтительности:

# 1. -cp / -classpath flag (clearest, scoped to the one command):
java -cp out:lib/mylib.jar com.example.App

# 2. The CLASSPATH environment variable (set once, used by every invocation):
export CLASSPATH=out:lib/mylib.jar
java com.example.App

# 3. JAR manifest Class-Path entry (for executable JARs):
java -jar app.jar

-cp имеет приоритет над CLASSPATH. Глобальная установка переменной окружения — частый источник ошибок: устаревший CLASSPATH от давно забытого проекта вызывает загадочное поведение. Предпочитайте -cp для каждой команды.

В Windows разделитель — ;. В macOS и Linux:. Заключайте значение в кавычки, если оно содержит пробелы.

Classpath по умолчанию

Если classpath не задан, JVM использует текущую директорию (.). Именно поэтому javac Hello.java && java Hello работает из коробки для файлов в пакете по умолчанию — Hello.class находится прямо здесь.

Как только вы помещаете код в пакет, нужно либо запускать программу из правильного места, либо явно передавать -cp:

# Source: src/com/example/App.java with `package com.example;`
javac -d out src/com/example/App.java
java -cp out com.example.App      # must use the fully-qualified name

Распространённая ошибка — java -cp out com/example/App. Аргумент команды java — это имя класса, а не путь: используйте точки, а не слэши.

Classpath во время компиляции

У javac есть собственный classpath, отдельный от runtime-classpath:

javac -cp lib/dependency.jar -d out $(find src -name "*.java")

Здесь -cp перечисляет места, где javac ищет типы, на которые ссылаются ваши источники. Всё, что эти источники importируют, должно находиться либо в lib/dependency.jar, либо в неявном classpath. Флаг -d компилятора указывает, куда помещаются выходные файлы .class — как правило, в параллельное дерево out/.

В большинстве сборок вы не запускаете javac и java вручную. Инструменты сборки — Maven, Gradle — собирают classpath из объявленных зависимостей. Понимание ручного управления classpath нужно, чтобы диагностировать их действия при возникновении проблем.

JAR-файлы в classpath

JAR — это ZIP-файл с файлами классов и метаданными. Поместите его в classpath, и JVM будет воспринимать его содержимое как ещё одно дерево пакетов:

java -cp app.jar:lib/json.jar:lib/db.jar com.example.Main

Несколько практических замечаний:

  • Маски раскрываются только для JAR-файлов: -cp lib/* соответствует всем файлам .jar в lib/, но не поддиректориям и не отдельным файлам .class.
  • Маски — не glob-шаблоны оболочки. Их обрабатывает сама JVM. Большинство оболочек раскрыли бы lib/* самостоятельно; JVM ожидает буквальную строку lib/*. Заключайте её в кавычки, чтобы предотвратить раскрытие оболочкой: -cp "lib/*".
  • Порядок важен при дублировании. Побеждает первый JAR, содержащий класс.

Исполняемые JAR-файлы

Если в файле META-INF/MANIFEST.MF JAR-файла указан Main-Class, его можно запустить просто с флагом -jar:

java -jar app.jar

Два нюанса при использовании -jar:

  • -cp игнорируется при использовании -jar. Единственный способ добавить зависимости к classpath исполняемого JAR — через атрибут Class-Path: манифеста, перечислив другие JAR-файлы.
  • Main-Class в JAR обязателен. Без него -jar отказывается запускаться.

Именно поэтому fat JAR — единый JAR, содержащий все зависимости, собранный с помощью плагина Maven Shade или плагина Gradle Shadow — стал стандартом. Он обходит всю сложность classpath для исполняемых JAR.

Диагностика проблем с classpath

Две ошибки прямо указывают на classpath, и они означают немного разные вещи:

  • ClassNotFoundException — код явно запросил класс по имени (часто через Class.forName(...) или рефлексию), и загрузчик не нашёл его нигде в classpath.
  • NoClassDefFoundError — класс присутствовал при компиляции вашего кода, но отсутствует или не может быть загружен во время выполнения. Обычная причина — зависимость JAR есть в classpath компиляции, но отсутствует в runtime-classpath.

При возникновении любой из этих ошибок проработайте следующий чеклист:

  1. Выведите classpath, который JVM фактически использовалSystem.getProperty("java.class.path"), как в примере ниже. Набор, который вы считаете переданным, и реально действующий часто различаются.
  2. Проверьте разделитель. : в macOS/Linux, ; в Windows. Неверный разделитель молча превращает два элемента в один ошибочный путь.
  3. Заключайте маски в кавычки. -cp "lib/*" — незакавыченный lib/* раскрывается оболочкой до того, как java его увидит.
  4. Помните: -jar игнорирует -cp. При запуске с -jar command-line classpath полностью отбрасывается.

Путь к модулям (небольшое отступление)

Начиная с Java 9 у JVM есть также путь к модулям (-p или --module-path), параллельный classpath. Модули — это более строгая, декларативная единица упаковки поверх пакетов. Большинство прикладного кода по-прежнему запускается через classpath; модули наиболее заметны на уровне JDK. Пока вы изучаете основы, можно их игнорировать и вернуться к ним, когда это потребует какой-либо фреймворк.

Разобранный пример

Эта программа показывает classpath изнутри — что JVM фактически загрузила и откуда. Используется только java.lang, поэтому запустить её можно где угодно.

java— editable, runs on the server

java.class.path сообщает application classpath; загрузчик класса String выводится как null, поскольку базовые классы JDK загружаются bootstrap-загрузчиком, а не из какого-либо видимого пользователю элемента classpath. Иерархия загрузчиков классов — это механизм, который заставляет classpath работать; её детали — тема для отдельной продвинутой главы.

Что дальше

На этом тема пакетов и импортов завершается. Теперь у вас есть все составляющие — именование, импорт, объявление, поиск и стандартная библиотека, находящаяся на другом конце каждой строки import. Следующая тема — одно из ключевых дизайнерских решений Java: проверяемые исключения и механизм try/catch/finally для обработки ошибок. Продолжайте на странице Java exceptions.

Практика

Практика
Java-программа нормально запускается командой `java -cp out:lib/dep.jar com.example.App`, но завершается с ошибкой при запуске `java -jar app.jar`, хотя в манифесте `app.jar` указан `Main-Class` и в нём содержится тот же класс `com.example.App`. Почему?
Java-программа нормально запускается командой `java -cp out:lib/dep.jar com.example.App`, но завершается с ошибкой при запуске `java -jar app.jar`, хотя в манифесте `app.jar` указан `Main-Class` и в нём содержится тот же класс `com.example.App`. Почему?
Was this page helpful?