Перейти к основному содержимому

JAR-файлы с несколькими выпусками

· 5 мин. чтения

1. Обзор

Java постоянно развивается и добавляет новые функции в JDK. И если мы хотим использовать эти функции в наших API, это может потребовать от нижестоящих зависимостей обновить свою версию JDK.

Иногда мы вынуждены ждать использования новых языковых функций , чтобы сохранить совместимость.

Однако в этом руководстве мы узнаем о JAR-файлах с несколькими выпусками (MRJAR) и о том, как они могут одновременно содержать реализации, совместимые с разными версиями JDK.

2. Простой пример

Давайте взглянем на служебный класс DateHelper , в котором есть метод проверки високосных лет . Предположим, что он был написан с использованием JDK 7 и создан для работы на JRE 7+:

public class DateHelper {
public static boolean checkIfLeapYear(String dateStr) throws Exception {
logger.info("Checking for leap year using Java 1 calendar API ");

Calendar cal = Calendar.getInstance();
cal.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(dateStr));
int year = cal.get(Calendar.YEAR);

return (new GregorianCalendar()).isLeapYear(year);
}
}

Метод checkIfLeapYear будет вызываться из основного метода нашего тестового приложения:

public class App {
public static void main(String[] args) throws Exception {
String dateToCheck = args[0];
boolean isLeapYear = DateHelper.checkIfLeapYear(dateToCheck);
logger.info("Date given " + dateToCheck + " is leap year: " + isLeapYear);
}
}

Давайте перенесемся в сегодняшний день.

Мы знаем, что в Java 8 есть более лаконичный способ разбора даты . Итак, мы хотели бы воспользоваться этим и переписать нашу логику. Для этого нам нужно перейти на JDK 8+. Однако это означало бы, что наш модуль перестанет работать с JRE 7, для которой он изначально был написан.

И мы не хотим, чтобы это произошло, если только в этом нет крайней необходимости.

3. Файлы JAR с несколькими выпусками

Решение в Java 9 состоит в том, чтобы оставить исходный класс нетронутым и вместо этого создать новую версию с использованием нового JDK и упаковать их вместе . Во время выполнения JVM (версия 9 или выше) будет вызывать любую из этих двух версий , отдавая предпочтение самой высокой версии, которую поддерживает JVM .

Например, если MRJAR содержит Java версии 7 (по умолчанию), 9 и 10 того же класса, то JVM 10+ будет выполнять версию 10, а JVM 9 будет выполнять версию 9. В обоих случаях версия по умолчанию не будет выполняться как для этой JVM существует более подходящая версия.

Обратите внимание, что общедоступные определения новой версии класса должны точно соответствовать исходной версии . Другими словами, нам не разрешено добавлять какие-либо новые общедоступные API исключительно в новую версию.

4. Структура папок

Поскольку классы в Java напрямую сопоставляются с файлами по их именам, создание новой версии DateHelper в том же месте невозможно. Следовательно, нам нужно создать их в отдельной папке.

Начнем с создания папки java9 на том же уровне, что и java . После этого клонируем файл DateHelper.java , сохраняя структуру папок пакета, и помещаем его в java9:

src/
main/
java/
com/
foreach/
multireleaseapp/
App.java
DateHelper.java
java9/
com/
foreach/
multireleaseapp/
DateHelper.java

Некоторые IDE, которые еще не поддерживают MRJAR , могут выдавать ошибки для повторяющихся классов DateHelper.java .

Мы рассмотрим, как интегрировать это с инструментами сборки, такими как Maven, в другом руководстве. А пока давайте просто сосредоточимся на основах.

5. Изменения кода

Перепишем логику клонированного класса java9 :

public class DateHelper {
public static boolean checkIfLeapYear(String dateStr) throws Exception {
logger.info("Checking for leap year using Java 9 Date Api");
return LocalDate.parse(dateStr).isLeapYear();
}
}

Обратите внимание, что мы не вносим никаких изменений в сигнатуры общедоступных методов клонированного класса, а меняем только внутреннюю логику. В то же время мы не добавляем никаких новых общедоступных методов.

Это очень важно, поскольку создание jar не удастся, если не соблюдать эти два правила.

6. Кросс-компиляция в Java

Кросс-компиляция — это функция Java, которая может компилировать файлы для работы в более ранних версиях. Это означает, что нам не нужно устанавливать отдельные версии JDK.

Давайте скомпилируем наши классы, используя JDK 9 или выше.

Во-первых, скомпилируйте старый код для платформы Java 7:

javac --release 7 -d classes src\main\java\com\foreach\multireleaseapp\*.java

Во-вторых, скомпилируйте новый код для платформы Java 9:

javac --release 9 -d classes-9 src\main\java9\com\foreach\multireleaseapp\*.java

Опция выпуска используется для указания версии компилятора Java и целевой JRE.

7. Создание MRJAR

Наконец, создайте файл MRJAR, используя версию 9+:

jar --create --file target/mrjar.jar --main-class com.foreach.multireleaseapp.App
-C classes . --release 9 -C classes-9 .

Опция выпуска , за которой следует имя папки, позволяет упаковать содержимое этой папки в файл jar под значением номера версии:

com/
foreach/
multireleaseapp/
App.class
DateHelper.class
META-INF/
versions/
9/
com/
foreach/
multireleaseapp/
DateHelper.class
MANIFEST.MF

В файле MANIFEST.MF установлено свойство, позволяющее JVM знать, что это файл MRJAR:

Multi-Release: true

Следовательно, JVM загружает соответствующий класс во время выполнения.

Старые JVM игнорируют новое свойство, указывающее, что это файл MRJAR, и обрабатывают его как обычный файл JAR.

8. Тестирование

Наконец, давайте протестируем нашу банку на Java 7 или 8:

> java -jar target/mrjar.jar "2012-09-22"
Checking for leap year using Java 1 calendar API
Date given 2012-09-22 is leap year: true

А затем давайте снова протестируем банку на Java 9 или более поздней версии:

> java -jar target/mrjar.jar "2012-09-22"
Checking for leap year using Java 9 Date Api
Date given 2012-09-22 is leap year: true

9. Заключение

В этой статье мы увидели, как создать файл jar с несколькими выпусками, используя простой пример.

Как всегда, кодовая база для мультирелизного приложения доступна на GitHub .