您的位置:首頁(yè) > 軟件教程 > 教程 > OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

來(lái)源:好特整理 | 時(shí)間:2024-06-04 11:59:24 | 閱讀:145 |  標(biāo)簽: T Pi El Ri S in 應(yīng)用   | 分享到:

背景 前段時(shí)間公司領(lǐng)導(dǎo)讓我排查一個(gè)關(guān)于在 JDK21 環(huán)境中使用 Spring Boot 配合一個(gè) JDK18 新增的一個(gè) SPI(java.net.spi.InetAddressResolverProvider) 不生效的問(wèn)題。 但這個(gè)不生效的前置條件有點(diǎn)多: JDK 的版本得在 18+ Spri

背景

前段時(shí)間公司領(lǐng)導(dǎo)讓我排查一個(gè)關(guān)于在 JDK21 環(huán)境中使用 Spring Boot 配合一個(gè) JDK18 新增的一個(gè) SPI( java.net.spi.InetAddressResolverProvider ) 不生效的問(wèn)題。

但這個(gè)不生效的前置條件有點(diǎn)多:

  • JDK 的版本得在 18+
  • SpringBoot3.x
  • 還在額外再配合使用 -javaagent:opentelemetry-javaagent.jar 使用,也就是 OpenTelemetry 提供的 agent。

才會(huì)導(dǎo)致自定義的 InetAddressResolverProvider 無(wú)法正常工作。


在復(fù)現(xiàn)這個(gè)問(wèn)題之前先簡(jiǎn)單介紹下 java.net.spi.InetAddressResolverProvider 這個(gè) SPI;它是在 JDK18 之后才提供的,在這之前我們使用 InetAddress 的內(nèi)置解析器來(lái)解析主機(jī)名和 IP 地址,但這個(gè)解析器之前是不可以自定義的。

在某些場(chǎng)景下會(huì)不太方便,比如我們需要請(qǐng)求 order.service 這個(gè)域名時(shí)希望可以請(qǐng)求到某一個(gè)具體 IP 地址上,我們可以自己配置 host ,或者使用服務(wù)發(fā)現(xiàn)機(jī)制來(lái)實(shí)現(xiàn)。

但現(xiàn)在通過(guò) InetAddressResolverProvider 就可以定義在請(qǐng)求這個(gè)域名的時(shí)候返回一個(gè)我們預(yù)期的 IP 地址。

同時(shí)由于它是一個(gè) SPI,所以我們只需要編寫一個(gè)第三方包,任何項(xiàng)目依賴它之后在發(fā)起網(wǎng)絡(luò)請(qǐng)求時(shí)都會(huì)按照我們預(yù)期的 IP 進(jìn)行請(qǐng)求。

復(fù)現(xiàn)

要使用它也很簡(jiǎn)單,主要是兩個(gè)類:

  • InetAddressResolverProvider :這是一個(gè)抽象類,我們可以繼承它之后重寫它的 get 函數(shù)返回一個(gè) InetAddressResolver 對(duì)象
  • InetAddressResolver :一個(gè)接口,主要提供了兩個(gè)函數(shù);一個(gè)用于傳入域名返回 IP 地址,另一個(gè)反之:傳入 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 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";
    }
}

---

