В Java есть специальная аннотация @Deprecated для маркировки уставшего кода. С определенной периодичностью такой код из JDK удаляется. Обычно о конкретных сроках удаления анонс делается заранее и в теории можно успеть подготовиться, но на практике не все так просто.

В больших проектах найти куски устаревшего кода в куче зависимостей задача не тривиальная и требующая хорошей автоматизации. В этой ситуации к нам приходит на помощь новый тип события в JFR. Он был добавлен в JDK 22.

Давайте посмотрим на простом примере как это работает.


Для начала создадим простой класс и в его методе main вызовим любой @Deprecated метод. Для примера я выбрал конструктор класса java.net.URL(String). Со списком всех методов помеченных аннотацией @Deprecated вы можете ознакомиться по этой ссылке

public class App
{
    public static void main(String[] args) throws MalformedURLException {

        URL url = new URL("https://habr.com/ru/");
    }
}

Если вы попробуете скомпилировать его при помощи javac, то получите сообщение c предупреждением о том, что у вас в коде используется устаревший код.

~/IdeaProjects/Tests/src/main/java (master*) » javac App.java                                                                                                         user@user-home
Note: App.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

Если добавить в параметры запуска еще и -Xlint:deprecation вывод станет чуть более информативен.

~/IdeaProjects/Tests/src/main/java (master*) » javac -Xlint:deprecation App.java                                                                                      user@user-home
App.java:8: warning: [deprecation] URL(String) in URL has been deprecated
        URL url = new URL("https://habr.com/ru/");
                  ^
1 warning

Но таким "дедовским" способом реальные проекты никто не собирает. Обычно используются инструменты автоматизирующие сборку. На данный момент довольно широко используются Maven и Gradle.

Если сделать простой Maven проект с таким pom.xml, то к удивлению мы не получим warning при компиляции того файла.

Hidden text
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.rodindenis</groupId>
    <artifactId>Tests</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>22</maven.compiler.source>
        <maven.compiler.target>22</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

Для получегия таких warning нужно будет еще добавить параметр конфигурации showDeprecation для maven-compiler-plugin.

Hidden text
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.rodindenis</groupId>
    <artifactId>Tests</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>22</maven.compiler.source>
        <maven.compiler.target>22</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Что будет если этот кусок кода уедет в библиотеку и мы с вами будем ее получать как уже скомпилированную зависимость? Ответ под спойлером.

Hidden text

Так как компилятор проверяет в момент компиляции, то соответственно warning в момент компиляции нашего кода мы не получим.

Давайте проверим? Я вынес вызов устаревшего кода в отдельный сабмодуль.

package com.github.rodindenis.jfr.event.lib;

import java.net.MalformedURLException;
import java.net.URL;

public class Printer {

    public static void print(String url) throws MalformedURLException {
        System.out.println(new URL(url).toString());
    }
}

Наш класс App тоже изменился

package com.github.rodindenis.jfr.event.main;

import com.github.rodindenis.jfr.event.lib.Printer;
import java.net.MalformedURLException;

public class App
{
    public static void main(String[] args) throws MalformedURLException {

        Printer.print("https://habr.com/ru/");
    }
}

Примеры pom.xml здесь не привожу. Их можно будет посмотреть в репозитории.

Как и ожидалось после компиляции класса Printer мы получаем warning, но вот когда мы уже подключаем уже скомпилированную библиотеку с этим классом как зависимость и пробуем скомпилировать App, то warning мы уже не ловим.

Для воспроизведения ситуации в скаченном из репозитория кода проведите следующие манипуляции. Сначала скомпилируйте и установитие все артефакты. На этом этапе вы увидите в логе warning при компиляции библиотеки.

mvn clean install

Так как у нас теперь локально установлена скомпилированная библиотека с кодом класса Printer, то мы можем попробовать отдельно перекомпилировать только основной код App. Эту команду выполняем только для сабмодуля main с классом App.

mvn clean package

И вот пришло время ощутить всю силу JFR.

Запускаем наше приложение с включенным JFR логированием в файл. Я перешел в каталог main/target своего проекта и из него выполнил команду

~/.jdks/temurin-22.0.2/bin/java -XX:StartFlightRecording:jdk.DeprecatedInvocation#level=all,filename=recording.jfr -cp ../../lib/target/lib-1.0-SNAPSHOT.jar:./main-1.0-SNAPSHOT.jar com.github.rodindenis.jfr.event.main.App

В текущем каталоге появился файл recording.jfr. Давайте посмотрим есть ли в нем нужное нам событие.

~/.jdks/temurin-22.0.2/bin/jfr print --events jdk.DeprecatedInvocation recording.jfr

В моем кейсе было напечатано

jdk.DeprecatedInvocation {
  startTime = 16:39:18.769 (2024-08-19)
  method = java.net.URL.<init>(String)
  invocationTime = 16:39:18.759 (2024-08-19)
  forRemoval = false
  stackTrace = [
    com.github.rodindenis.jfr.event.lib.Printer.print(String) line: 9
    ...
  ]
}

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

Комментарии (4)


  1. novoselov
    19.08.2024 16:20

    Что будет если этот кусок кода уедет в библиотеку и мы с вами будем ее получать как уже скомпилированную зависимость?

    А можно просто падать при warning'ах и такой код не будет компилироваться.

    <failOnWarning>true</failOnWarning>


    1. c0d3_r3d Автор
      19.08.2024 16:20

      Я отчасти согласен, что можно было добавить в статью про этот кейс, но не хотел слишком расплываться.

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

      Новое событие в JFR начиная с JDK22 решает эту задачу, но опять же частично. Мы можем отлавливать использование такого кода в рантайме.


      1. Antgor
        19.08.2024 16:20

        Падать при компиляции - это безумие. Ну если это pet-проект, то наверное да. Но Deprecated может висеть годами и десятилетиями. Например Thread.stop() и еще несколько методов ушли в Deprecated в Java 1.2 !!! 26 лет назад. И до сих пор висит со статусом "Deprecated, for removal: This API element is subject to removal in a future version."

        А вот собирать в рантайме через JFR и рефакторить по мере обнаружения - очень правильный путь.


        1. c0d3_r3d Автор
          19.08.2024 16:20

          Согласен.

          Лучше быть в курсе используемого устаревшего кода в проекте, но при этом не ломая сбокри и в "спокойном режиме" планировать переход на целевые версии API.

          Новое событие в JFR позволит собирать такую информацию в том числе и для зависимостей в проекте.