2019 был очень насыщенным годом в плане конференций. Наша команда могла уезжать на целые недели в командировки. А как известно, конференция – время делиться знаниями. Помимо того, что мы выступали с докладами и много интересного рассказывали на нашем стенде, мы также узнавали много нового от общения с участниками конференции и от докладчиков. Так вот на осенней конференции Joker 2019 доклад от Dalia Abo Sheasha "Migrating beyond Java 8" вдохновил нас на реализацию нового диагностического правила, которое позволяет выявлять несовместимости в Java SE API между разными версиями Java. Об этом и пойдет речь.
На текущей момент уже вышла Java SE 14. Несмотря на это, многие компании продолжают использовать прежние версии Java (Java SE 6, 7, 8, ...). В связи с тем, что время идет, и Java все время обновляется, то проблема совместимости различных версий Java SE API с каждым годом становится все актуальней.
Когда выходят новые версии Java SE, то они, как правило, обратно совместимы с более ранними версиями, то есть, например, приложение разработанное на основе Java SE 8 должно без проблем запуститься на 11 версии Java. Однако на практике может возникать некоторая несовместимость в ряде классов и методов. Эта несовместимость заключаются в том, что некоторые API претерпевают изменения: удаляются, меняются в поведении, помечаются как устаревшие и многое другое.
Эта проблема только обострится, когда вы начнете задумываться о миграции своего проекта на более свежую Java SE. Или же когда в техническую поддержку вашего приложения будут все чаще и чаще приходить письма о том, что приложение некорректно себя ведет или вообще не может запуститься.
Думаю, этого вполне достаточно, чтобы заострить внимание!
Для перехода с одной версии Java на другую не существует универсального и простого решения. А если ваше приложение – результат непрерывной многолетней разработки с применением множества нетривиальных решений, то, когда вы озадачитесь переходом на свежую версию Java, вы с большой вероятностью можете столкнуться с достаточно трудоемким процессом.
Этот процесс заключается в выявлении проблемного или потенциально проблемного API, который необходимо пересмотреть и заменить альтернативным решением. Это может достаточно серьезно затронуть бизнес-логику, на исправление и тестирование которой может уйти уйма времени. Если надо не только перейти на новую версию Java SE, а ещё обеспечить совместимость с рядом версий, то это задача многократно усложнится.
Покопавшись в просторах интернета на тему выявления несовместимости API между разными Java SE, мне встретились только инструменты, которые идут с JDK: javac, jdeps, jdeprscan.
Стороннего инструмента в этом деле так и не нашлось, кроме того, с которым я имел честь познакомиться на докладе Joker 2019 - Migration Toolkit for Application Binaries.
Когда разрабатываете приложение, не стоит забывать про предупреждения компилятора. У всех разное отношение к предупреждениям:
Первый вариант хорош тем, что часть проблем можно не откладывая решить, например, просто не использовать метод или класс, который помечен как устаревший. Использование такого API не является блокирующей проблемой, однако на него следует обратить внимание, так как уже появляется вероятность, что при использовании вновь вышедшей версии Javа ваше приложение иначе себя поведет или вовсе упадет.
Если вы уже озадачились вопросом миграции вашего приложения на более свежую версию Java SE, то в помощь вам спешит jdeps.
jdeps – инструмент командной строки, производящий статический анализ зависимостей вашего приложения и библиотек, принимая на вход *.class файлы или *.jar. Начиная с Java SE 8, он идет в комплекте с JDK.
И тут нас интересует то, что если вы запустите этот инструмент с опцией ‑‑jdk-internals, то он вам сообщит, от какого внутреннего JDK API зависит каждый ваш класс. Это очень важный момент в силу того, что внутренний API не гарантирует, что он не изменится в следующих версиях Java.
Давайте рассмотрим пример. Допустим вы долгое время разрабатывали свое приложение на Java 8 и никогда не озадачивались совместимостью используемого Java SE API с более свежими версиями. Встал вопрос о миграции вашего приложения на, например, Java 11. В таком случае можно пойти по пути минимального сопротивления и сразу же запустить приложение при помощи Java 11. Но что-то пошло не так, и, предположим, запуск вашего приложения закончился падением. В таком случае можно запустить jdeps из Java 11, натравив на файлы *.jar вашего приложения.
Результат примерно будет следующим:
Из вывода видно, что зависимость sun.misc.BASE64Encoder в Java 11 будет удалена. И падение вашего приложения, когда сперва запускали на Java 11, скорее всего связано с ошибкой java.lang.NoClassDefFoundError. Помимо этого информирования, что не может ни радовать, jdeps может предлагать вам альтернативную зависимость, которую можно использовать вместо текущей. В данном случае он предложил удаленную зависимость заменить на java.util.Base64.
Второе предупреждение инструмента сигнализирует о том, что в коде мы еще используем другую внутреннюю зависимость, но пока она корректна. Будут ли изменения этого API в следующих версии Java неизвестно, но это нужно принять во внимание.
Разумеется, jdeps может сделать нечто большее, чем просто проверить использование внутренних компонентов JDK. Это не входит в рамки данной статьи, но вы можете с его возможностями ознакомиться на официальной страничке самостоятельно.
Цели у jdeprscan ровно такие же, как и у jdeps, а именно, помощь в поиске нежелательного и проблемного API.
jdeprescan - инструмент статического анализа, который сканирует *.jar файл (или некоторую другую совокупность *.class файлов) на предмет использования устаревших элементов API. Использование устаревшего API не является блокирующей проблемой, однако на него следует обращать внимание. Начиная с Java SE 9, он идет в комплекте с JDK.
Также предположим, что стоит вопрос миграции приложения на Java 11. В таком случае, запустив команду
jdeprscan --release 8 app.jar
вы получите список API, который стал нерекомендуемым для Java 8, то есть тот API, который в будущих версиях Java может быть удален. Исправив все предупреждения, можно запустить
jdeprscan --release 11 app.jar
который выдаст список API, устаревший уже для Java 11. Таким образом, можно найти и исправить (если потребуется) весь нерекомендуемый API.
Этот инструмент ориентирован на помощь в быстром оценивании пользовательского приложения на наличие потенциальных проблем перед развертыванием на различных серверах (JBOSS, WebShere, Tomcat, WebLogic, ...). Помимо всего функционала инструмент также позволяет выявлять различия в Java SE API разных версий.
Вкратце взглянем, что из себя представляет этот инструмент.
Быстрый запуск инструмента выглядит так:
java -jar binaryAppScanner.jar yourApp.jar --analyzeJavaSE
--sourceJava=oracle8 --targetJava=java11 ....
Опция analyzeJavaSE подразумевает использование различных параметров, о которых вы можете узнать, вызвав help.
После запуска анализа вам вскоре откроется отчет в веб-браузере:
На скрине не все поместилось =(. А пока вы еще не пробовали запускать у себя этот инструмент, я опишу словами.
В отчете можно увидеть срабатывания правил с 3-мя уровнями серьезности:
Каждое срабатывание можно развернуть и увидеть описание что да как. В информационных сообщениях есть рекомендация, мол запустите jdeps дополнительно помимо нас. Мол мы ориентированы на миграцию приложения, а jdeps дополнительно поможет обнаружить проблему во внутренних пакетах JDK (помимо тех, что находят они).
Также внизу отчета вы сможете найти список правил, согласно которым производился анализ.
Если вы используете Eclipse IDE, то вы можете воспользоваться плагином. Для более детального изучения стоит посетить их страничку.
После изучения рассмотренных инструментов, мы пришли к выводу, что поиск потенциальных проблем совместимости разных версий Java SE API - достойная задача для статического анализа.
Рассмотренные инструменты действительно облегчат задачу миграции приложения или поиска API, который может ломать вам работоспособность приложения на более свежих версиях Java SE (при нежелании мигрировать приложение). Но подумав, что эти инструменты все время нужно запускать в командной строке отдельно от процесса разработки для выявления проблем, пришли к решению, что это не совсем удобно. И отталкиваясь от того, что статический анализ необходим именно для обнаружения проблемного или потенциально проблемного кода на самых ранних этапах разработки, реализовали диагностическое правило V6078, которое и будет вам сигнализировать о "проблемном" API.
Правило V6078 заранее предупредит вас о том, что ваш код зависим от некоторых функций и классов Java SE API, которые на следующих версиях Java могут доставить вам трудности. И вы еще на самом старте реализации той или иной фичи не будете завязываться на этот API, тем самым уменьшая технические риски в будущем.
Диагностическое правило выдает предупреждения в следующих случаях:
Правило на данный момент позволяет проанализировать совместимость Oracle Java SE с 8 по 14 версий. Чтобы правило стало активным, его необходимо настроить.
В IntelliJ IDEA плагине вам необходимо во вкладке Settings > PVS-Studio > API Compatibility Issue Detection включить правило и указать параметры, а именно:
Используя gradle плагин, вам необходимо сконфигурировать настройки анализатора в build.gardle:
apply plugin: com.pvsstudio.PvsStudioGradlePlugin
pvsstudio {
....
compatibility = true
sourceJava = /*version*/
targetJava = /*version*/
excludePackages = [/*pack1, pack2, ...*/]
}
Используя maven плагин, вам необходимо сконфигурировать настройки анализатора в pom.xml:
<build>
<plugins>
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
....
<configuration>
<analyzer>
....
<compatibility>true</compatibility>
<sourceJava>/*version*/</sourceJava>
<targetJava>/*version*/</targetJava>
<excludePackages>/*pack1, pack2, ...*/</excludePackages>
</analyzer>
</configuration>
</plugin>
</plugins>
</build>
Если вы используете анализатор напрямую через командную строку, то, чтобы активировать анализ совместимости выбранных Java SE API, необходимо использовать следующие параметры:
java -jar pvs-studio.jar /*other options*/ --compatibility
--source-java /*version*/ --target-java /*version*/
--exclude-packages /*pack1 pack2 ... */
Давайте предположим, что мы разрабатываем приложение на базе Java SE 8, и у нас есть класс со следующим содержимым:
/* imports */
import java.util.jar.Pack200;
public class SomeClass
{
/* code */
public static void someFunction(Pack200.Packer packer, ...)
{
/* code */
packer.addPropertyChangeListener(evt -> {/* code */});
/* code */
}
}
Запустив статический анализ с различными параметрами настройки диагностического правила, мы будем наблюдать следующую картину:
Сначала в Java SE 9 был удален метод 'addPropertyChangeListener' в классе 'Pack200.Packer'. В 11 версии к этому добавился тот факт, что класс 'Pack200' пометили как устаревший. А в 14 версии вовсе этот класс был удален.
Поэтому, запустив приложение на Java 11, вы получите 'java.lang.NoSuchMethodError', а если запустите на Java 14 – 'java.lang.NoClassDefFoundError'.
Зная эту информацию, при разработке своего приложения вы будете рассматривать альтернативные решения поставленной задачи.
В ходе реализации диагностического правила возникли идеи по расширению:
Это все вопросы исследования, и что получится в итоге, покажет время =) Если вы знаете интересные сценарии для этой диагностики и хотели бы увидеть их в PVS-Studio, то напишите нам.
Поиск потенциальных ошибок несовместимости полностью соответствует нашей идеологии – использование PVS-Studio для поиска и исправления ошибок на ранних этапах написания кода. Как и опечатку, вызов специфичной функции можно внести в код в любой момент, поэтому регулярно запускать PVS-Studio на проекте теперь в двойне полезнее.
Диагностика V6078 доступна в анализаторе, начиная с версии 7.08. Скачать и попробовать анализатор на своём проекте можно на странице загрузки.