背景 前段時間公司領(lǐng)導(dǎo)讓我排查一個關(guān)于在 JDK21 環(huán)境中使用 Spring Boot 配合一個 JDK18 新增的一個 SPI(java.net.spi.InetAddressResolverProvider) 不生效的問題。 但這個不生效的前置條件有點(diǎn)多: JDK 的版本得在 18+ Spri
前段時間公司領(lǐng)導(dǎo)讓我排查一個關(guān)于在 JDK21 環(huán)境中使用 Spring Boot 配合一個 JDK18 新增的一個 SPI(
java.net.spi.InetAddressResolverProvider
) 不生效的問題。
但這個不生效的前置條件有點(diǎn)多:
-javaagent:opentelemetry-javaagent.jar
使用,也就是 OpenTelemetry 提供的 agent。
才會導(dǎo)致自定義的
InetAddressResolverProvider
無法正常工作。
在復(fù)現(xiàn)這個問題之前先簡單介紹下
java.net.spi.InetAddressResolverProvider
這個 SPI;它是在 JDK18 之后才提供的,在這之前我們使用
InetAddress
的內(nèi)置解析器來解析主機(jī)名和 IP 地址,但這個解析器之前是不可以自定義的。
在某些場景下會不太方便,比如我們需要請求
order.service
這個域名時希望可以請求到某一個具體 IP 地址上,我們可以自己配置 host ,或者使用服務(wù)發(fā)現(xiàn)機(jī)制來實(shí)現(xiàn)。
但現(xiàn)在通過
InetAddressResolverProvider
就可以定義在請求這個域名的時候返回一個我們預(yù)期的 IP 地址。
同時由于它是一個 SPI,所以我們只需要編寫一個第三方包,任何項(xiàng)目依賴它之后在發(fā)起網(wǎng)絡(luò)請求時都會按照我們預(yù)期的 IP 進(jìn)行請求。
要使用它也很簡單,主要是兩個類:
InetAddressResolverProvider
:這是一個抽象類,我們可以繼承它之后重寫它的 get 函數(shù)返回一個
InetAddressResolver
對象
InetAddressResolver
:一個接口,主要提供了兩個函數(shù);一個用于傳入域名返回 IP 地址,另一個反之:傳入 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
這里我簡單實(shí)現(xiàn)了一個對域名 fedora 的解析,會直接返回
127.127.10.1
。
如果使用 IP 地址進(jìn)行查詢時:
InetAddress byAddress = InetAddress.getByAddress(new byte[]{127, 127, 10, 1});
System.out.println("+++++" + byAddress.getHostName());
// output: fedora
當(dāng)然要要使得這個 SPI 生效的前提條件是我們需要新建一個文件:
META-INF/services/java.net.spi.InetAddressResolverProvider
里面的內(nèi)容是我們自定義類的全限定名稱:
com.example.demo.MyAddressResolverProvider
這樣一個完整的 SPI 就實(shí)現(xiàn)完成了。
正常情況下我們將應(yīng)用打包為一個 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í)也沒啥問題,也能按照預(yù)期輸出結(jié)果。
但我們加上 OpenTelemetry 的 agent 時:
java -javaagent:opentelemetry-javaagent.jar \
-jar target/demo-0.0.1-SNAPSHOT.jar
就會發(fā)現(xiàn)在執(zhí)行解析的時候拋出了
java.net.UnknownHostException
異常。
從結(jié)果來看就是沒有進(jìn)入我們自定義的解析器。
在講排查過程之前還是要先預(yù)習(xí)下關(guān)于 Java SPI 的原理以及應(yīng)用場景。
以前寫過一個 http 框架 cicada ,其中有一個可拔插 IOC 容器的功能:
就是可以自定義實(shí)現(xiàn)自己的 IOC 容器,將自己實(shí)現(xiàn)的 IOC 容器打包為一個第三方包加入到依賴中,cicada 框架就會自動使用自定義的 IOC 實(shí)現(xiàn)。
要實(shí)現(xiàn)這個功能本質(zhì)上就是要定義一個接口,然后根據(jù)依賴的不同實(shí)現(xiàn)創(chuàng)建接口的實(shí)例對象。
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() ;
}
獲取具體的示例代碼時就只需要使用 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();
}
代碼也非常的簡潔,和剛才提到的
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
的文件,然后解析其中的內(nèi)容從而反射生成對應(yīng)的接口對象。
這里還有一個關(guān)鍵是通常我們的代碼都會打包為一個 JAR 包,類加載器需要加載這個 JAR 包,同時需要在這個 JAR 包里找到我們之前定義的那個 spi 文件,如果這里查不到文件那就認(rèn)為沒有定義 SPI。
這個是本次問題的重點(diǎn),會在后文分析原因的時候用到。
因?yàn)閱栴}就出現(xiàn)在是否使用 opentelemetry-javaagent.jar 上,所以我需要知道在使用了 agent 之后有什么區(qū)別。
從剛才的對 SPI 的原理分析,加上 agent 出現(xiàn)異常,說明理論上就是沒有讀取到我們配置的文件:
java.net.spi.InetAddressResolverProvider
。
于是我便開始 debug,在 ServiceLoader 加載 jar 包的時候是可以看到具體使用的是什么
classLoader
。
這是不配置 agent 的時候使用的 classLoader:
使用這個 loader 是可以通過文件路徑在 jar 包中查找到我們配置的文件。
而配置上 agent 之后使用的 classLoader:
卻是一個 JarLoader,這樣是無法加載到在 springboot 格式下的配置文件的,至于為什么加載不到,那就要提一下 maven 打包后的文件目錄和 spring boot 打包后的文件目錄的區(qū)別了。
這里我截圖了同樣的一份代碼不同的打包方式:
上面的是傳統(tǒng) maven,下圖是 spring boot;其實(shí)主要的區(qū)別就是在 pom 中使用了一個構(gòu)建插件:
org.springframework.boot
spring-boot-maven-plugin
或者使用
spring-boot
命令再次打包的效果也是一樣的。
會發(fā)現(xiàn) spring boot 打包后會多出一層
BOOT-INF
的文件夾,然后會在
MANIFIST.MF
文件中定義
Main-Class
和
Start-Class
.
通過上面的 debug 其實(shí)會發(fā)現(xiàn) JarLoader 只能在加載 maven 打包后的文件,也就是說無法識別 BOOT-INF 這個目錄。
正常情況下 spring boot 中會有一個額外的
java.nio.file.spi.FileSystemProvider
實(shí)現(xiàn):
通過這個類的實(shí)現(xiàn)可以直接從 JAR 包中加載資源,比如我們自定義的 SPI 資源等。
初步判斷使用
opentelemetry-javaagent.jar
的 agent 之后,它的類加載器優(yōu)先于了 spring boot ,從而導(dǎo)致后續(xù)的加載失敗。
這里穿插幾個 debug 小技巧,其中一個是遠(yuǎn)程 debug,因?yàn)檫@里我是需要調(diào)試 javaagent,正常情況下是無法直接 debug 的。
所以我們可以使用以下命令啟動應(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
然后在 idea 中配置一個 remote 啟動。
注意這里的端口得和命令行中的保持一致。
當(dāng)應(yīng)用啟動之后便可以在 idea 中啟動這個 remote 了,這樣便可以正常 debug 了。
第二個是條件斷點(diǎn)也非常有用,有時候我們需要調(diào)試一個公共函數(shù),調(diào)用的地方非常多。
而我們只需要關(guān)心某一類行為的調(diào)用,此時就可以對這個函數(shù)中的變量進(jìn)行判斷,當(dāng)他們滿足某些條件時再進(jìn)入斷點(diǎn),這樣可以極大的提高我們的調(diào)試效率:
配置也很簡單,只需要在斷點(diǎn)上右鍵就可以編輯條件了。
雖然我根據(jù)現(xiàn)象初步可以猜測下原因,但依然不確定如何調(diào)整才能解決這個問題,于是便去社區(qū)提了一個 issue 。
最后在社區(qū)大佬的幫助下發(fā)現(xiàn)我們需要禁用掉 OpenTelemetry agent 中的一個 resource 就可以了。
這個 resource 是由 agent 觸發(fā)的,它優(yōu)先于 spring boot 之前進(jìn)行 SPI 的加載。
目的是為了給 metric 和 trace 新增兩個屬性:
加載的核心代碼在這里,只要禁用掉之后就不會再加載了。
禁用前:
禁用后:
當(dāng)我們禁用掉之后就不會存在這兩個屬性了,不過我們目前并沒有使用這兩個屬性,所以為了使得 SPI 生效就只有先禁用掉了,后續(xù)再看看社區(qū)還有沒有其他的方案。
想要復(fù)現(xiàn) debug 的可以在這里嘗試:
https://github.com/crossoverJie/demo
參考連接:
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實(shí)現(xiàn)對象集合與DataTable的相互轉(zhuǎn)換
閱讀鴻蒙NEXT元服務(wù):論如何免費(fèi)快速上架作品
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動態(tài)代理的對比分析
閱讀Win11筆記本“自動管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)