```java
addresses = InetAddress.getAllByName("fedora");
// output: 127 127 10 1

這里我簡(jiǎn)單實(shí)現(xiàn)了一個(gè)對(duì)域名 fedora 的解析,會(huì)直接返回 127.127.10.1 。

如果使用 IP 地址進(jìn)行查詢時(shí):

InetAddress byAddress = InetAddress.getByAddress(new byte[]{127, 127, 10, 1});

System.out.println("+++++" + byAddress.getHostName());
// output: fedora

當(dāng)然要要使得這個(gè) SPI 生效的前提條件是我們需要新建一個(gè)文件:
META-INF/services/java.net.spi.InetAddressResolverProvider
里面的內(nèi)容是我們自定義類的全限定名稱:

com.example.demo.MyAddressResolverProvider

這樣一個(gè)完整的 SPI 就實(shí)現(xiàn)完成了。


正常情況下我們將應(yīng)用打包為一個(gè) jar 之后運(yùn)行:

java -jar target/demo-0.0.1-SNAPSHOT.jar

是可以看到輸出結(jié)果是符合預(yù)期的。

一旦我們使用配合上 spring boot 打包之后,也就是加上以下的依賴:

  
  org.springframework.boot  
  spring-boot-starter-parent  
  3.2.3  
     


  
    
     
    org.springframework.boot  
    spring-boot-maven-plugin  
     
    

再次執(zhí)行其實(shí)也沒(méi)啥問(wèn)題,也能按照預(yù)期輸出結(jié)果。

但我們加上 OpenTelemetry 的 agent 時(shí):

java  -javaagent:opentelemetry-javaagent.jar \
      -jar target/demo-0.0.1-SNAPSHOT.jar

就會(huì)發(fā)現(xiàn)在執(zhí)行解析的時(shí)候拋出了 java.net.UnknownHostException 異常。

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
從結(jié)果來(lái)看就是沒(méi)有進(jìn)入我們自定義的解析器。

SPI 原理

在講排查過(guò)程之前還是要先預(yù)習(xí)下關(guān)于 Java SPI 的原理以及應(yīng)用場(chǎng)景。

以前寫過(guò)一個(gè) http 框架 cicada ,其中有一個(gè)可拔插 IOC 容器的功能:

就是可以自定義實(shí)現(xiàn)自己的 IOC 容器,將自己實(shí)現(xiàn)的 IOC 容器打包為一個(gè)第三方包加入到依賴中,cicada 框架就會(huì)自動(dòng)使用自定義的 IOC 實(shí)現(xiàn)。

要實(shí)現(xiàn)這個(gè)功能本質(zhì)上就是要定義一個(gè)接口,然后根據(jù)依賴的不同實(shí)現(xiàn)創(chuàng)建接口的實(shí)例對(duì)象。

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 
     * @return bean
     * @throws Exception
     */
     T getBean(Class clazz) throws Exception;

    /**
     * release all beans
     */
    void releaseBean() ;
}

獲取具體的示例代碼時(shí)就只需要使用 JDK 內(nèi)置的 ServiceLoader 進(jìn)行加載即可:

public static CicadaBeanFactory getCicadaBeanFactory() {  
    ServiceLoader cicadaBeanFactories = ServiceLoader.load(CicadaBeanFactory.class);  
    if (cicadaBeanFactories.iterator().hasNext()){  
        return cicadaBeanFactories.iterator().next() ;  
    }  
    return new CicadaDefaultBean();  
}

代碼也非常的簡(jiǎn)潔,和剛才提到的 InetAddressResolverProvider 一樣我們需要新增一個(gè) META-INF/services/top.crossoverjie.cicada.base.bean.CicadaBeanFactory 文件來(lái)配置我們的類名稱。

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 類中會(huì)會(huì)去查找 META-INF/services 的文件,然后解析其中的內(nèi)容從而反射生成對(duì)應(yīng)的接口對(duì)象。

這里還有一個(gè)關(guān)鍵是通常我們的代碼都會(huì)打包為一個(gè) JAR 包,類加載器需要加載這個(gè) JAR 包,同時(shí)需要在這個(gè) JAR 包里找到我們之前定義的那個(gè) spi 文件,如果這里查不到文件那就認(rèn)為沒(méi)有定義 SPI。

這個(gè)是本次問(wèn)題的重點(diǎn),會(huì)在后文分析原因的時(shí)候用到。

排查

因?yàn)閱?wèn)題就出現(xiàn)在是否使用 opentelemetry-javaagent.jar 上,所以我需要知道在使用了 agent 之后有什么區(qū)別。

從剛才的對(duì) SPI 的原理分析,加上 agent 出現(xiàn)異常,說(shuō)明理論上就是沒(méi)有讀取到我們配置的文件: java.net.spi.InetAddressResolverProvider 。

于是我便開始 debug,在 ServiceLoader 加載 jar 包的時(shí)候是可以看到具體使用的是什么 classLoader 。

這是不配置 agent 的時(shí)候使用的 classLoader:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
使用這個(gè) loader 是可以通過(guò)文件路徑在 jar 包中查找到我們配置的文件。

而配置上 agent 之后使用的 classLoader:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
卻是一個(gè) JarLoader,這樣是無(wú)法加載到在 springboot 格式下的配置文件的,至于為什么加載不到,那就要提一下 maven 打包后的文件目錄和 spring boot 打包后的文件目錄的區(qū)別了。

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
這里我截圖了同樣的一份代碼不同的打包方式:
上面的是傳統(tǒng) maven,下圖是 spring boot;其實(shí)主要的區(qū)別就是在 pom 中使用了一個(gè)構(gòu)建插件:

  
    
     
    org.springframework.boot  
    spring-boot-maven-plugin  
     
    

或者使用 spring-boot 命令再次打包的效果也是一樣的。

會(huì)發(fā)現(xiàn) spring boot 打包后會(huì)多出一層 BOOT-INF 的文件夾,然后會(huì)在 MANIFIST.MF 文件中定義 Main-Class Start-Class .


通過(guò)上面的 debug 其實(shí)會(huì)發(fā)現(xiàn) JarLoader 只能在加載 maven 打包后的文件,也就是說(shuō)無(wú)法識(shí)別 BOOT-INF 這個(gè)目錄。

正常情況下 spring boot 中會(huì)有一個(gè)額外的 java.nio.file.spi.FileSystemProvider 實(shí)現(xiàn):
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
通過(guò)這個(gè)類的實(shí)現(xiàn)可以直接從 JAR 包中加載資源,比如我們自定義的 SPI 資源等。

初步判斷使用 opentelemetry-javaagent.jar 的 agent 之后,它的類加載器優(yōu)先于了 spring boot ,從而導(dǎo)致后續(xù)的加載失敗。

遠(yuǎn)程 debug

這里穿插幾個(gè) debug 小技巧,其中一個(gè)是遠(yuǎn)程 debug,因?yàn)檫@里我是需要調(diào)試 javaagent,正常情況下是無(wú)法直接 debug 的。

所以我們可以使用以下命令啟動(dòng)應(yīng)用:

java -agentlib:jdwp="transport=dt_socket,server=y,suspend=y,address=5000" -javaagent:opentelemetry-javaagent.jar \
      -jar target/demo-0.0.1-SNAPSHOT.jar

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

然后在 idea 中配置一個(gè) remote 啟動(dòng)。

注意這里的端口得和命令行中的保持一致。

當(dāng)應(yīng)用啟動(dòng)之后便可以在 idea 中啟動(dòng)這個(gè) remote 了,這樣便可以正常 debug 了。

條件斷點(diǎn)

第二個(gè)是條件斷點(diǎn)也非常有用,有時(shí)候我們需要調(diào)試一個(gè)公共函數(shù),調(diào)用的地方非常多。

而我們只需要關(guān)心某一類行為的調(diào)用,此時(shí)就可以對(duì)這個(gè)函數(shù)中的變量進(jìn)行判斷,當(dāng)他們滿足某些條件時(shí)再進(jìn)入斷點(diǎn),這樣可以極大的提高我們的調(diào)試效率:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

配置也很簡(jiǎn)單,只需要在斷點(diǎn)上右鍵就可以編輯條件了。

社區(qū)咨詢

雖然我根據(jù)現(xiàn)象初步可以猜測(cè)下原因,但依然不確定如何調(diào)整才能解決這個(gè)問(wèn)題,于是便去社區(qū)提了一個(gè) issue 。

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
最后在社區(qū)大佬的幫助下發(fā)現(xiàn)我們需要禁用掉 OpenTelemetry agent 中的一個(gè) resource 就可以了。

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
這個(gè) resource 是由 agent 觸發(fā)的,它優(yōu)先于 spring boot 之前進(jìn)行 SPI 的加載。
目的是為了給 metric 和 trace 新增兩個(gè)屬性:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例
加載的核心代碼在這里,只要禁用掉之后就不會(huì)再加載了。

禁用前:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

禁用后:
OpenTelemetry agent 對(duì) Spring Boot 應(yīng)用的影響:一次 SPI 失效的案例

當(dāng)我們禁用掉之后就不會(huì)存在這兩個(gè)屬性了,不過(guò)我們目前并沒(méi)有使用這兩個(gè)屬性,所以為了使得 SPI 生效就只有先禁用掉了,后續(xù)再看看社區(qū)還有沒(méi)有其他的方案。

想要復(fù)現(xiàn) debug 的可以在這里嘗試:
https://github.com/crossoverJie/demo

參考連接:

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

RPG Ri序章 0.2.1
RPG Ri序章 0.2.1
類型:角色扮演  運(yùn)營(yíng)狀態(tài):正式運(yùn)營(yíng)  語(yǔ)言: 日文  

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

《RPG_Ri序章》是GameMaker'Child-Dream'制作的一款幻想廢土風(fēng)RPG手游,完全免費(fèi)的幻想廢土風(fēng)RPG登場(chǎng)!元

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)