前段時間公司領導讓我排查一個關于在 JDK21 環境中使用 Spring Boot 配合一個 JDK18 新增的一個 SPI(java.net.spi.InetAddressResolverProvider) 不生效的問題。
但這個不生效的前置條件有點多:
才會導致自定義的 InetAddressResolverProvider 無法正常工作。
在復現這個問題之前先簡單介紹下 java.net.spi.InetAddressResolverProvider 這個 SPI;它是在 JDK18 之后才提供的,在這之前我們使用 InetAddress 的內置解析器來解析主機名和 IP 地址,但這個解析器之前是不可以自定義的。
在某些場景下會不太方便,比如我們需要請求 order.service 這個域名時希望可以請求到某一個具體 IP 地址上,我們可以自己配置 host ,或者使用服務發現機制來實現。
但現在通過 InetAddressResolverProvider 就可以定義在請求這個域名的時候返回一個我們預期的 IP 地址。
同時由于它是一個 SPI,所以我們只需要編寫一個第三方包,任何項目依賴它之后在發起網絡請求時都會按照我們預期的 IP 進行請求。
要使用它也很簡單,主要是兩個類:
public class MyAddressResolverProvider extends InetAddressResolverProvider { @Override public InetAddressResolver get(Configuration configuration) { return new MyAddressResolver(); } @Override public String name() { return "MyAddressResolverProvider Internet Address Resolver Provider"; }}public class MyAddressResolver implements InetAddressResolver { public MyAddressResolver() { System.out.println("=====MyAddressResolver"); } @Override public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException { if (host.equals("fedora")) { return Stream.of(InetAddress.getByAddress(new byte[] {127, 127, 10, 1})); } return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1})); } @Override public String lookupByAddress(byte[] addr) { System.out.println("++++++" + addr[0] + " " + addr[1] + " " + addr[2] + " " + addr[3]); return "fedora"; }}---```javaaddresses = InetAddress.getAllByName("fedora");// output: 127 127 10 1
這里我簡單實現了一個對域名 fedora 的解析,會直接返回 127.127.10.1。
如果使用 IP 地址進行查詢時:
InetAddress byAddress = InetAddress.getByAddress(new byte[]{127, 127, 10, 1});System.out.println("+++++" + byAddress.getHostName());// output: fedora
當然要要使得這個 SPI 生效的前提條件是我們需要新建一個文件:META-INF/services/java.net.spi.InetAddressResolverProvider里面的內容是我們自定義類的全限定名稱:
com.example.demo.MyAddressResolverProvider
這樣一個完整的 SPI 就實現完成了。
正常情況下我們將應用打包為一個 jar 之后運行:
java -jar target/demo-0.0.1-SNAPSHOT.jar
是可以看到輸出結果是符合預期的。
一旦我們使用配合上 spring boot 打包之后,也就是加上以下的依賴:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.3</version> <relativePath/> <!-- lookup parent from repository --> </parent><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
再次執行其實也沒啥問題,也能按照預期輸出結果。
但我們加上 OpenTelemetry 的 agent 時:
java -javaagent:opentelemetry-javaagent.jar / -jar target/demo-0.0.1-SNAPSHOT.jar
就會發現在執行解析的時候拋出了 java.net.UnknownHostException異常。
圖片
從結果來看就是沒有進入我們自定義的解析器。
在講排查過程之前還是要先預習下關于 Java SPI 的原理以及應用場景。
以前寫過一個 http 框架 cicada,其中有一個可拔插 IOC 容器的功能:
就是可以自定義實現自己的 IOC 容器,將自己實現的 IOC 容器打包為一個第三方包加入到依賴中,cicada 框架就會自動使用自定義的 IOC 實現。
要實現這個功能本質上就是要定義一個接口,然后根據依賴的不同實現創建接口的實例對象。
public interface CicadaBeanFactory { /** * Register into bean Factory * @param object */ void register(Object object); /** * Get bean from bean Factory * @param name * @return * @throws Exception */ Object getBean(String name) throws Exception; /** * get bean by class type * @param clazz * @param <T> * @return bean * @throws Exception */ <T> T getBean(Class<T> clazz) throws Exception; /** * release all beans */ void releaseBean() ;}
獲取具體的示例代碼時就只需要使用 JDK 內置的 ServiceLoader 進行加載即可:
public static CicadaBeanFactory getCicadaBeanFactory() { ServiceLoader<CicadaBeanFactory> cicadaBeanFactories = ServiceLoader.load(CicadaBeanFactory.class); if (cicadaBeanFactories.iterator().hasNext()){ return cicadaBeanFactories.iterator().next() ; } return new CicadaDefaultBean(); }
代碼也非常的簡潔,和剛才提到的 InetAddressResolverProvider 一樣我們需要新增一個 META-INF/services/top.crossoverjie.cicada.base.bean.CicadaBeanFactory 文件來配置我們的類名稱。
private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // PREFIX = META-INF/services/ String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true;}
在 ServiceLoader 類中會會去查找 META-INF/services 的文件,然后解析其中的內容從而反射生成對應的接口對象。
這里還有一個關鍵是通常我們的代碼都會打包為一個 JAR 包,類加載器需要加載這個 JAR 包,同時需要在這個 JAR 包里找到我們之前定義的那個 spi 文件,如果這里查不到文件那就認為沒有定義 SPI。
這個是本次問題的重點,會在后文分析原因的時候用到。
因為問題就出現在是否使用 opentelemetry-javaagent.jar 上,所以我需要知道在使用了 agent 之后有什么區別。
從剛才的對 SPI 的原理分析,加上 agent 出現異常,說明理論上就是沒有讀取到我們配置的文件: java.net.spi.InetAddressResolverProvider。
于是我便開始 debug,在 ServiceLoader 加載 jar 包的時候是可以看到具體使用的是什么 classLoader 。
這是不配置 agent 的時候使用的 classLoader:使用這個 loader 是可以通過文件路徑在 jar 包中查找到我們配置的文件。
而配置上 agent 之后使用的 classLoader:卻是一個 JarLoader,這樣是無法加載到在 springboot 格式下的配置文件的,至于為什么加載不到,那就要提一下 maven 打包后的文件目錄和 spring boot 打包后的文件目錄的區別了。
這里我截圖了同樣的一份代碼不同的打包方式:上面的是傳統 maven,下圖是 spring boot;其實主要的區別就是在 pom 中使用了一個構建插件:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
或者使用 spring-boot 命令再次打包的效果也是一樣的。
會發現 spring boot 打包后會多出一層 BOOT-INF 的文件夾,然后會在 MANIFIST.MF 文件中定義 Main-Class 和 Start-Class.
通過上面的 debug 其實會發現 JarLoader 只能在加載 maven 打包后的文件,也就是說無法識別 BOOT-INF 這個目錄。
正常情況下 spring boot 中會有一個額外的 java.nio.file.spi.FileSystemProvider 實現:通過這個類的實現可以直接從 JAR 包中加載資源,比如我們自定義的 SPI 資源等。
初步判斷使用 opentelemetry-javaagent.jar的 agent 之后,它的類加載器優先于了 spring boot ,從而導致后續的加載失敗。
這里穿插幾個 debug 小技巧,其中一個是遠程 debug,因為這里我是需要調試 javaagent,正常情況下是無法直接 debug 的。
所以我們可以使用以下命令啟動應用:
java -agentlib:jdwp="transport=dt_socket,server=y,suspend=y,address=5000" -javaagent:opentelemetry-javaagent.jar / -jar target/demo-0.0.1-SNAPSHOT.jar
然后在 idea 中配置一個 remote 啟動。
注意這里的端口得和命令行中的保持一致。
當應用啟動之后便可以在 idea 中啟動這個 remote 了,這樣便可以正常 debug 了。
第二個是條件斷點也非常有用,有時候我們需要調試一個公共函數,調用的地方非常多。
而我們只需要關心某一類行為的調用,此時就可以對這個函數中的變量進行判斷,當他們滿足某些條件時再進入斷點,這樣可以極大的提高我們的調試效率:
配置也很簡單,只需要在斷點上右鍵就可以編輯條件了。
雖然我根據現象初步可以猜測下原因,但依然不確定如何調整才能解決這個問題,于是便去社區提了一個 issue。
最后在社區大佬的幫助下發現我們需要禁用掉 OpenTelemetry agent 中的一個 resource 就可以了。
這個 resource 是由 agent 觸發的,它優先于 spring boot 之前進行 SPI 的加載。目的是為了給 metric 和 trace 新增兩個屬性:
加載的核心代碼在這里,只要禁用掉之后就不會再加載了。
禁用前:
禁用后:
當我們禁用掉之后就不會存在這兩個屬性了,不過我們目前并沒有使用這兩個屬性,所以為了使得 SPI 生效就只有先禁用掉了,后續再看看社區還有沒有其他的方案。
想要復現 debug 的可以在這里嘗試:https://github.com/crossoverJie/demo
參考連接:
本文鏈接:http://www.tebozhan.com/showinfo-26-88352-0.htmlOpenTelemetry agent 對 Spring Boot 應用的影響:一次 SPI 失效的調查
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 程序員為什么一定要去造幾個輪子
下一篇: C#事件:實現安全的發布/訂閱模型