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.
При возникновении любой из этих ошибок проработайте следующий чеклист:
- Выведите classpath, который JVM фактически использовал —
System.getProperty("java.class.path"), как в примере ниже. Набор, который вы считаете переданным, и реально действующий часто различаются. - Проверьте разделитель.
:в macOS/Linux,;в Windows. Неверный разделитель молча превращает два элемента в один ошибочный путь. - Заключайте маски в кавычки.
-cp "lib/*"— незакавыченныйlib/*раскрывается оболочкой до того, какjavaего увидит. - Помните:
-jarигнорирует-cp. При запуске с-jarcommand-line classpath полностью отбрасывается.
Путь к модулям (небольшое отступление)
Начиная с Java 9 у JVM есть также путь к модулям (-p или --module-path), параллельный classpath. Модули — это более строгая, декларативная единица упаковки поверх пакетов. Большинство прикладного кода по-прежнему запускается через classpath; модули наиболее заметны на уровне JDK. Пока вы изучаете основы, можно их игнорировать и вернуться к ним, когда это потребует какой-либо фреймворк.
Разобранный пример
Эта программа показывает classpath изнутри — что JVM фактически загрузила и откуда. Используется только java.lang, поэтому запустить её можно где угодно.
java.class.path сообщает application classpath; загрузчик класса String выводится как null, поскольку базовые классы JDK загружаются bootstrap-загрузчиком, а не из какого-либо видимого пользователю элемента classpath. Иерархия загрузчиков классов — это механизм, который заставляет classpath работать; её детали — тема для отдельной продвинутой главы.
Что дальше
На этом тема пакетов и импортов завершается. Теперь у вас есть все составляющие — именование, импорт, объявление, поиск и стандартная библиотека, находящаяся на другом конце каждой строки import. Следующая тема — одно из ключевых дизайнерских решений Java: проверяемые исключения и механизм try/catch/finally для обработки ошибок. Продолжайте на странице Java exceptions.