Статьи

Поддержка Java 8

Хотя Java имеет версию 13, выпущенную на данный момент, есть много производственных установок, работающих с Java 8. Как профессионал, я разрабатываю код Java 8 много раз даже в эти дни, и я должен быть счастлив, что это не Java 6. Вкл. С другой стороны, как разработчик с открытым исходным кодом, я имею право разрабатывать свой код Java с использованием Java 11, 12 или даже 13, если это мне нравится. И это так.

С другой стороны, я хочу, чтобы мой код использовался. Разработка такого инструмента, как License3j или Java :: Geci, которые являются своего рода библиотеками, выпускающими совместимый с Java 11 байт-код, отключает все приложения на основе Java 8, которые могут использовать эти библиотеки.

Я хочу, чтобы библиотеки были доступны из Java 8.

Одно из решений состоит в том, чтобы поддерживать две ветви в репозитории Git и иметь версию кода Java 11+ и Java 8. Это то, что я сделал для выпуска Java :: Geci 1.2.0. Это громоздко, подвержено ошибкам и много работы. У меня был этот код только потому, что мой сын, который также является разработчиком Java, начинал свою карьеру.

(Нет, я не оказывал на него давления. Он говорит и пишет лучше по-английски, чем я, и он регулярно просматривает эти статьи, исправляя мои сломанные языки. Если у него другое мнение о давлении, он может свободно вставить сюда любую заметку до закрытия. в скобках я не буду удалять или изменять это. ПРИМЕЧАНИЕ:)

Все, что находится между NOTE: а ) является его мнением.

Другая возможность — использовать Jabel .

В этой статье я напишу о том, как я использовал Jabel в проекте Java :: Geci. Документация по Jabel короткая, но все еще полная, и она действительно работает для простых проектов. Например, мне действительно нужно было добавить несколько строк в pom.xml в случае проекта Licenese3j. Для более сложных проектов, которые были разработаны в течение года без каких-либо компромиссов для совместимости с Java 8, это немного сложнее.

О Джебель

Jabel — это проект с открытым исходным кодом, доступный по адресу https://github.com/bsideup/jabel . Если у вас есть исходный код проекта Java 9+, вы можете настроить Jabel для участия в процессе компиляции. Это процессор аннотаций, который подключается к процессу компиляции и отчасти обманывает компилятор, чтобы он принимал функции Java 9+ такими, какими они были доступны для Java 8. Компилятор будет работать и генерировать Java 8, Jabel не мешает генерации байт-кода , так что это настолько искренне, насколько это может быть свежим и теплым из компилятора Java. Он только указывает компилятору не волноваться о возможностях Java 9+ при компиляции кода.

Как это работает и почему это может работать, хорошо написано на странице проекта GitHub. То, что я написал выше, может даже не быть точным.

Проблемы с бэкпортом

При создании кода Java с использованием функций Java 9+, нацеленных на Java 8 JVM, мы должны заботиться не только о версии байтового кода. Код, выполняемый с использованием Java 8 JVM, будет использовать версию JDK для Java 8, и в случае, если мы будем использовать некоторые классы или методы, которые там недоступны, код не будет выполняться. Поэтому у нас есть две задачи:

  • Сконфигурируйте сборку для использования Jabel для создания байт-кода Java 8
  • устранить вызовы JDK, которые не доступны в Java 8.

Настроить сборку

Я не буду описывать здесь, как настроить Jabel для участия в сборке с использованием Maven. Он задокументирован на сайте и прост.

В случае Java :: Geci я хотел что-то другое. Я хотел проект Maven, который можно использовать для создания целей Java 8 и Java 11. Я хотел этого, потому что хотел, чтобы Java :: Geci, как и прежде, поддерживал JPMS, а также создавал современный байт-код (например, вложение классов вместо методов моста) для тех проектов, которые работают на Java 11 или более поздней версии.

