Записки о миграции на Java 10
Здравствуй, Хабр. Как ты помнишь, недавно произошёл официальный релиз Java 10. Учитывая, что практически все сейчас используют преимущественно 8-ку, с выходом 10-ки нас ждут такие вкусности как модульность (вошла в 9-ку) и local variable type inference. Звучит неплохо, можно попробовать поэкспериментировать с переносом какого-нибудь существующего проекта на 10-ку.
О том, какие разновидности боли ждут нас, можно узнать под катом.
Я просто оставлю это здесь не преследую цели изложить здесь все возможные проблемы с миграцией java-проектов с 8-й версии Java на 9-ю или 10-ю. Хочется зафиксировать свой небольшой опыт первого контакта с новыми версиями Java как-то кроме как в устных обсуждениях с коллегами. Возможно он кого-то остановит кому-то окажется полезен
1. Проблемы с реализацией модульности
Для начала давайте сделаем наше приложение модульным (не зря же разработчики JDK старались и страдали над проектом Jigsaw)?
1.1. Проблемы с утилитой jdeps
Чтобы из существующей иерархии классов получить модуль java, нам потребуется:
- написать 1 или несколько файлов module-info.java с указанием в них имен модулей, зависимостей и экспортируемых пакетов
- добавить аргументы для компилятора (--module-path и прочие)
Вроде бы выглядит всё довольно просто на первый взгляд.
1.1.1. Зависимости от внешних библиотек больше не работают без специальных заклинаний
Ничего просто не соберётся. Если вы хотите собрать модульно приложенье, то ваши зависимости также должны быть модульны. Иначе просто не получится добавить их в module-info.java. Точнее добавить-то можно — старомодные jar-файлы библиотек рассматриваются как так называемые «автоматические модули». Однако, проблема в том, что автоматические модули при сборке использовать нельзя. Да, в популярных репозиториях пока что очень мало библиотек, собраных в качестве модулей и вы просто не сможете их использовать. Nobody cares.
Здесь-то и понадобилась утилита jdeps, которая может проанализировать jar-файл и сгенерировать module-info.java для него, чтобы сделать из старомодного jar модуль. В качестве костыля workaround было решено просто взять и уйти распаковать все зависимости в одну кучку и сделать единый jar из которого можно попытаться сделать модный модуль-со-всеми-зависимостями. Пример на stackoverflow прилагается.
1.1.2. Потребуется включить значительно больше зависимостей, чем реально требуется
Jdeps разрешает зависимости рекурсивно, причём требует обязательного разрешения даже зависимостей, которые Maven/Gradle считают необязательными (optional). В итоге дерево зависимостей даже небольшого подопытного проекта разрастается в несколько раз. А модуль с зависимостями, который было решено изготовить в пункте (1.1.1) выкачивает пол интернета становится весьма тяжеловесным. Пример изложения сей боли здесь. Неожиданно обнаруживается, что log4j требует AWT, OSGI, ZeroMQ, Kafka или ещё что-нибудь в этом духе.
1.1.3. Неожиданный публичный [Роскомнадзор] утилиты
Обнаружилось, что в ситуации, когда некоторые классы дублируются в разных модулях, jdeps вываливается со стэктрэйсом IllegalStateException откуда-то из пучин своего кода без всякого пояснения что же пошло не так. Только одна маленькая зацепочка — сообщение «dependency on self» в качестве аргумента конструктора исключения. В какой-то момент мне показалось, что сие безобразие как бы намекало на то, что вообще всё (классы приложения, классы зависимостей, классы «необязательных» зависимостей) нужно свалить в один jar-файл. И действительно — сработало.
1.2. Проблемы с runtime
Даже когда всё уже чудесным образом собралось, модульно приложенье и даже custom образ JRE к нему, оказалось, что самые главные сюрпризы — ещё впереди. То есть в runtime.
1.2.1. Теперь невозможно определить путь стартовому файлу/каталогу
В приложения Java зачастую есть необходимость определить путь к каталогу, где сидит фазан лежит jar-файл, из которого произошёл запуск приложения (например jar-файл, содержащий точку входа). Это обычно нужно, чтобы понять, где искать файлы приложения — конфигурацию, плагины и т.п. Такие вещи, как правило лежат по соседству (что весьма логично). Для определения этого пути обычно помогает заклинание вида:
Main.getProtectionDomain().getCodeSource().getLocation().toURI()
Каково же было моё удивление, когда я обнаружил, что в новых версиях Java это заклинание более не работает и я вижу вместо пути к файлу что-то вроде «jrt://com.example.myapp». Я начал лихорадочно гуглить и переполнять стэк. Пробовал определять модуль и как-то выяснить, какие файлы к нему относятся. Попытки обращения к classpath тоже привели меня ни к чему, так как модульно приложенье запускается с так называемым module path заместо старомодного classpath. Это был финиш. Именно в этот момент я решил сворачивать свой эксперимент по переезду на Java 10. Нет, конечно можно было бы путь к каталогу передавать в командной строке или принудить пользователя запускать только из правильной директории. Но я для себя лично решил, что с меня пока что хватит.
1.2.2. Провал при попытке определения MIME-типа файла.
Оказалось, что вишенка на торте была не одна. Для экспериментов с десяткой я установил её и настроил в качестве JVM по умолчанию в системе. Поэтому сюрпризы продолжились. Код вроде
final String mimeType = Files.probeContentType(itemInputFilePath);
начал приводить к тому, что возвращался null в случае, если аргумент представлял из себя обычный CSV-файл с соответсвующим расширением. «Как же так?» — подумал я, «неужели JVM не в состоянии определить MIME-тип для обычного CSV-файла?». Я немного подебажил и обнаружил, что вызов вида
Files.readAllLines("/etc/mime.types", Charset.defaultCharset())
совершенно нагло кидает маловнятное исключение с сообщением вроде «malformed input».
Глубже в причинах этого безобразия я разбираться пока не стал. Лишь проверил файл /etc/mime.types — кракозябры не обнаружены вроде бы нормальный текстовый файл. То есть, JVM не смогла прочитать строки из текстового файла.
1.2.3. Нерабочий новый механизм Service Providers
Начиная с версии 9 Java также предлагает новый механизм Service Providers взамен старого. Вещь неплохая, позволяет делать плагины, которые разрешаются в runtime. Новый механизм подразумевает объявление расширения в module-info.java с помощью ключевых слов «provides» и «with». Казалось бы, что может быть проще. На практике это не заработало, в отличие от старого механизма (использующего файлы resources/META-INF/services/…). Разбираться, в чём проблема времени не хватило. Однако отметку о том, что без специальных заклинаний не взлетает, я пожалуй, тут сделаю.
2. Размышления о реализации local variable type inference
В случае с local variable type inference мы имеем новое ключевое слово «var» (variable, переменная), что намекает на влияние Scala. Однако лично мне показалось странным, почему не добавили также «val» (value, значение). Вместо этого приходится писать «final var», что читается глазами как «окончательная переменная» или даже как «постоянная переменная» —, а это не только менее лаконично, но и как-то слегка противоречиво, на мой, привыкший к хорошему взгляд.
Также обнаружились довольно забавные вещи. Например:
final var someStrings = new ArrayList();
Здесь JVM «запоминает» за именем «someStrings» тип ArrayList
final var someStrings = (List) new ArrayList<>();
, но это уже выглядит как-то не очень лаконично. Этот момент не претендует на звание какого-то фатального недостатка, но демонстрирует лёгкий налёт противоречивости.
3. Неутешительные выводы
Некоторые из озвученных в этой записке проблем, возникли ещё в 9-й версии. Учитывая, что они так и не были пофиксеты до сих пор, есть неприятное ощущение, что весь этот бардак будет и в 11-й версии и даже далее. Не хотелось бы кликушествовать, но из этого субъективного ощущения вытекает другое: Java, как язык программирования, начинает переживать свой упадок. Возможно я не прав, а возможно так и есть и тогда на смену Java нужно искать другой инструмент.