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 .