В качестве первого шага я создал профиль с именем JVM8 . Jabel настроен на запуск только тогда, когда этот профиль активен.

Этот профиль также устанавливает выпуск как

1
<release>8</release>

так что самый первый раз, когда компилятор module-info.java с module-info.java , когда увидел файлы module-info.java . К счастью, я могу исключить файлы из файла POM в профиле JVM8 . Я также исключил javax0/geci/log/LoggerJDK9.java и об этом я расскажу позже.

Я также пытался использовать Maven для автоматической настройки номера версии на -JVM8 постфикса -JVM8 если он работает с профилем JVM8 но это было невозможно. Maven — это универсальный инструмент, который может делать много вещей, и в случае более простого проекта такой подход должен быть. В случае Java :: Geci я не мог этого сделать, потому что Java: Geci — это многомодульный проект.

Мультимодульные проекты ссылаются друг на друга. По крайней мере, дочерний модуль ссылается на родительский модуль. Версия дочернего модуля может отличаться от версии родительского модуля. Это логично, поскольку их эволюция и развитие не обязательно связаны друг с другом. Однако, как правило, это так. В проектах, таких как Java :: Geci, который имеет семь дочерних модулей и каждый дочерний модуль имеет тот же номер версии, что и родительский, дочерние модули могут наследовать все параметры, зависимости, параметры компилятора и т. Д. От родительского, кроме версии. Он не может наследовать версию, потому что не знает, от какой родительской версии он наследуется. Это подвох 22.

Разработка Java :: Geci обходит эту проблему, используя препроцессор Jamal, поддерживающий восемь файлов pom.xml . Всякий раз, когда происходит изменение в конфигурации сборки, его необходимо отредактировать в одном из файлов pom.xml.jam или в одном из включенных файлов *.jim а затем в командной строке mvn -f genpom.xml clean восстановятся все новые файлы pom.xml . Это также сохраняет некоторый повторяющийся код, поскольку предварительно обработанные файлы Jamal не настолько многословны, как соответствующие файлы XML. Ценой этого является то, что используемые макросы должны быть сохранены.

Java :: Geci имеет файл version.jim который содержит версию проекта в виде макроса. При нацеливании на выпуск Java 8 необходимо изменить версию в этом файле на xyz-JVM8 и xyz-JVM8 команду mvn -f genpom.xml clean . К сожалению, это ручной шаг, который я могу забыть. Я также могу забыть удалить постфикс -JVM8 после создания цели Java 8.

Чтобы уменьшить риск этой человеческой ошибки, я разработал модульный тест, который проверяет, соответствует ли номер версии профилю компиляции. Он определил профиль компиляции, прочитав файл /javax0/geci/compilation.properties . Это файл ресурсов в проекте, отфильтрованный Maven и содержащий

1
2
projectVersion=${project.version}
profile=${profile}

При выполнении теста свойства заменяются фактическими значениями, определенными в проекте. project.version — версия проекта. profile свойства определяется в двух профилях (по умолчанию и JVM8 ), чтобы быть именем профиля.

Если версия и профиль не совпадают, тест не пройден. Следуя философии Java :: Geci, тест не просто предписывает программисту исправить «ошибку», когда сам тест также может исправить ошибку. Он изменяет файл version.jim так, чтобы он содержал правильную версию. Однако он не запускает файл pom, генерирующий макросы Jamal.

В результате я получу файлы релизов с версией xyz а также xyz-JVM8 после второй сборки с некоторой ручной работой по редактированию.

Устранить вызовы Java 8+ JDK

Простые звонки

Это простая задача с первого взгляда. Вы не должны использовать методы, которых нет в Java 8 JDK. Мы могли бы делать что угодно с использованием Java 8, так что это задача, которая безусловно возможна

Например каждый

1
" ".repeat(tab)

должен быть устранен. Для этого я создал класс JVM8Tools который содержит статические методы. Например:

