Главная » Хабрахабр » [Перевод] Руководство по Java 9 для тех, кому приходится работать с legacy-кодом

[Перевод] Руководство по Java 9 для тех, кому приходится работать с legacy-кодом

Добрый вечер, коллеги. Ровно месяц назад мы получили контракт на перевод книги "Modern Java" от издательства Manning, которая должна стать одной из наших самых заметных новинок в будущем году. Проблема «Modern» и «Legacy» в Java настолько остра, что необходимость такой книги довольно назрела. Масштабы бедствия и способы решения возникающих проблем в Java 9 кратко описаны в статье Уэйна Ситрина (Wayne Citrin), перевод которой мы и хотим вам сегодня предложить.
Раз в несколько лет, с выходом новой версии Java, докладчики на JavaOne начинают смаковать новые языковые конструкции и API, хвалить их достоинства. А ретивым разработчикам тем временем не терпится внедрить новые возможности. Такая картина далека от реальности – она совершенно не учитывает, что большинство программистов заняты поддержкой и доработкой уже существующих приложений, а не пишут новые приложения с нуля.

Наконец, большинство заказчиков и конечных пользователей, особенно в сегменте больших предприятий, настороженно относятся к радикальному обновлению Java-платформы, предпочитая выждать, пока она окрепнет. Большинство приложений – в особенности коммерческие — должны быть обратно совместимы с более ранними версиями Java, в которых не поддерживаются все эти новые супер-пупер возможности.

Вы бы стали использовать у себя в коде методы интерфейсов по умолчанию? Поэтому, как только разработчик собирается попробовать новую возможность, он сталкивается с проблемами. Хотите использовать класс java.util.concurrent. Возможно – если вы счастливчик, и вашему приложению не требуется взаимодействовать с Java 7 или ниже. Не выйдет, если ваше приложение должно работать одновременно на Java 6, 7, 8 или 9. ThreadLocalRandom для генерации псевдослучайных чисел в многопоточном приложении?

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

Что-то, способное облегчить им жизнь? Итак, есть ли в новом релизе Java 9 что-нибудь для программистов, занятых поддержкой унаследованного кода? К счастью — да.

Что приходилось делать при поддержке legacy-кода то появления Java 9

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

Допустим, вам требуется использовать класс java.util.stream. Например, можно применить позднее связывание, если вы хотите получить доступ к новому API, когда вашему приложению также требуется работать со старыми версиями Java, не поддерживающими этот API. Можно создать вспомогательный класс, вот так: LongStream, появившийся в Java 8, и вы хотите применить метод anyMatch(LongPredicate) этого класса, но приложение должно быть совместимо с Java 7.

