[Перевод] Руководство по Java 9 для тех, кому приходится работать с legacy-кодом
Добрый вечер, коллеги. Ровно месяц назад мы получили контракт на перевод книги «Modern Java» от издательства Manning, которая должна стать одной из наших самых заметных новинок в будущем году. Проблема «Modern» и «Legacy» в Java настолько остра, что необходимость такой книги довольно назрела. Масштабы бедствия и способы решения возникающих проблем в Java 9 кратко описаны в статье Уэйна Ситрина (Wayne Citrin), перевод которой мы и хотим вам сегодня предложить.
Раз в несколько лет, с выходом новой версии Java, докладчики на JavaOne начинают смаковать новые языковые конструкции и API, хвалить их достоинства. А ретивым разработчикам тем временем не терпится внедрить новые возможности. Такая картина далека от реальности — она совершенно не учитывает, что большинство программистов заняты поддержкой и доработкой уже существующих приложений, а не пишут новые приложения с нуля.
Большинство приложений — в особенности коммерческие — должны быть обратно совместимы с более ранними версиями Java, в которых не поддерживаются все эти новые супер-пупер возможности. Наконец, большинство заказчиков и конечных пользователей, особенно в сегменте больших предприятий, настороженно относятся к радикальному обновлению Java-платформы, предпочитая выждать, пока она окрепнет.
Поэтому, как только разработчик собирается попробовать новую возможность, он сталкивается с проблемами. Вы бы стали использовать у себя в коде методы интерфейсов по умолчанию? Возможно — если вы счастливчик, и вашему приложению не требуется взаимодействовать с Java 7 или ниже. Хотите использовать класс java.util.concurrent.ThreadLocalRandom
для генерации псевдослучайных чисел в многопоточном приложении? Не выйдет, если ваше приложение должно работать одновременно на Java 6, 7, 8 или 9.
С выходом нового релиза разработчики, занятые поддержкой унаследованного кода, чувствуют себя как дети, вынужденные таращиться на витрину кондитерского магазина. Внутрь их не пускают, поэтому их удел — разочарование и фрустрация.
Итак, есть ли в новом релизе Java 9 что-нибудь для программистов, занятых поддержкой унаследованного кода? Что-то, способное облегчить им жизнь? К счастью — да.
Что приходилось делать при поддержке legacy-кода то появления Java 9
Конечно, можно впихнуть возможности новой платформы в унаследованные приложения, в которых нужно соблюдать обратную совместимость. В частности, всегда есть возможности воспользоваться преимуществами новых API. Однако, может получиться немного некрасиво.
Например, можно применить позднее связывание, если вы хотите получить доступ к новому API, когда вашему приложению также требуется работать со старыми версиями Java, не поддерживающими этот API. Допустим, вам требуется использовать класс java.util.stream.LongStream
, появившийся в Java 8, и вы хотите применить метод anyMatch(LongPredicate)
этого класса, но приложение должно быть совместимо с Java 7. Можно создать вспомогательный класс, вот так:
public classLongStreamHelper {
private static Class longStreamClass;
private static Class longPredicateClass;
private static Method anyMatchMethod;
static {
try {
longStreamClass = Class.forName("java.util.stream.LongStream");
longPredicateClass = Class.forName("java.util.function.LongPredicate");
anyMatchMethod = longStreamClass.getMethod("anyMatch", longPredicateClass):
} 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();
}
}
}
Есть способы упростить эту операцию, либо сделать ее более общей, либо более эффективной — идею вы уловили.
Вместо того, чтобы вызывать theLongStream.anyMatch(thePredicate)
, как вы поступили бы в Java 8, можно вызвать LongStreamHelper.anyMatch(theLongStream, thePredicate)
в любой версии Java. Если вы имеете дело с Java 8 — это сработает, но, если с Java 7 — то программа выбросит исключение NotImplementedException
.
Почему это некрасиво? Потому что код может чрезмерно усложниться, если требуется обращаться ко множеству API (на самом деле, даже сейчас, с единственным API, это уже неудобно). Кроме того, такая практика и не типобезопасна, поскольку в коде нельзя прямо упомянуть LongStream
или LongPredicate
. Наконец, такая практика гораздо менее эффективна, из-за издержек, связанных с рефлексией, а также из-за дополнительных блоков try-catch
. Следовательно, хотя и можно так сделать, это не слишком интересно, и чревато ошибками по невнимательности.
Да, вы можете обращаться к новым API, а ваш код при этом сохраняет обратную совместимость, но с новыми языковыми конструкциями вам это не удастся. Например, допустим, что нам нужно использовать лямбда-выражения в коде, который должен оставаться обратно-совместимым и работать в Java 7. Вам не повезло. Компилятор Java не позволит указать версию исходного кода выше целевой. Так, если задать уровень соответствия исходного кода 1.8 (т.е., Java 8), а целевой уровень соответствия будет 1.7 (Java 7), то компилятор вам этого не позволит.
Вам помогут разноверсионные JAR-файлы
Сравнительно недавно появилась еще одна отличная возможность использовать новейшие возможности Java, позволяя при этом приложениям работать со старыми версиями Java, где такие приложения не поддерживались. В Java 9 такая возможность предоставляется как для новых API, так и для новых языковых конструкций Java: речь о разноверисонных JAR-файлах.
Разноверсионные JAR-файлы почти не отличаются от старых добрых JAR-файлов, но с одной важнейшей оговоркой: в новых JAR-файлах появилась своеобразная «ниша», куда можно записывать классы, использующие новейшие возможности Java 9. Если вы работаете с Java 9, то JVM найдет эту «нишу», станет использовать классы из нее и игнорировать одноименные классы из основной части JAR-файла.
Однако, при работе с Java 8 или ниже, JVM неизвестно о существовании этой «ниши». Она игнорирует ее и использует классы из основной части JAR-файла. С выходом Java 10 появится новая аналогичная «ниша» для классов, использующих наиболее актуальные возможности Java 10 и так далее.
В JEP 238 — предложении на доработку Java, где описаны ращновенсионные JAR-файлы, приводится простой пример. Допустим, у нас есть JAR-файл с четырьмя классами, работающими в Java 8 или ниже:
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-файл не только приобрел новую структуру; теперь в его манифесте указано, что этот файл разноверсионный.
Когда вы запускаете этот JAR-файл на Java 8 JVM, он игнорирует раздел \META-INF\Versions
, поскольку даже не подозревает о нем и не ищет его. Используются лишь оригинальные классы A, B, C и D.
При запуске под 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.
Итак, если в вашем приложении вам нужен новый ProcessBuilder API из Java 9, но нужно обеспечить, чтобы приложение продолжало работать и под Java 8, просто запишите в раздел \META-INF\Versions\9
JAR-файла новые версии ваших классов, использующие ProcessBuilder, а старые классы оставьте в основной части архива, используемой по умолчанию. Именно так проще всего использовать новые возможности Java 9, не жертвуя при этом обратной совместимостью.
В Java 9 JDK есть версия инструмента jar.exe, поддерживающая создание разноверсионных JAR-файлов. Такую поддержку также обеспечивают и другие инструменты, не входящие в JDK.
Java 9: модули, повсюду модули
Система модулей Java 9, (также известная под названием Project Jigsaw) — это, несомненно, крупнейшее изменение в Java 9. Одна из целей модуляризации — усилить действующий в Java механизм инкапсуляции, чтобы разработчик мог указывать, какие API предоставляются другим компонентам и мог рассчитывать, что JVM будет навязывать инкапсуляцию. При модуляризации инкапсуляция получается сильнее, чем при применении модификаторов доступа public/protected/private
у классов или членов классов.
Вторая цель модуляризации — указать, каким модулям необходимы для работы другие модули и еще до запуска приложения заблаговременно убедиться, что все необходимые модули на месте. В таком смысле модули сильнее традиционного механизма classpath, поскольку пути classpath заблаговременно не проверяются, и возможны ошибки из-за отсутствия необходимых классов. Таким образом, некорректный classpath может быть обнаружен уже тогда, когда приложение успеет проработать достаточно долго, либо после того, как оно будет много раз запущено.
Вся система модулей большая и сложная, и ее подробное обсуждение выходит за рамки этой статьи (Вот хорошее, подробное объяснение). Здесь я уделю внимание именно тем аспектам модуляризации, которые помогают разработчику при поддержке унаследованных приложений.
Модуляризация — хорошая штука, и разработчик должен стараться разбивать новый код на модули, когда это только возможно, даже если остаток приложения (пока) не модуляризован. К счастью, это легко сделать благодаря спецификации о работе с модулями.
Во-первых, JAR-файл становится модуляризован (и превращается в модуль) с появлением в нем файла module-info.class (скомпилированного из module-info.java) у корня файла JAR. module-info.java
содержит метаданные, в частности, название модуля, пакеты которого экспортируются (т.e., становятся видны извне), каких модулей требует данный модуль и некоторая иная информация.
Информация в module-info.class
видна лишь в случаях, когда JVM ищет ее — то есть, система трактует модуляризованные JAR-файлы точно как обычные, если работает со старыми версиями Java (предполагается, что код был скомпилирован для работы с более старой версией Java. Строго говоря, требуется немного похимичить, и все равно указывать в качестве целевой версии module-info.class именно Java 9, но это реально).
Таким образом, у вас должна остаться возможность запускать модуляризованные JAR-файлы с Java 8 и ниже при условии, что в иных отношениях они также совместимы с более ранними версиями Java. Также отметим, что файлы module-info.class
можно, с оговорками, помещать в версионируемых областях разноверсионных JAR-файлов.
В Java 9 существует как classpath, так и путь к модулю. and a module path. Classpath работает как обычно. Если поставить модуляризованный JAR-файл в classpath, он тратуется как любой другой JAR-файл. То есть, если вы модуляризовали JAR-файл, а ваше приложение еще не готово обращаться с ним как с модулем, его можно поставить в classpath, он будет работать как всегда. Ваш унаследованный код должен вполне успешно с ним справиться.
Также отметим, что коллекция всех JAR-файлов в classpath считается частью единственного безымянного модуля. Такой модуль считается самым обычным, однако, всю информацию он экспортирует другим модулям, и может обращаться к любым другим модулям. Таким образом, если у вас еще нет модуляризованного Java-приложения, но есть некие старые библиотеки, которые тоже пока не модуляризованы (и, вероятно, никогда не будут) — можно просто положить все эти библиотеки в classpath, и вся система будет нормально работать.
В Java 9 есть путь к модулям, работающий наряду с classpath. При использовании модулей из этого пути JVM может проверять (как во время компиляции, так и во время выполнения), все ли необходимые модули на месте, и сообщать об ошибке, если каких-то модулей не хватает. Все JAR-файлы в classpath, как члены безымянного модуля, доступны модулям, перечисленным в модульном пути — и наоборот.
Не составляет труда перенести JAR-файл из classpath в путь модулей — и пользоваться всеми преимуществами модуляризации. Во-первых, можно добавить файл module-info.class
в файл JAR, а затем поставить модуляризованный JAR-файл в путь модулей. Такой новоиспеченный модуль все равно сможет обращаться ко всем оставшимся JAR-файлам в classpath JAR, поскольку они входят в безымянный модуль и остаются в доступе.
Также возможно, что вы не хотите модуляризовать JAR-файл, либо, что JAR-файл принадлежит не вам, а кому-то еще, так что модуляризовать его сами вы не можете. В таком случае JAR-файл все равно можно поставить в путь модулей, он станет автоматическим модулем.
Автоматический модуль считается модулем, даже если в нем нет файла module-info.class
. Этот модуль одноименен тому JAR-файлу, в котором содержится, и другие модули могут явно затребовать его. Он автоматически экспортирует все свои публично доступные API и читает (то есть, требует) все прочие именованные модули, а также безымянные модули.
Таким образом, немодуляризованный JAR-файл из classpath можно превратить в модуль, вообще ничего для этого не делая. Унаследованные JAR-файлы автоматически превращаются в модули, просто в них не хватает некоторй информации, которая позволяла бы определить, все ли нужные модули на месте, либо определить, чего не хватает.
Не каждый немодуляризованный JAR-файл можно переместить в путь модулей и превратить его в автоматический модуль. Существует правило: пакет может входить в состав всего одного именованного модуля. То есть, если пакет находится более чем в одном JAR-файле, то всего один JAR-файл с этим пакетом в составе можно превратить в автоматический модуль. Остальные могут остаться в classpath и войти в состав безымянного модуля.
На первый взгляд, описанный здесь механизм кажется сложным, но на практике он весьма прост. На самом деле, речь в данном случае лишь о том, что можно оставить старые JAR-файлы в classpath или переместить их в путь модулей. Можно разбить их на модули или не делать этого. А когда ваши старые JAR-файлы будут модуляризованы, можно оставить их в classpath или переместить в путь модулей.
В большинстве случаев все должно попросту работать, как и раньше. Ваши унаследованные JAR-файлы должны прижиться в новой модульной системе. Чем больше вы модуляризуете код, тем больше информации о зависимостях требуется проверять, а недостающие модули и API будут обнаруживаться на гораздо более ранних этапах разработки и, возможно, избавят вас от больших кусков работы.
Java 9 «делает сама»: Модульный JDK и Jlink
Одна из проблем с унаследованными Java-приложениями заключается в том, что конечный пользователь может не работать с подходящей средой Java. Один из способов гарантировать работоспособность Java-приложения — предоставить среду выполнения вместе с приложением. Java позволяет создавать приватные (многократно распространяемые) JRE, которые можно распространять в пределах приложения. Здесь рассказано, как создать приватную JRE. Как правило, берется иерархия файлов JRE, устанавливаемая вместе с JDK, нужные файлы сохраняются, а также сохраняются опциональные файлы с тем функционалом, который может понадобиться в вашем приложении.
Процесс немного хлопотный: необходимо поддерживать иерархию установочных файлов, быть при этом внимательным, поэтому вы не пропускаете ни одного файла, ни одного каталога. Само по себе это не повредит, однако, все-таки хочется избавиться от всего лишнего, поскольку эти файлы занимают место. Да, легко поддаться и совершить такую ошибку.
Так что почему бы не перепоручить эту работу JDK?
В Java 9 можно создать самодостаточную среду, добавляемую к приложению — и в этой среде будет все необходимое для работы приложения. Больше не придется волноваться, что на пользовательском компьютере окажется не та среда для выполнения Java, не придется беспокоиться о том, что вы сами неправильно собрали приватную JRE.
Ключевой ресурс для создания таких самодостаточных исполняемых образов — это модульная система. Модуляризовать теперь можно не только собственный код, но и сам Java 9 JDK. Теперь библиотека классов Java — это коллекция модулей, из модулей же состоят и инструменты JDK. Система модулей требует указывать модули базовых классов, которые необходимы в вашем коде, и при этом вы указываете необходимые элементы JDK.
Чтобы собрать все это вместе, в Java 9 предусмотрен специальный новый инструмент под названием jlink. Запустив jlink, вы получаете иерархию файлов — именно тех, что нужны для запуска вашего приложения, ни больше, ни меньше. Такой набор будет гораздо меньше стандартной JRE, причем, он будет платформо-специфичным (то есть, подбираться для конкретной операционной системы и машины). Поэтому, если вы захотите создать такие исполняемые образы для других платформ, потребуется запустить jlink в контексте установки на каждой конкретной платформе, для которой вам нужен такой образ.
Также обратите внимание: если запустить jlink с приложением, в котором ничего не модуляризовано, у инструмента просто не будет нужной информации, позволяющей ужать JRE, поэтому jlink ничего не останется, кроме как упаковать целую JRE. Даже в таком случае вам будет немного удобнее: jlink сам упакует для вас JRE, поэтому можете не волноваться о том, как правильно скопировать файловую иерархию.
С jlink становится легко упаковать приложение и все, что необходимо для его запуска — и можете не волноваться, что сделаете что-нибудь неправильно. Инструмент упакует только ту часть среды исполнения, которая требуется для работы приложения. То есть, унаследованное Java-приложение гарантированно получит такую среду, в которой оно окажется работоспособным.
Встреча старого и нового
Одна из проблем, возникающих при поддержке унаследованного Java-приложения заключается в том, что вы оказываетесь лишены всех плюшек, появляющихся при выходе новой версии. В Java 9, как и в предшествующих версиях, появилась целая куча великолепных новых API и языковых возможностей, но разработчики (наученные горьким опытом) могут предположить, что попросту не смогут ими воспользоваться, не нарушив обратной совместимости с более ранними версиями Java.
Следует отдать должное проектировщикам Java 9: по-видимому, они это учли и хорошо поработали, чтобы предоставить эти новые возможности и тем разработчикам, которым приходится поддерживать старые версии Java.
Разноверсионные JAR-файлы позволяют разработчикам применять новые возможности Java 9 и выносить их в отдельную часть JAR-файла, где более ранние версии Java их не заметят. Таким образом, разработчику легко писать код для Java 9, оставить старый код для Java 8 и ниже и предоставить среде исполнения выбор — какие классы она сможет запустить.
Благодаря модулям Java, разработчику проще проверять зависимости, достаточно писать все новые JAR-файлы в модульном виде, а старый код оставлять немодуляризованным. Система очень щадящая, она ориентирована на постепенную миграцию и практически всегда поддерживает работу с унаследованным кодом, который «слыхом не слыхивал» о модульной системе.
Благодаря модульному JDK и jlink, можно с легкостью создавать исполняемые образы и гарантированно обеспечивать приложение такой средой исполнения, в которой будет все необходимое для работы. Ранее такой процесс был чреват множеством ошибок, но современный инструментарий Java позволяет его автоматизировать — и все просто работает.
В отличие от предыдущих релизов Java, в Java 9 вы можете свободно пользоваться всеми новыми возможностями, а если имеете дело со старым приложением и должны гарантировать его поддержку — то сможете удовлетворить потребности всех ваших пользователей, независимо от того, горят ли они желанием обновляться до самой актуальной версии Java.