1
2
3
4
5
6
7
public static String space(int n){
    final StringBuilder sb = new StringBuilder(/*20 spaces*/"                    ");
    while( sb.length() < n){
        sb.append(sb);
    }
    return sb.substring(0,n).toString();
}

там определено и с помощью этого метода я могу написать

1
space(tab)

вместо вызова метода String::repeat . Эта часть была легкой.

Подражая getNestHost

Немного сложнее было реализовать метод getNestHost() . В Java 8 такого нет, но селекторные выражения, включенные в модуль «Инструменты» Java :: Geci, позволяют использовать такие выражения, как

1
Selector.compile("nestHost -> (!null & simpleName ~ /^Map/)").match(Map.Entry.class)

чтобы проверить, что класс Entry объявлен внутри Map , который он тривиально. Имеет смысл использовать это выражение даже в среде Java 8, которую кто-то выбрал для этого, и я не хотел выполнять ампутацию, отбрасывая эту функцию из Java :: Geci. Это должно было быть реализовано.

Реализация проверяет фактическое время выполнения, и, если метод присутствует в JDK, он вызывает это посредством отражения. В других случаях он имитирует функциональность, используя имя класса и пытаясь найти символ $ который разделяет внутреннее и включающее имя класса. Это может привести к ложным результатам в крайне редком случае, когда несколько экземпляров одинаковых структур классов загружаются с использованием разных загрузчиков классов. Я думаю, что такой инструмент, как Java :: Geci, может с этим смириться, это практически не происходит при выполнении модульных тестов.

Есть также недостаток скорости, вызывающий метод Class#getNestHost . Я решил исправить это, если будет реальный спрос.

Поддержка журналирования

Последней проблемой была регистрация. Java 9 представила фасад ведения журнала, который настоятельно рекомендуется использовать в библиотеках. Ведение журнала — давняя проблема в среде Java. Проблема не в том, что нет ни одного. Наоборот. Слишком много. Существует Apache Commons Logging, Log4j, Logback, JDK встроенный журнал утилит Java. Автономное приложение может выбрать используемую инфраструктуру ведения журнала, но в случае, если библиотека использует другую среду, трудно, если не невозможно, направить различные сообщения журнала в один и тот же поток.

Java 9, таким образом, представил новый фасад, который библиотека может использовать для отправки своих журналов, и приложения могут направлять вывод через фасад в любую среду ведения журналов, которую они хотят. Java :: Geci использует этот фасад и через него предоставляет API для ведения журналов для генераторов. В случае среды JVM8 это невозможно. В этом случае Java :: Geci направляет сообщения журнала в стандартный регистратор Java. Для этого существует новый интерфейс LoggerJDK реализованный двумя классами LoggerJVM8 и LoggerJDK9 . Исходный код для последнего исключается из компиляции, если целью является Java 8.

Фактический регистратор пытается получить javax0.geci.log.LoggerJDK9#factory через отражение. Если это там, то возможно использовать регистрацию Java 9. Если его там нет, то регистратор возвращается к фабрике javax0.geci.log.LoggerJVM8#factory . Таким образом, через рефлексию вызывается только фабрика регистратора, что происходит только один раз для каждого регистратора. Само ведение журнала упрощается и использует целевое ведение без какого-либо отражения, таким образом, без ограничения скорости.

навынос

Можно поддерживать Java 8 в большинстве проектов библиотеки без неприемлемых компромиссов. Мы можем создать два разных двоичных файла из одного источника, которые поддерживают две разные версии таким образом, чтобы версия, поддерживающая Java 9 и более поздние версии, не «страдала» от старого байт-кода. Есть определенные компромиссы. Вы должны избегать вызова API Java 9+, и в случае абсолютной необходимости у вас есть возможность предоставить запасной вариант и вы можете предоставить решение для обнаружения во время выполнения на основе отражения.

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Поддержка Java 8

Мнения, высказанные участниками Java Code Geeks, являются их собственными.