Управление Java classpath (UNIX и Mac OS X)
Classpath представляет собой связующее звено между исполняемым модулем Java и файловой системой. Он определяет, где интерпретатор ищет файлы класса для загрузки. Основная идея состоит в том, что иерархия файловой системы отражает иерархию пакета Java, а classpath определяет, какие директории в файловой системе служат корневыми каталогами для иерархии пакета Java.
К несчастью, файловые системы сложны, и сильно зависят от платформы, и они не точно соответствуют пакетам Java. Соответственно, classpath долгие годы является источником постоянного раздражения как для новых пользователей, так и для опытных Java-программистов. Он — далеко не самый приятный компонент Java-платформы. Он — раздражающая помеха, которая вынуждает вас задерживаться на работе в попытках устранить маленькую проблему, которая упорно не хочет решаться.
Хорошая IDE типа Eclipse может защитить вас от некоторых трудностей управления classpath, но только отчасти, и только если ничего не случится (а что-то постоянно случается). Следовательно, каждый Java-программист непременно должен полностью понимать classpath. Только при глубоком понимании вы можете надеяться на устранение сложных проблем, которые возникают из-за classpath.
В этой статье я излагаю все, что вам нужно знать о Java classpath (и соответствующем sourcepath) в UNIX, Linux, и Mac OS X. Следование методам, изложенным здесь, послужит вам руководством на этом пути и должно помочь в решении большинства проблем с classpath.
Структура пакета
Освоение classpath начинается с исходного кода. Каждый класс принадлежит пакету, и этот пакет программ должен следовать стандартным соглашениям по именованию. Сделаем краткий обзор: Имя пакета начинается с двухуровневого обратного имени домена, например com.example или edu.poly. За ним следует по крайней мере еще одно слово, которое описывает содержимое пакета программ. Например, так как я являюсь владельцем имени домена elharo.com, если бы я должен был написать класс Fraction, я мог бы расположить его в одном из следующих пакетов:
- com.elharo.math
- com.elharo.numbers
- com.elharo.math.algebra.fields
После обратного имени домена используйте только имена подпакета, состоящие из одного слова. Не используйте сокращения и пишите все слова правильно. Используйте программу проверки орфографии, если вам это нужно. Большой процент проблем, связанных с classpath, вызван использованием одного слова в исходной программе и слегка измененного написания или сокращения этого слова в файловой системе. Единственный разумный выбор — всегда использовать правильно написанные, несокращенные имена.
Полное имя пакета должно быть написано строчными буквами, даже для имен собственных и аббревиатур, которые обычно пишутся заглавными буквами. Имя пакета должно быть составлено только из ASCII-символов. Хотя компилятор принимает имена пакета, записанные на иврите, кириллице, греческом, и других шрифтах, многие файловые системы не принимают их. Как вы скоро увидите, эти имена пакетов будут выполнять двойную задачу в качестве имен директорий. Следовательно, имена пакетов (и классов) должны быть ограничены ASCII. (В то время как в пакетах Java и именах классов используется юникод, многие файловые системы ещё его не понимают. Простое копирование файла в систему с другой кодировкой по умолчанию может помешать компилятору и интерпретатору найти правильные классы.)
Одноразовая программа
Если вы просто пишите отдельный класс, чтобы проверить вашу интуицию в отношении API, и вы выбросите его сразу же после того, как вы его однократно запустите, тогда вам не нужно вкладывать его в пакет. Тем не менее, любой класс, который будет использоваться чаще, чем один раз, должен быть в пакете.
Не экономьте на имени вашего пакета! Это только приведет к катастрофе в конечном счете. Если вам нужно имя домена, купите его. Если имена слишком длинные, приобретите имя покороче. (Я однажды приобрел xom.nu, таким образом мой префикс пакета составлял только шесть символов.) Не располагайте ваши классы в пакете по умолчанию (пакете, который вы получаете, если не включаете оператор пакета в класс). Если доступ пакета мешает объектам посылать сообщения, добавьте больше общедоступных методов в классы. Каждый класс, который будет использоваться чаще, чем один раз, должен быть в пакете.
Структура директории
Следующий шаг — это организовать ваши исходные файлы так, чтобы они соответствовали структуре пакета. Создайте где-нибудь чистую, пустую директорию. Для целей этой статьи я назову ее project (проект). Внутри этой директории создайте еще две директории: bin и src. (Некоторые люди предпочитают называть их build и source соответственно.)
Затем внутри директории src создайте иерархию, которая зеркально отображает вашу иерархию пакета. Например, при классе с именем com.elharo.math.Fraction, я бы расположил директорию com в директории src. Затем я бы создал директорию elharo внутри директории com. Затем я бы поместил директорию math внутрь директории elharo. И наконец, я бы поместил Fraction.java внутри этой директории math, как показано на рисунке 1:
Рисунок 1. Структура директории отражает структуру пакета
Очень важно: никогда не помещайте ничего, кроме исходного кода в вашу директорию src. Обычно единственные файлы, которые вы помещаете туда, это .java-файлы. Иногда вы можете расположить .html-файлы (для Javadoc) или другие типы исходного кода в эту директорию. Тем не менее, вы не должны помещать .class-файлы или другие скомпилированные, сгенерированные артефакты в эту иерархию. Выполнение этого действия непременно приведет к катастрофе. К сожалению, компилятор javac сделает именно так, если вы не проявите осторожность. В следующем разделе я покажу вам, как наладить это.
Компилирование
Компилирование кода Java — коварная вещь, потому что вам нужно отслеживать несколько связанных, но разных вещей:
- Целевой файл, который вы компилируете.
- Директорию, где компилятор ищет .java-файлы, которые импортирует целевой файл.
- Директорию, где компилятор ищет .class-файлы, которые импортирует целевой файл.
- Директорию, куда компилятор помещает скомпилированный вывод.
По умолчанию, компилятор javac думает, что все это — текущая рабочая директория, что почти всегда не то, что вам нужно. Следовательно, при компиляции вы должны четко точно определить каждый из элементов.
Файл для компиляции
Первое, что вы определяете, — это .java-файл, который вы собираетесь компилировать. Это дается как путь к файлу из текущей рабочей директории. Например, предположим, что вы в директории project, как показывает Рисунок 1. Эта директория содержит директорию src. Директория src содержит директорию com, которая содержит директорию example, которая содержит файл Fraction.java. Следующая командная строка компилирует его:
$ javac src/com/elharo/math/Fraction.java
Если путь неверен, вы получите сообщение об ошибке следующего рода:
error: cannot read: src/com/example/mtah/Fraction.java
Если вы видите такое сообщение об ошибке, проверьте каждый фрагмент пути, чтобы убедиться, что он написан правильно. Затем проверьте, находится ли файл именно там, где он должен находиться, выполнив ls, как показано здесь:
$ ls src/com/example/math ls: src/com/example/math: No such file or directory
Эта проблема обычно указывает на неверно набранный путь, но она также может означать, что вы не находитесь в той директории, в которой вы рассчитываете находиться. В этом примере вы должны убедиться, что текущая рабочая директория — это директория проекта. Здесь поможет команда pwd. Например, следующее сообщает мне, что на самом деле я в project/src, а не в директории project:
$ pwd /Users/elharo/documents/articles/classpath/project/src
Мне нужно выполнить команду cd ... перед компилированием.
Куда идет вывод
Предполагая, что синтаксические ошибки отсутствуют, javac располагает скомпилированный .class-файл в той же директории, где находится .java файл. Вам это не нужно. Смешивание файлов .class и .java затрудняет очистку скомпилированных файлов без случайного удаления .java-файлов, которые вы хотите сохранить. Это делает чистую сборку проблематичной и ведет к возникновению различных проблем. Это также затрудняет упаковку в jar только что скомпилированных .class-файлов при распространении двоичного файла. В связи с этим вам нужно велеть компилятору поместить скомпилированный вывод в совершенно другую директорию. Переключатель -d точно определяет директорию вывода (обычно называемую bin, build, или classes):
$ javac -d bin src/com/elharo/math/Fraction.java
Сейчас вывод такой, как показано на рисунке 2. Обратите внимание, что javac создал полную иерархию директорий com/elharo/math. Вам не нужно делать это вручную.
Рисунок 2. Параллельные иерархии исходных и скомпилированных файлов
Путь к исходным файлам (sourcepath)
Директория, где Java ищет исходные файлы называется sourcepath. В схеме, намеченной здесь, это src директория. Это директория, которая содержит иерархию исходных файлов, организованных в их собственных директориях. Это не директория com или src/com/elharo/math.
Большинство проектов используют больше, чем один класс и больше, чем один пакет. Они связаны импортирующими операторами и полностью подготовленными для пакета именами класса. Например, предположим, что вы сейчас создаете новый класс MainFrame в пакете com.elharo.gui, как показано в листинге 1:
Листинг 1. Класс в одном пакете может импортировать класс в другом
package com.elharo.gui; import com.elharo.math.*; public class MainFrame { public static void main(String[] args) { Fraction f = new Fraction(); // ... } }
Этот класс использует класс com.elharo.math.Fraction в другом пакете из класса MainFrame. Установка исходных файлов сейчас такова, как показано на рисунке 3. (Я удалил скомпилированный вывод из предыдущего шага. Я всегда смогу скомпилировать его снова.)
Рисунок 3. Структура исходных файлов для нескольких пакетов
Сейчас давайте посмотрим, что происходит, когда я пытаюсь скомпилировать MainFrame.java так, как я делал это ранее:
Листинг 2. Компилирование MainFrame.java
$ javac -d bin src/com/elharo/gui/MainFrame.java src/com/elharo/gui/MainFrame.java:3: package com.elharo.math does not exist import com.elharo.math.*; ^ src/com/elharo/gui/MainFrame.java:7: cannot find symbol symbol : class Fraction location: class com.elharo.gui.MainFrame private Fraction f = new Fraction(); ^ src/com/elharo/gui/MainFrame.java:7: cannot find symbol symbol : class Fraction location: class com.elharo.gui.MainFrame private Fraction f = new Fraction(); ^ 3 errors
Ошибки в листинге 2 произошли, потому что хотя javac знал, где найти MainFrame.java, он не знал, где найти Fraction.java. (Вы подумаете, что будет достаточно разумно заметить соответствующие иерархии пакета, но это не так.) Раскрывая секрет, я должен указать sourcepath. Он указывает директории, где компилятор ищет иерархию исходных файлов. В листинге 2 это src. Итак, я использую опцию -sourcepath, например, так:
$ javac -d bin -sourcepath src src/com/elharo/gui/MainFrame.java
Сейчас программа компилируется без ошибок и производит вывод, показанный на рисунке 4. Обратите внимание, что javac также скомпилировал файл Fraction.java, на который ссылается файл, который я компилировал.
Рисунок 4. Многоклассовый вывод
Компилирование множественных директорий в sourcepath
На самом деле вы можете иметь более одной директории в вашем sourcepath (они разделяются двоеточиями), хотя это обычно не нужно. Например, если я хочу включить и локальную директорию src, и директорию /Users/elharo/Projects/XOM/src, где я храню исходный код для другого проекта, я могу компилировать следующим образом:
$ javac -d bin -sourcepath src:/Users/elharo/Projects/XOM/src src/com/elharo/gui/MainFrame.java
Эта команда не компилирует каждый файл, найденный в какой-либо из этих иерархий. Она компилирует только файлы, на который делается ссылка прямо или косвенно одним .java-файлом, который я явно прошу скомпилировать.
Гораздо чаще у вас будет одна директория source для .java файлов, но много директорий для классов или JAR-архивов, где расположены прекомпилированные библиотеки сторонних производителей. Такова роль classpath.
Установка classpath
В средних и больших проектах перекомпилирование каждого файла каждый раз может занимать очень много времени. Вы можете облегчить нагрузку, отдельно компилируя и храня независимые части одного и того же проекта в разных классах или директориях bin. Эти директории добавляются к classpath.
Есть несколько способов добавить класс к classpath. Переключатель командной строки -classpath, тем не менее, — единственное, что следует использовать. Например, предположим, что я хочу импортировать файлы из другого проекта, который я до этого скомпилировал в директорию /Users/elharo/classes. Затем я бы добавил -classpath /Users/elharo/classes к командной строке следующим образом:
$ javac -d bin -sourcepath src -classpath /Users/elharo/classes src/com/elharo/gui/MainFrame.java
Теперь предположим, что мне нужно добавить две директории, /Users/elharo/project1/classes и /Users/elharo/project2/classes. Тогда я бы включил обе, разделив их двоеточием, как показано далее:
$ javac -d bin -sourcepath src -classpath /Users/elharo/project1/classes:/Users/elharo/project2/classes src/com/elharo/gui/MainFrame.java
Директории верхнего уровня
Обратите внимание, что директории, которые я указываю здесь, все являются директориями верхнего уровня, которые содержат иерархию, подобную com/elharo/foo/bar or nu/xom/util. Директории, имена которых соответствуют именам пакета (com, elharo, math, etc.) никогда не включаются прямо в sourcepath или classpath.
Конечно, вы можете использовать различные формы соответствующих путей, если вы предпочитаете. Например, если project1 и project2 — «братья» текущей рабочей директории (то есть, они имеют одну и ту же родительскую директорию), тогда я бы мог сделать на них ссылку на них следующим образом:
$ javac -d bin -sourcepath src -classpath ../project1/classes:../project2/classes src/com/elharo/gui/MainFrame.java
До сих пор я предполагал, что программа завершается сама и не использует никакие отдельно скомпилированные библиотеки сторонних производителей. Но если она использует их, вам нужно их также добавить к classpath. Библиотеки обычно размещаются как JAR-файлы, например junit.jar или icu4j.jar. В этом случае вы добавляете именно JAR-файл к classpath, а не директорию, которая содержит его. (По существу, JAR-файл действует как директория, которая содержит скомпилированные .class-файлы.) Например, следующая команда добавляет три вещи к classpath: директорию /Users/elharo/classes, файл icu4j.jar в текущей рабочей директории, и файл junit.jar в /Users/elharo/lib:
$ javac -d bin -sourcepath src -classpath /Users/elharo/classes:icu4j.jar:/Users/elharo/lib/junit.jar src/com/elharo/gui/MainFrame.java
JAR-файлы используются только для .class-файлов и classpath, а не для .java-файлов и sourcepath.
Запуск программы
Сейчас вы успешно скомпилировали вашу программу и готовы запустить ее. Это похоже на компилирование, но проще его. Во время запуска программы вам нужно определить только две вещи:
- classpath.
- Полностью классифицированное для пакета имя класса, которое содержит ваш main () method.
Вам не нужно указывать sourcepath.
Обычно classpath — это тот же самый classpath, который вы использовали, чтобы скомпилировать программу, с добавлением директории, куда был размещен скомпилированный вывод. Например, если команда компилирования была следующей:
$ javac -d bin -sourcepath src -classpath /Users/elharo/classes:/Users/elharo/lib/junit.jar src/com/elharo/gui/MainFrame.java
и метод main () был в классе com.elharo.gui.MainFrame, тогда вы будете запускать программу следующим образом:
$ java -classpath bin:/Users/elharo/classes:/Users/elharo/lib/junit.jar com.elharo.gui.MainFrame
Обратите особое внимание, что последний элемент командной строки — это имя класса. Это не имя файла. Оно не заканчивается на .java или .class. Этот класс должен быть найден где-нибудь в classpath.
Другие места, где располагаются классы
Я настоятельно рекомендую вам точно указывать classpath, когда вы компилируете и когда запускаете программу. Есть другие места, куда вы можете помещать файлы так, что они добавляются к classpath и обнаруживаются и компилятором javac и интерпретатором java. Эти опции экономят лишь немного набора текста, и они делают это за счет большого количества отлаживания, когда — а не если — вы случайно располагаете старую версию класса в classpath.
В этом разделе я покажу вам некоторые места, где вы можете найти прячущиеся классы, которые неожиданно появляются в вашем classpath и вызывают проблемы. Это наиболее вероятно может произойти на машинах, которые вы не контролируете, например на сервере.
Текущая рабочая директория
Компилятор всегда добавляет текущую рабочую директорию (.) к classpath, запрашиваете вы это явно или нет. Очень легко забыть, что находится или не находится в той же директории, где и вы. Таким образом, попытайтесь избежать размещения любых классов или иерархий в ваш проект или начальную директорию. Вместо этого всегда храните вещи аккуратно разделенными на src-директории для .java-файлов и директории bin для .class-файлов.
CLASSPATH
Через некоторое время вы можете устать от ручного добавления директорий bin и JAR-архивов к classpath. Тогда вы можете открыть для себя переменную среды classpath. Вы можете добавить директории и JAR-архивы к переменной среды classpath только один раз. Затем вам не придется набирать их пути каждый раз, когда вы запускаете javac или java.
Сопротивляйтесь соблазну. Это только вызовет новые проблемы, если вы загрузите неверный класс или неверную версию класса. Любое время, которое вы сэкономите на наборе текста, будет отнято у вас отладкой проблем, вызванных случайной загрузкой неверных классов, и оно будет в сотни раз больше. Есть более надежные способы автоматизировать classpath и избежать набора текста.
jre/lib/ext
JAR-архивы, расположенные в вашей jre/lib/ext директории, добавляются к classpath всех приложений, запущенных на данной виртуальной машине. Хотя это кажется удобным, эта ошибка даст о себе знать позже, она сродни добавлению директорий к переменной среды classpath. Рано или поздно (возможно рано), вы загрузите неверную версию класса из места, о котором вы даже и не думаете, и потеряете много времени на отладку.
Эта проблема особенно серьезна во время использования серверных приложений. Убедитесь, что сервер, который вы используете, не имеет никаких дополнительных JAR'ов в своей директории jre/lib/ext. Проблемы, вызванные неверной версией JAR-архива в classpath может быть чрезвычайно трудно устранить, если вы не распознаете признаки или не знаете, что конкретно искать. Чтобы избежать этих проблем, некоторые среды разработки зашли настолько далеко, что пишут свои собственные программы загрузки классов, которые обходят обычные загрузочные механизмы кода Java.
jre/lib/endorsed
JAR-файлы в директории jre/lib/endorsed также добавляются к classpath всех приложений, запущенных на данной виртуальной машине. Разница заключается в том, что эти файлы на самом деле добавляются скорее к bootclasspath, чем обычному classpath, и могут замещать стандартные классы, поставляющиеся с JDK. Этот подход особенно полезен для модернизации XML-анализатора и устранения ошибок в виртуальной машине.
Еще раз повторю, что хотя этот способ кажется удобным, это долговременная ошибка по той же самой причине. Если вам нужно переместить классы JDK, используйте опцию -Xbootclasspath/p во время запуска, чтобы избежать случайной загрузки неверной версии класса:
$ java -classpath /Users/elharo/classes -Xbootclasspath/p:xercesImpl.jar com.elharo.gui.MainFrame
Автоматизация управления classpath
Вам следует научиться использовать молоток, прежде чем вы возьмете в руки «пистолет» для забивания гвоздей. Точно так же, вы сперва должны научится чувствовать себя уверенно при ручном управлении классами, прежде чем вы попытаетесь использовать более мощные инструменты. Тем не менее, существуют инструменты, разработанные для того, чтобы уменьшить мучения от операций с sourcepath и classpath. Чаще всего они делают это, организуя для вас файлы в соответствии с теми принципами, которые я изложил в этой статье.
IDE
Большинство интегрированных сред разработки, например Eclipse и NetBeans автоматизируют управление classpath и помогают в некоторых его аспектах. Например, когда вы меняете имя пакета, Eclipse предлагает передвинуть соответствующий .java-файл, чтобы он совпадал, как показано на рисунке 5:
Рисунок 5. Быстрое исправление classpath в Eclipse
Помните, однако, что эти IDE по-прежнему располагаются на вершине файловой системы, что должно быть должным образом настроено, особенно, если вам нужно осуществить интеграцию с другими инструментами и другими IDE. Главный вклад этих инструментов состоит в том, что переключатели командной строки заменяются графическими диалогами, древовидными списками и вкладками; основная же файловая структура остается прежней.
Ant
Ant — это, фактически, стандартный инструмент для автоматизации процесса сборки. В отличие от расположения директорий в jre/lib/ext или в переменной среды CLASSPATH, Ant на самом деле позволяет вам создать пошаговые процессы сборки. Вам все еще нужно настроить classpath в файле build.xml редактора Ant и вручную расположить исходные файлы в правильных директориях, но, по крайней мере, вам не нужно указывать их повторно каждый раз во время компиляции.
Maven
Maven идет еще дальше, чем Ant в организации и автоматизации процесса сборки и связанных с ним операций с classpath. Maven обеспечивает рациональную установку по умолчанию, которая помогает вам компоновать простые проекты лишь с несколькими строками кода, если вы располагает исходные файлы там, где Maven ожидает их найти. Вам все-таки придется координировать иерархию файловой системы и иерархию пакета. Maven особенно полезен при управлении зависимостями в библиотеках сторонних производителей, хотя его не так легко настраивать, как Ant.
В заключение
Даже такую непростую проблему как classpath, вы можете решить, используя несколько простых правил. В частности:
- Располагайте каждый класс в пакете.
- Точно следуйте стандартам называния пакета и класса и правилам использования прописных букв.
- Убедитесь, что иерархия вашего пакета соответствует иерархии директории.
- Всегда используйте опцию -d для javac.
- Никогда ничего не помещайте в jre/lib/ext.
- Никогда ничего не помещайте в jre/lib/endorsed.
- Никогда не помещайте .java-файлы в ту же самую директорию, что и .class-файлы.
- Никогда не помещайте никакие .java или .class-файлы в текущую рабочую директорию.
Один последний совет: много проблем с classpath, требующих большого расхода времени, вращаются вокруг простых ошибок, таких как неправильное написание имени директории или компилирование из неверной директории. Если вы не можете определить, что идет не так, попросите друга или коллегу взглянуть на вашу проблему. Часто я обнаруживаю, что нахожусь слишком близко к ошибке, чтобы её увидеть, а для кого-то ещё она сразу же очевидна. Еще одна пара глаз — это эффективный метод устранения ошибок.
Classpath не легок, но он управляем, и есть способ справиться с его сумасшествием. Немного осторожности и внимания к заданию имён, аргументам командной строки, и структуре директорий должны помочь вам скомпилировать и запустить программы с минимальными трудностями.