public classLongStreamHelper catch (ClassNotFoundException e) { longStreamClass = null; longPredicateClass = null; anyMatchMethod = null } catch (NoSuchMethodException e) { longStreamClass = null; longPredicateClass = null; anyMatchMethod = null; } public static boolean anyMatch(Object theLongStream, Object thePredicate) throws NotImplementedException { if (longStreamClass == null) throw new NotImplementedException(); try { Boolean result = (Boolean) anyMatchMethod.invoke(theLongStream, thePredicate); return result.booleanValue(); } catch (Throwable e) { // lots of potential exceptions to handle. Let’s simplify. throw new NotImplementedException(); } } }

Есть способы упростить эту операцию, либо сделать ее более общей, либо более эффективной – идею вы уловили.

Если вы имеете дело с Java 8 – это сработает, но, если с Java 7 – то программа выбросит исключение NotImplementedException. Вместо того, чтобы вызывать theLongStream.anyMatch(thePredicate), как вы поступили бы в Java 8, можно вызвать LongStreamHelper.anyMatch(theLongStream, thePredicate) в любой версии Java.

Потому что код может чрезмерно усложниться, если требуется обращаться ко множеству API (на самом деле, даже сейчас, с единственным API, это уже неудобно). Почему это некрасиво? Наконец, такая практика гораздо менее эффективна, из-за издержек, связанных с рефлексией, а также из-за дополнительных блоков try-catch. Кроме того, такая практика и не типобезопасна, поскольку в коде нельзя прямо упомянуть LongStream или LongPredicate. Следовательно, хотя и можно так сделать, это не слишком интересно, и чревато ошибками по невнимательности.

Например, допустим, что нам нужно использовать лямбда-выражения в коде, который должен оставаться обратно-совместимым и работать в Java 7. Да, вы можете обращаться к новым API, а ваш код при этом сохраняет обратную совместимость, но с новыми языковыми конструкциями вам это не удастся. Компилятор Java не позволит указать версию исходного кода выше целевой. Вам не повезло. 8 (т.е., Java 8), а целевой уровень соответствия будет 1. Так, если задать уровень соответствия исходного кода 1. 7 (Java 7), то компилятор вам этого не позволит.

Вам помогут разноверсионные JAR-файлы

В Java 9 такая возможность предоставляется как для новых API, так и для новых языковых конструкций Java: речь о разноверисонных JAR-файлах. Сравнительно недавно появилась еще одна отличная возможность использовать новейшие возможности Java, позволяя при этом приложениям работать со старыми версиями Java, где такие приложения не поддерживались.

Если вы работаете с Java 9, то JVM найдет эту «нишу», станет использовать классы из нее и игнорировать одноименные классы из основной части JAR-файла. Разноверсионные JAR-файлы почти не отличаются от старых добрых JAR-файлов, но с одной важнейшей оговоркой: в новых JAR-файлах появилась своеобразная «ниша», куда можно записывать классы, использующие новейшие возможности Java 9.

Она игнорирует ее и использует классы из основной части JAR-файла. Однако, при работе с Java 8 или ниже, JVM неизвестно о существовании этой «ниши». С выходом Java 10 появится новая аналогичная «ниша» для классов, использующих наиболее актуальные возможности Java 10 и так далее.

Допустим, у нас есть JAR-файл с четырьмя классами, работающими в Java 8 или ниже: В JEP 238 – предложении на доработку Java, где описаны ращновенсионные JAR-файлы, приводится простой пример.

JAR root - A.class - B.class - C.class - D.class

Теперь представим, что после выхода Java 9 мы переписываем классы A и B, чтобы они могли использовать новые возможности, специфичные для Java 9. Затем выходит Java 10, и мы вновь переписываем класс A, чтобы он мог использовать новые возможности Java 10. При этом, приложение по-прежнему должно нормально работать с Java 8. Новый разноверсионный JAR-файл выглядит так:

JAR root - A.class - B.class - C.class - D.class - META-INF Versions - 9 - A.class - B.class - 10 - A.class

JAR-файл не только приобрел новую структуру; теперь в его манифесте указано, что этот файл разноверсионный.

Используются лишь оригинальные классы A, B, C и D. Когда вы запускаете этот JAR-файл на Java 8 JVM, он игнорирует раздел \META-INF\Versions, поскольку даже не подозревает о нем и не ищет его.

При запуске под Java 9, используются классы, находящиеся в \META-INF\Versions\9, причем, они используются вместо оригинальных классов A и B, но классы в \META-INF\Versions\10 игнорируются.

При запуске под Java 10 используются обе ветки \META-INF\Versions; в частности, версия A от Java 10, версия B от Java 9 и используемые по умолчанию версии C и D.

Именно так проще всего использовать новые возможности Java 9, не жертвуя при этом обратной совместимостью. Итак, если в вашем приложении вам нужен новый ProcessBuilder API из Java 9, но нужно обеспечить, чтобы приложение продолжало работать и под Java 8, просто запишите в раздел \META-INF\Versions\9 JAR-файла новые версии ваших классов, использующие ProcessBuilder, а старые классы оставьте в основной части архива, используемой по умолчанию.

Такую поддержку также обеспечивают и другие инструменты, не входящие в JDK. В Java 9 JDK есть версия инструмента jar.exe, поддерживающая создание разноверсионных JAR-файлов.

Java 9: модули, повсюду модули

Одна из целей модуляризации — усилить действующий в Java механизм инкапсуляции, чтобы разработчик мог указывать, какие API предоставляются другим компонентам и мог рассчитывать, что JVM будет навязывать инкапсуляцию. Система модулей Java 9, (также известная под названием Project Jigsaw) – это, несомненно, крупнейшее изменение в Java 9. При модуляризации инкапсуляция получается сильнее, чем при применении модификаторов доступа public/protected/private у классов или членов классов.

В таком смысле модули сильнее традиционного механизма classpath, поскольку пути classpath заблаговременно не проверяются, и возможны ошибки из-за отсутствия необходимых классов. Вторая цель модуляризации – указать, каким модулям необходимы для работы другие модули и еще до запуска приложения заблаговременно убедиться, что все необходимые модули на месте. Здесь я уделю внимание именно тем аспектам модуляризации, которые помогают разработчику при поддержке унаследованных приложений. Таким образом, некорректный classpath может быть обнаружен уже тогда, когда приложение успеет проработать достаточно долго, либо после того, как оно будет много раз запущено.
Вся система модулей большая и сложная, и ее подробное обсуждение выходит за рамки этой статьи (Вот хорошее, подробное объяснение).

К счастью, это легко сделать благодаря спецификации о работе с модулями. Модуляризация – хорошая штука, и разработчик должен стараться разбивать новый код на модули, когда это только возможно, даже если остаток приложения (пока) не модуляризован.

module-info.java содержит метаданные, в частности, название модуля, пакеты которого экспортируются (т.e., становятся видны извне), каких модулей требует данный модуль и некоторая иная информация. Во-первых, JAR-файл становится модуляризован (и превращается в модуль) с появлением в нем файла module-info.class (скомпилированного из module-info.java) у корня файла JAR.

Строго говоря, требуется немного похимичить, и все равно указывать в качестве целевой версии module-info.class именно Java 9, но это реально). Информация в module-info.class видна лишь в случаях, когда JVM ищет ее – то есть, система трактует модуляризованные JAR-файлы точно как обычные, если работает со старыми версиями Java (предполагается, что код был скомпилирован для работы с более старой версией Java.

Также отметим, что файлы module-info.class можно, с оговорками, помещать в версионируемых областях разноверсионных JAR-файлов. Таким образом, у вас должна остаться возможность запускать модуляризованные JAR-файлы с Java 8 и ниже при условии, что в иных отношениях они также совместимы с более ранними версиями Java.

and a module path. В Java 9 существует как classpath, так и путь к модулю. Если поставить модуляризованный JAR-файл в classpath, он тратуется как любой другой JAR-файл. Classpath работает как обычно. Ваш унаследованный код должен вполне успешно с ним справиться. То есть, если вы модуляризовали JAR-файл, а ваше приложение еще не готово обращаться с ним как с модулем, его можно поставить в classpath, он будет работать как всегда.

Такой модуль считается самым обычным, однако, всю информацию он экспортирует другим модулям, и может обращаться к любым другим модулям. Также отметим, что коллекция всех JAR-файлов в classpath считается частью единственного безымянного модуля. Таким образом, если у вас еще нет модуляризованного Java-приложения, но есть некие старые библиотеки, которые тоже пока не модуляризованы (и, вероятно, никогда не будут) – можно просто положить все эти библиотеки в classpath, и вся система будет нормально работать.

При использовании модулей из этого пути JVM может проверять (как во время компиляции, так и во время выполнения), все ли необходимые модули на месте, и сообщать об ошибке, если каких-то модулей не хватает. В Java 9 есть путь к модулям, работающий наряду с classpath. Все JAR-файлы в classpath, как члены безымянного модуля, доступны модулям, перечисленным в модульном пути – и наоборот.

Во-первых, можно добавить файл module-info.class в файл JAR, а затем поставить модуляризованный JAR-файл в путь модулей. Не составляет труда перенести JAR-файл из classpath в путь модулей – и пользоваться всеми преимуществами модуляризации. Такой новоиспеченный модуль все равно сможет обращаться ко всем оставшимся JAR-файлам в classpath JAR, поскольку они входят в безымянный модуль и остаются в доступе.

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

Этот модуль одноименен тому JAR-файлу, в котором содержится, и другие модули могут явно затребовать его. Автоматический модуль считается модулем, даже если в нем нет файла module-info.class. Он автоматически экспортирует все свои публично доступные API и читает (то есть, требует) все прочие именованные модули, а также безымянные модули.

Унаследованные JAR-файлы автоматически превращаются в модули, просто в них не хватает некоторй информации, которая позволяла бы определить, все ли нужные модули на месте, либо определить, чего не хватает. Таким образом, немодуляризованный JAR-файл из classpath можно превратить в модуль, вообще ничего для этого не делая.

Существует правило: пакет может входить в состав всего одного именованного модуля. Не каждый немодуляризованный JAR-файл можно переместить в путь модулей и превратить его в автоматический модуль. Остальные могут остаться в classpath и войти в состав безымянного модуля. То есть, если пакет находится более чем в одном JAR-файле, то всего один JAR-файл с этим пакетом в составе можно превратить в автоматический модуль.

На самом деле, речь в данном случае лишь о том, что можно оставить старые JAR-файлы в classpath или переместить их в путь модулей. На первый взгляд, описанный здесь механизм кажется сложным, но на практике он весьма прост. А когда ваши старые JAR-файлы будут модуляризованы, можно оставить их в classpath или переместить в путь модулей. Можно разбить их на модули или не делать этого.

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

Java 9 «делает сама»: Модульный JDK и Jlink

Один из способов гарантировать работоспособность Java-приложения – предоставить среду выполнения вместе с приложением. Одна из проблем с унаследованными Java-приложениями заключается в том, что конечный пользователь может не работать с подходящей средой Java. Здесь рассказано, как создать приватную JRE. Java позволяет создавать приватные (многократно распространяемые) JRE, которые можно распространять в пределах приложения. Как правило, берется иерархия файлов JRE, устанавливаемая вместе с JDK, нужные файлы сохраняются, а также сохраняются опциональные файлы с тем функционалом, который может понадобиться в вашем приложении.

Само по себе это не повредит, однако, все-таки хочется избавиться от всего лишнего, поскольку эти файлы занимают место. Процесс немного хлопотный: необходимо поддерживать иерархию установочных файлов, быть при этом внимательным, поэтому вы не пропускаете ни одного файла, ни одного каталога. Да, легко поддаться и совершить такую ошибку.

Так что почему бы не перепоручить эту работу JDK?

Больше не придется волноваться, что на пользовательском компьютере окажется не та среда для выполнения Java, не придется беспокоиться о том, что вы сами неправильно собрали приватную JRE. В Java 9 можно создать самодостаточную среду, добавляемую к приложению – и в этой среде будет все необходимое для работы приложения.

Модуляризовать теперь можно не только собственный код, но и сам Java 9 JDK. Ключевой ресурс для создания таких самодостаточных исполняемых образов – это модульная система. Система модулей требует указывать модули базовых классов, которые необходимы в вашем коде, и при этом вы указываете необходимые элементы JDK. Теперь библиотека классов Java – это коллекция модулей, из модулей же состоят и инструменты JDK.

Запустив jlink, вы получаете иерархию файлов – именно тех, что нужны для запуска вашего приложения, ни больше, ни меньше. Чтобы собрать все это вместе, в Java 9 предусмотрен специальный новый инструмент под названием jlink. Поэтому, если вы захотите создать такие исполняемые образы для других платформ, потребуется запустить jlink в контексте установки на каждой конкретной платформе, для которой вам нужен такой образ. Такой набор будет гораздо меньше стандартной JRE, причем, он будет платформо-специфичным (то есть, подбираться для конкретной операционной системы и машины).

Даже в таком случае вам будет немного удобнее: jlink сам упакует для вас JRE, поэтому можете не волноваться о том, как правильно скопировать файловую иерархию. Также обратите внимание: если запустить jlink с приложением, в котором ничего не модуляризовано, у инструмента просто не будет нужной информации, позволяющей ужать JRE, поэтому jlink ничего не останется, кроме как упаковать целую JRE.

Инструмент упакует только ту часть среды исполнения, которая требуется для работы приложения. С jlink становится легко упаковать приложение и все, что необходимо для его запуска – и можете не волноваться, что сделаете что-нибудь неправильно. То есть, унаследованное Java-приложение гарантированно получит такую среду, в которой оно окажется работоспособным.

Встреча старого и нового

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

Следует отдать должное проектировщикам Java 9: по-видимому, они это учли и хорошо поработали, чтобы предоставить эти новые возможности и тем разработчикам, которым приходится поддерживать старые версии Java.

Таким образом, разработчику легко писать код для Java 9, оставить старый код для Java 8 и ниже и предоставить среде исполнения выбор – какие классы она сможет запустить. Разноверсионные JAR-файлы позволяют разработчикам применять новые возможности Java 9 и выносить их в отдельную часть JAR-файла, где более ранние версии Java их не заметят.

Система очень щадящая, она ориентирована на постепенную миграцию и практически всегда поддерживает работу с унаследованным кодом, который «слыхом не слыхивал» о модульной системе. Благодаря модулям Java, разработчику проще проверять зависимости, достаточно писать все новые JAR-файлы в модульном виде, а старый код оставлять немодуляризованным.

Ранее такой процесс был чреват множеством ошибок, но современный инструментарий Java позволяет его автоматизировать – и все просто работает. Благодаря модульному JDK и jlink, можно с легкостью создавать исполняемые образы и гарантированно обеспечивать приложение такой средой исполнения, в которой будет все необходимое для работы.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Сегодня MIPS стал Open Source, против RISC/V и ARM. Как Россия повлияла на стратегию американской процессорной компании

То, о чем говорили сторонники Open Source с 1980-х — свершилось! Сегодня архитектура процессоров MIPS стала Open Source. Учитывая, что такие компании как Broadcom, Cavium, китайский ICT и Ingenic платили MIPS за архитектурную лицензию (право сделать совместимую по системе команд ...

Вышла новая версия Unity 2018.3

Вышла новая версия Unity, которая уже доступна для пользователей. Unity 2018.3 содержит более 2000 новых функций, исправлений и улучшений, включая улучшенный воркфлоу префабов, Visual Effect Graph (Preview) и обновленную систему Terrain, которые дают разработчикам возможность повысить производительность и создавать многогранные ...