New event in JFR to diagnose use of deprecated code

Java has a special annotation @Deprecated for marking outdated code. Such code is removed from the JDK at certain intervals. Usually, specific removal dates are announced in advance and, in theory, you can have time to prepare, but in practice, things are not so simple.

In large projects, finding pieces of obsolete code in a bunch of dependencies is not a trivial task and requires good automation. In this situation, a new event type in JFR comes to our aid. It was added in JDK 22.

Let's look at a simple example of how this works.


First, let's create a simple class and call any @Deprecated method in its main method. For this example, I chose the java.net.URL(String) class constructor. With a list of all methods marked with the @Deprecated annotation, you can check out this link

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

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

If you try to compile it using javacyou will receive a warning message that your code uses outdated code.

~/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.

If you add -Xlint:deprecation to the launch parameters, the output will become a little more informative.

~/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

But no one assembles real projects in this “old-fashioned” way. Usually, tools that automate the assembly are used. At the moment, they are quite widely used Maven And Gradle.

If we make a simple Maven project with such pom.xml, then surprisingly we will not get a warning when compiling that file.

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>

To get such warnings you will need to add the showDeprecation configuration parameter to the 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>

What will happen if this piece of code goes to the library and we will receive it as an already compiled dependency? The answer is under the spoiler.

Hidden text

Since the compiler checks at the time of compilation, we will not receive a warning at the time of compilation of our code.

Let's check? I moved the call to the legacy code to a separate submodule.

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());
    }
}

Our App class has changed too.

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/");
    }
}

I won't provide pom.xml examples here. You can see them in the repository.

As expected, after compiling the Printer class we get a warning, but when we already connect the already compiled library with this class as a dependency and try to compile App, we no longer catch the warning.

To reproduce the situation in the code downloaded from the repository, perform the following manipulations. First, compile and install all artifacts. At this stage, you will see a warning in the log when compiling the library.

mvn clean install

Since we now have a locally installed compiled library with the Printer class code, we can try to recompile only the main App code separately. We execute this command only for the main submodule with the App class.

mvn clean package

And now it's time to feel the full power of JFR.

We launch our application with JFR logging to a file enabled. I went to the main/target directory of my project and executed the command from there

~/.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

The file recording.jfr has appeared in the current directory. Let's see if it contains the event we need.

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

It was printed in my case

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
    ...
  ]
}

Important caveat: This approach will allow you to detect calls to outdated code at runtime, but if the code is not called, you will not receive such an event.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *