JFR 事件用於偵測 Java 中已棄用方法的調用
1.概述
JDK 22 引進了一個新的 JFR(Java Flight Recorder)事件,用於偵測 JDK 中已棄用方法jdk.DeprecatedInvocation
使用情況。 jdk.DeprecatedInvocation 事件記錄來自 JDK 外部的直接方法呼叫。在本文中,我們將探討如何捕捉這些事件,並分析記錄這些事件和不記錄這些事件的場景。
2. 設定
為了示範這些功能,讓我們使用一個呼叫已棄用的 JDK API 的簡單 Spring Boot REST 資源:
@RestController
public class DeprecatedApiDemo {
@GetMapping("/deprecated")
public String triggerDeprecated() {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.setProperty("demo.log", "true");
return null;
});
Boolean b = new Boolean("true");
return "Done";
}
}
AccessController
類別自 JDK 17 起已被棄用,並標記為將在未來的 JDK 版本中移除;而Boolean
建構函數自 JDK 9 起已被棄用,並標記為將被移除。當我們從 JDK 外部呼叫這些方法時,JFR 應該能夠偵測到該操作。
也要確保我們運行的是 JDK 22 或更高版本。如果使用SDKMAN
,我們可以如下切換 SDK:
$ sdk use java <sdk-identifier>
3. 捕捉事件
啟動我們的服務時,讓我們在啟動命令中指定-XX:StartFlightRecording
標誌,以便捕捉事件:
$ mvn spring-boot:run -Dspring-boot.run.jvmArguments="-XX:StartFlightRecording=filename=demo-deprecated.jfr"
此命令中的航班記錄標誌僅報告從 JDK 外部發起的直接已棄用的 JDK 方法調用,特別是那些標有@Deprecated( **forRemoval=true** )
的調用。 demo-deprecated.jfr
檔案在服務啟動時創建,但其內容僅在服務終止後才會刷新到該檔案中。
接下來,讓我們呼叫我們的 REST 端點:
$ curl http://localhost:8080/deprecated
讓我們停止正在運行的服務並檢查飛行記錄檔案中是否存在已棄用的方法呼叫:
$ jfr view jdk.DeprecatedInvocation demo-deprecated.jfr
此命令列印類似以下內容的輸出:
| Deprecated Method Invocation |
| Time | Stack Trace | Deprecated Method | Invocation Time | For Removal |
|----------|------------------------------------------|------------------------------------------|-----------------|-------------|
| 10:22:09 | org.apache.tomcat.util.threads.ThreadP...| java.lang.System.getSecurityManager() | 10:22:09 | true |
| 10:22:09 | com.baeldung.jfr_event_demo.Deprecated...| java.lang.Boolean.<init>(String) | 10:22:06 | true |
| 10:22:09 | com.baeldung.jfr_event_demo.Deprecated...| java.security.AccessController.doPrivi...| 10:22:06 | true |
| 10:22:09 | jakarta.security.auth.message.config.A...| java.security.AccessController.doPrivi...| 10:22:06 | true |
| 10:22:09 | jakarta.security.auth.message.config.A...| java.security.AccessController.doPrivi...| 10:22:06 | true |
| 10:22:09 | jakarta.security.auth.message.config.A...| java.lang.System.getSecurityManager() | 10:22:06 | true |
| 10:22:09 | com.baeldung.jfr_event_demo.StartupTes...| java.security.AccessController.doPrivi...| 10:21:57 | true |
| 10:22:09 | org.apache.tomcat.util.threads.Constan...| java.lang.System.getSecurityManager() | 10:21:56 | true |
| 10:22:09 | org.apache.tomcat.util.compat.JrePlatf...| java.lang.System.getSecurityManager() | 10:21:56 | true |
| 10:22:09 | org.apache.tomcat.util.threads.TaskThr...| java.lang.System.getSecurityManager() | 10:21:56 | true |
| 10:22:09 | org.apache.catalina.loader.WebappClass...| java.lang.System.getSecurityManager() | 10:21:56 | true |
| 10:22:09 | org.apache.catalina.Globals.<clinit>() | java.lang.System.getSecurityManager() | 10:21:56 | true |
| 10:22:09 | org.slf4j.LoggerFactory.getServiceLoad...| java.lang.System.getSecurityManager() | 10:21:55 | true |
從列印輸出中,我們可以看到只報告了標記為刪除的調用,即forRemoval
為true
。為了報告我們服務使用的所有已棄用的 JDK 方法,包括那些標記為@Deprecated( **forRemoval=false** )
的方法,讓我們更新啟動命令以包含此設定:
$ mvn spring-boot:run -Dspring-boot.run.jvmArguments="-XX:StartFlightRecording:jdk.DeprecatedInvocation#level=all,filename=demo-deprecated.jfr"
all
等級標誌表示應報告所有已棄用的呼叫。執行curl
指令並停止服務後, demo-deprecated.jfr
檔案應顯示類似以下內容的清單:
| Deprecated Method Invocation |
| Time | Stack Trace | Deprecated Method | Invocation Time | For Removal |
|----------|------------------------------------------|------------------------------------------|-----------------|-------------|
| 11:55:12 | org.apache.tomcat.util.threads.ThreadP... | java.lang.System.getSecurityManager() | 11:55:12 | true |
| 11:55:12 | com.fasterxml.jackson.databind.util.in... | java.lang.Thread.getId() | 11:55:08 | false |
| 11:55:12 | com.baeldung.jfr_event_demo.Deprecated... | java.math.BigDecimal.setScale(int, int) | 11:55:08 | false |
| 11:55:12 | com.baeldung.jfr_event_demo.Deprecated... | java.lang.Boolean.<init>(String) | 11:55:08 | true |
| 11:55:12 | com.baeldung.jfr_event_demo.Deprecated... | java.security.AccessController.doPrivi... | 11:55:08 | true |
| 11:55:12 | jakarta.security.auth.message.config.A... | java.security.AccessController.doPrivi... | 11:55:08 | true |
| 11:55:12 | jakarta.security.auth.message.config.A... | java.security.AccessController.doPrivi... | 11:55:08 | true |
| 11:55:12 | jakarta.security.auth.message.config.A... | java.lang.System.getSecurityManager() | 11:55:08 | true |
| 11:55:12 | org.apache.coyote.Request.setRequestTh... | java.lang.Thread.getId() | 11:55:08 | false |
| 11:55:12 | com.baeldung.jfr_event_demo.StartupTes... | java.security.AccessController.doPrivi... | 11:55:02 | true |
| 11:55:12 | org.apache.catalina.webresources.Cache... | java.net.URL.<init>(...) | 11:55:02 | false |
| 11:55:12 | org.springframework.util.ConcurrentLru... | java.lang.Thread.getId() | 11:55:02 | false |
| 11:55:12 | org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 11:55:02 | false |
| 11:55:12 | org.apache.tomcat.util.threads.Constan... | java.lang.System.getSecurityManager() | 11:55:02 | true |
| 11:55:12 | org.apache.tomcat.util.compat.JrePlatf... | java.lang.System.getSecurityManager() | 11:55:02 | true |
| 11:55:12 | org.apache.tomcat.util.threads.TaskThr... | java.lang.System.getSecurityManager() | 11:55:02 | true |
| 11:55:12 | org.apache.catalina.loader.WebappClass... | java.lang.System.getSecurityManager() | 11:55:02 | true |
| 11:55:12 | org.apache.catalina.Globals.<clinit>() | java.lang.System.getSecurityManager() | 11:55:02 | true |
| 11:55:12 | org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 11:55:01 | false |
| 11:55:12 | org.springframework.util.ResourceUtils... | java.net.URL.<init>(String) | 11:55:01 | false |
| 11:55:12 | org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 11:55:01 | false |
| 11:55:12 | org.slf4j.LoggerFactory.getServiceLoad... | java.lang.System.getSecurityManager() | 11:55:01 | true |
從該表中我們可以看到,甚至已棄用但尚未標記為刪除的方法也被報告了。
4. 無效的情況
如前所述,JFR 僅報告已棄用的 JDK 方法,這意味著不會報告 JDK 以外的已棄用的欄位和方法。為了示範這一點,讓我們棄用一個存在於 JDK 之外的方法:
@Component
public class LegacyClass {
@Deprecated(forRemoval = true)
public void oldMethod() {
System.out.println("Deprecated method");
}
}
接下來,讓我們公開並呼叫此方法的端點:
@RestController
public class DeprecatedApiDemo {
@Autowired
LegacyClass legacyClass;
//...
@GetMapping("/deprecated2")
public String triggerDeprecated2() {
legacyClass.oldMethod();
return "Completed";
}
}
$ curl http://localhost:8080/deprecated2
檢查產生的 JFR 檔案發現沒有記錄oldMethod
方法:
| Deprecated Method Invocation |
| Time | Stack Trace | Deprecated Method | Invocation Time | For Removal |
|----------|------------------------------------------|-------------------------------------------|-----------------|-------------|
| 13:33:55 |org.apache.coyote.Constants.<clinit>() | java.lang.System.getSecurityManager() | 13:33:50 | true |
| 13:33:55 |com.fasterxml.jackson.databind.util.in... | java.lang.Thread.getId() | 13:33:50 | false |
| 13:33:55 |jakarta.security.auth.message.config.A... | java.security.AccessController.doPrivi... | 13:33:50 | true |
| 13:33:55 |jakarta.security.auth.message.config.A... | java.security.AccessController.doPrivi... | 13:33:50 | true |
| 13:33:55 |jakarta.security.auth.message.config.A... | java.lang.System.getSecurityManager() | 13:33:50 | true |
| 13:33:55 |org.apache.coyote.Request.setRequestTh... | java.lang.Thread.getId() | 13:33:50 | false |
| 13:33:55 |com.baeldung.jfr_event_demo.StartupTes... | java.security.AccessController.doPrivi... | 13:33:40 | true |
| 13:33:55 |org.apache.catalina.webresources.Cache... | java.net.URL.<init>(...) | 13:33:39 | false |
| 13:33:55 |org.springframework.util.ConcurrentLru... | java.lang.Thread.getId() | 13:33:39 | false |
| 13:33:55 |org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 13:33:39 | false |
| 13:33:55 |org.apache.tomcat.util.threads.Constan... | java.lang.System.getSecurityManager() | 13:33:39 | true |
| 13:33:55 |org.apache.tomcat.util.compat.JrePlatf... | java.lang.System.getSecurityManager() | 13:33:39 | true |
| 13:33:55 |org.apache.tomcat.util.threads.TaskThr... | java.lang.System.getSecurityManager() | 13:33:39 | true |
| 13:33:55 |org.apache.catalina.loader.WebappClass... | java.lang.System.getSecurityManager() | 13:33:39 | true |
| 13:33:55 |org.apache.catalina.Globals.<clinit>() | java.lang.System.getSecurityManager() | 13:33:39 | true |
| 13:33:55 |org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 13:33:39 | false |
| 13:33:55 |org.springframework.util.ResourceUtils... | java.net.URL.<init>(String) | 13:33:38 | false |
| 13:33:55 |org.springframework.util.ReflectionUti... | java.lang.reflect.AccessibleObject.isA... | 13:33:38 | false |
| 13:33:55 |org.slf4j.LoggerFactory.getServiceLoad... | java.lang.System.getSecurityManager() | 13:33:38 | true |
5. 特殊注意事項
產生事件的一個限制是,除非呼叫是 JIT 編譯的,否則只會報告一個呼叫點。讓我們透過從同一個類別呼叫相同的已棄用方法來演示這一點:
@Component
public class LegacyClass {
//...
public void callDeprecatedMethod() {
Boolean boolean1 = new Boolean("true");
}
public void wrapperCall() {
callDeprecatedMethod();
Boolean boolean2 = new Boolean("false");
}
}
讓我們公開並呼叫這個方法:
@RestController
public class DeprecatedApiDemo {
@Autowired
LegacyClass legacyClass;
//...
@GetMapping("/deprecated3")
public String triggerDeprecated3() {
legacyClass.wrapperCall();
return "Finished";
}
}
$ curl http://localhost:8080/deprecated3
產生的 JFR 檔案僅顯示一個呼叫站點:
| Deprecated Method Invocation |
| Time | Stack Trace | Deprecated Method | Invocation Time | For Removal |
|--------- |------------------------------------------|------------------------------------------|-----------------|-------------|
| 13:55:06 | org.apache.tomcat.util.threads.ThreadP...| java.lang.System.getSecurityManager() | 13:55:06 | true |
| 13:55:06 | com.fasterxml.jackson.databind.util.in...| java.lang.Thread.getId() | 13:54:59 | false |
| 13:55:06 | com.baeldung.jfr_event_demo.LegacyClas...| java.lang.Boolean.<init>(String) | 13:54:59 | true |
| 13:55:06 | jakarta.security.auth.message.config.A...| java.security.AccessController.doPrivi...| 13:54:59 | true |
讓我們調整呼叫以手動觸發 JIT 編譯:
@Component
public class LegacyClass {
//...
public void wrapperCall() {
for (int i = 0; i < 26000; i++) {
callDeprecatedMethod();
}
callDeprecatedMethod();
Boolean boolean2 = new Boolean("false");
}
}
重新啟動服務並重新運行請求後,我們現在看到兩個針對已棄用方法呼叫的呼叫網站:
| Deprecated Method Invocation |
| Time | Stack Trace | Deprecated Method | Invocation Time | For Removal |
|----------|------------------------------------------|------------------------------------------|-----------------|-------------|
| 14:15:53 | org.apache.tomcat.util.threads.ThreadP...| java.lang.System.getSecurityManager() | 14:15:53 | true |
| 14:15:53 | org.apache.coyote.Constants.<clinit>() | java.lang.System.getSecurityManager() | 14:15:42 | true |
| 14:15:53 | com.fasterxml.jackson.databind.util.in...| java.lang.Thread.getId() | 14:15:42 | false |
| 14:15:53 | com.baeldung.jfr_event_demo.LegacyClas...| java.lang.Boolean.<init>(String) | 14:15:42 | true |
| 14:15:53 | com.baeldung.jfr_event_demo.LegacyClas...| java.lang.Boolean.<init>(String) | 14:15:42 | true |
| 14:15:53 | jakarta.security.auth.message.config.A...| java.security.AccessController.doPrivi...| 14:15:42 | true |
6. 結論
在本文中,我們研究了新的 JFR 事件,該事件用於偵測已棄用的 JDK 方法的呼叫。我們探討瞭如何在啟動時配置服務以報告這些實例,最後,我們討論了無法捕獲此類事件的情況。
與往常一樣,程式碼可在 GitHub 上取得。