Статьи

JAR Manifest Class-Path не только для запуска приложений Java

Почти с тех пор, как я начал узнавать о Java, поле заголовка Class-Path в файле Manifest задает относительный путь к классу времени выполнения для исполняемых JAR (JAR с начальной точкой приложения, указанной в другом манифесте с именем Main-Class ). Недавно коллега столкнулся с проблемой, которая удивила меня, поскольку доказала, что запись Class-Path к Class-Path в манифесте файла JAR также влияет на Class-Path к классам времени компиляции, когда содержащий JAR включается в Class-Path к классам во время выполнения javac . Этот пост демонстрирует этот новый для меня нюанс.

В разделе « Добавление классов в путь к классам JAR-файла» в журнале развертывания Учебных материалов по Java говорится: «Вы указываете классы для включения в поле заголовка Class-Path в файле манифеста апплета или приложения». В этом же разделе также говорится: «Используя заголовок Class-Path в манифесте, вы можете избежать указания длинного флага -classpath при вызове Java для запуска вашего приложения». Эти два предложения в основном суммируют то, как я всегда думал о заголовке Class-Path в файле манифеста: как Class-Path к Class-Path для содержащего JAR, выполняемого через средство запуска приложения Java (исполняемый файл java ).

Оказывается, что запись Class-Path в манифесте JAR влияет на компилятор Java ( javac ) так же, как он влияет на панель запуска приложений Java ( java ). Чтобы продемонстрировать это, я собираюсь использовать простой интерфейс ( PersonIF ), простой класс ( Person ), который реализует этот интерфейс, и простой класс Main который использует класс, который реализует интерфейс. Списки кода приведены ниже для них.

PersonIF.java

1
2
3
4
public interface PersonIF
{
   void sayHello();
}

Person.java

1
2
3
4
5
6
7
8
9
import static java.lang.System.out;
 
public class Person implements PersonIF
{
   public void sayHello()
   {
      out.println("Hello!");
   }
}

Main.java

1
2
3
4
5
6
7
8
public class Main
{
   public static void main(final String[] arguments)
   {
      final Person person = new Person();
      person.sayHello();
   }
}

Как видно из приведенных выше списков кода, класс Main зависит от (использует) класс Person а класс Person зависит от (реализует) PersonIF . Я намеренно PersonIF.class файл PersonIF.class в его собственный JAR- PersonIF.jar и PersonIF.jar этот JAR-файл в (другом) подкаталоге. Файл Person.class будет существовать в своем собственном JAR-файле Person.jar и этот JAR-файл содержит MANIFEST.MF file с заголовком Class-Path ссылающимся на PersonIF.jar в относительном подкаталоге.

manifestFileInPersonJarReferencesArchivePersonIFJar

Сейчас я Main.class скомпилировать Main.class из Main.java только с текущим каталогом в classpath. Ранее я ожидал, что компиляция потерпит неудачу, когда javac не сможет найти PersonIF.jar в отдельном подкаталоге. Тем не менее, это не подводит!

compilingMainJavaWithoutExplicitlySpecifyingPersonIFOnCP

Это казалось мне удивительным. Почему это компилируется, когда я не указал явно PersonIF.class (или JAR, содержащий его) в качестве значения classpath, предоставленного с помощью флага -cp ? Ответ можно увидеть, запустив javac с флагом -verbose .

compilingMainJavaWoutExplicitSpecifyPersonIFOnCP_verbose

Выходные данные javac -verbose предоставляют «путь поиска исходных файлов» и «путь поиска файлов классов ». «Путь поиска для файлов классов» был в этом случае значительным, потому что я переместил исходные файлы PersonIF.java и Person.java в совершенно не связанный каталог, а не в указанные пути поиска. Интересно видеть, что путь поиска файлов классов (а также путь поиска исходных файлов) включает archive/PersonIF.jar хотя я не указал этот JAR (или даже его каталог) в значении -cp . Это демонстрирует, что предоставленный Oracle компилятор Java учитывает содержимое пути к Class-Path указанное в заголовке Class-Path файла MANIFEST.MF любого JAR-файла, указанного в пути к классам.

Следующий снимок экрана демонстрирует запуск вновь скомпилированного класса Main.class и получение зависимости PersonIF.class из archive/PersonIF.jar без archive/PersonIF.jar его в значении, передаваемом флагу java -cp archive/PersonIF.jar запуска приложения java -cp . Я ожидал, что поведение во время выполнения будет таким, хотя по общему признанию я никогда не пробовал это или даже думал о том, чтобы сделать это с JAR, у которого файл MANIFEST.MF не имел заголовка Main-Class (неисполняемый JAR). В Person.jar манифеста Person.jar в этом примере не указан заголовок Main-Class а указан только заголовок Class-Path , но он по-прежнему мог использовать содержимое этого classpath во время выполнения при вызове с java .

Последняя демонстрация для этого поста включает в себя удаление заголовка Class-Path и связанного с ним значения из файла JAR и попытку компиляции с использованием javac и того же указанного в командной строке classpath. В этом случае JAR, содержащий Person.class , называется Person2.jar и на следующем снимке экрана показано, что его файл MANIFEST.MF не имеет заголовка Class-Path .

Person2JarManifestWithoutClassPathForPersonIF

Следующий снимок экрана демонстрирует, что компиляция с использованием javac теперь завершается неудачно, поскольку, как и ожидалось, PersonIF.class явно не указан в пути к классам и больше не доступен по ссылке из заголовка MANIFEST.MF Class-Path JAR, который находится на CLASSPATH.

notCompilingMainWoutPersonIfInManifestClassPath

Из предыдущего снимка экрана видно, что пути поиска для исходных файлов и файлов классов больше не включают в себя archive/PersonIF.jar . Без этого JAR-файла javac не может найти PersonIF.class и сообщает об ошибке: «файл класса для PersonIF не найден».

Общие замечания

  • Заголовок Class-Path в файле MANIFEST.MF не зависит от существования заголовка Main-Class существующего в том же файле JAR MANIFEST.MF .
    • JAR с заголовком манифеста Class-Path сделает эти записи пути к классам доступными для загрузчика классов Java независимо от того, выполняется ли этот JAR с помощью java -jar ... или просто помещается в java -jar ... к классам более крупного Java-приложения.
    • JAR с заголовком манифеста Class-Path сделает эти записи пути к классам доступными для компилятора Java ( javac ), если этот JAR включен в Class-Path к классам, указанный для компилятора Java.
  • Поскольку использование Class-Path в файле манифеста JAR не ограничивается областью действия JAR, чей Main-Class выполняется, зависимости класса могут быть потенциально непреднамеренно (возможно, даже с неправильными версиями) ими, вместо того, чтобы разрешать явно указанные записи пути к классам , Следует соблюдать осторожность при создании JAR с манифестами, в которых указан Class-Path или при использовании сторонних JAR с Class-Path указанным в их файлах манифеста.
  • Важность файла манифеста JAR иногда недооценивается, но эта тема является напоминанием о полезности знания того, что находится в конкретном файле манифеста JAR .
  • Эта тема является напоминанием о том, что время от времени можно запускать javac без -verbose чтобы узнать, что это такое.
  • Всякий раз, когда вы помещаете JAR в путь к классам компилятора javac или в программу запуска приложений java , вы помещаете в путь к классам не только определения классов в этом JAR; вы также помещаете любые классы и JAR, на которые ссылается Class-Path этого манифеста JAR, в Class-Path к классам компилятора или модуля запуска приложения.

Вывод

Существует много мест, из которых загрузчик классов Java может загружать классы для создания и запуска приложений Java. Как показано в этом посте, заголовок Class-Path файла MANIFEST.MF в JAR является еще одной точкой соприкосновения для определения того, какие классы загрузчик классов будет загружать как во время выполнения, так и во время компиляции. Использование Class-Path не влияет только на JAR, которые являются «исполняемыми» (имеют заголовок Main-Class указанный в их файле манифеста и запускаются с java -jar ... ), но могут влиять на загруженные классы для компиляции и для любого Выполнение приложения Java, в котором JAR с файлом манифеста, содержащим заголовок Class-Path находится в пути к классам.