OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的
背景
前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。
但这个不生效的前置条件有点多:
- JDK 的版本得在 18+
- SpringBoot3.x
- 还在额外再配合使用
-javaagent:opentelemetry-javaagent.jar使用,也就是 OpenTelemetry 提供的 agent。
才会导致自定义的 InetAddressResolverProvider 无法正常工作。
在复现这个问题之前先简单介绍下 java.net.spi.InetAddressResolverProvider 这个 SPI;它是在 JDK18 之后才提供的,在这之前我们使用 InetAddress 的内置解析器来解析主机名和 IP 地址,但这个解析器之前是不可以自定义的。
在某些场景下会不太方便,比如我们需要请求 order.service 这个域名时希望可以请求到某一个具体 IP 地址上,我们可以自己配置 host ,或者使用服务发现机制来实现。
但现在通过 InetAddressResolverProvider 就可以定义在请求这个域名的时候返回一个我们预期的 IP 地址。
同时由于它是一个 SPI,所以我们只需要编写一个第三方包,任何项目依赖它之后在发起网络请求时都会按照我们预期的 IP 进行请求。
复现
要使用它也很简单,主要是两个类:
InetAddressResolverProvider:这是一个抽象类,我们可以继承它之后重写它的 get 函数返回一个InetAddressResolver对象InetAddressResolver:一个接口,主要提供了两个函数;一个用于传入域名返回 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<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";
}
}
---
```java
addresses = 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异常。

从结果来看就是没有进入我们自定义的解析器。
SPI 原理
在讲排查过程之前还是要先预习下关于 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 小技巧,其中一个是远程 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
参考连接:
- https://github.com/TogetherOS/cicada
- https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/#packaging.repackage-goal
- https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10921
- https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/resources/library/README.md#host
OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的的更多相关文章
- Spring/Spring boot正确集成Quartz及解决@Autowired失效问题
周五检查以前Spring boot集成Quartz项目的时候,发现配置错误,因此通过阅读源码的方式,探索Spring正确集成Quartz的方式. 问题发现 检查去年的项目代码,发现关于QuartzJo ...
- Spring Boot 16 条最佳实践
Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...
- Spring Boot 最流行的 16 条实践解读,你值得收藏!
Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...
- Spring Boot 最流行的 16 条实践解读!【华为云技术分享】
置顶:华为云618大促火热进行中,全场1折起,免费抽主机,消费满额送P30 Pro,点此抢购. Spring Boot是最流行的用于开发微服务的Java框架.在本文中,将与大家分享自2016年以来笔者 ...
- Spring Boot 最流行的 16 条实践解读!
Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...
- Spring Boot2 系列教程(十八)Spring Boot 中自定义 SpringMVC 配置
用过 Spring Boot 的小伙伴都知道,我们只需要在项目中引入 spring-boot-starter-web 依赖,SpringMVC 的一整套东西就会自动给我们配置好,但是,真实的项目环境比 ...
- Spring Boot 中自定义 SpringMVC 配置,到底继承谁哪一个类或则接口?
看了这篇文章,写的非常的言简意赅,特此记录下: 1.Spring Boot 1.x 中,自定义 SpringMVC 配置可以通过继承 WebMvcConfigurerAdapter 来实现. 2.Sp ...
- 深入Spring Boot:ClassLoader的继承关系和影响
前言 对spring boot本身启动原理的分析, Spring boot里的ClassLoader继承关系 可以运行下面提供的demo,分别在不同的场景下运行,可以知道不同场景下的Spring bo ...
- Spring Boot 2.4版本前后的分组配置变化及对多环境配置结构的影响
前几天在<Spring Boot 2.4 对多环境配置的支持更改>一文中,给大家讲解了Spring Boot 2.4版本对多环境配置的配置变化.除此之外,还有一些其他配置变化,所以今天我们 ...
- Spring Boot
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.通过 ...
随机推荐
- 新零售SaaS架构:客户管理系统架构设计(万字图文总结)
什么是客户管理系统? 客户管理系统,也称为CRM(Customer Relationship Management),主要目标是建立.发展和维护好客户关系. CRM系统围绕客户全生命周期的管理,吸引和 ...
- Git 教程:解密 .gitignore 文件、合并分支、解决冲突、及 Git 帮助
Git 帮助 如果你忘记了命令或命令的选项,你可以使用 Git 帮助. 在命令行中,有几种不同的使用帮助命令的方式: git command -help - 查看特定命令的所有可用选项 git hel ...
- 如何通过 Makefile 优化加速编译过程提高开发效率
在软件开发中,编译是一个必不可少的过程.但是,当代码规模变得越来越大时,编译时间也会变得越来越长,这会严重影响开发效率.在这种情况下,优化Makefile可以帮助我们加速编译过程,以下是一些Makef ...
- 【FAQ】HarmonyOS SDK 闭源开放能力 —IAP Kit
1.问题描述 根据https://developer.huawei.com/consumer/cn/doc/harmonyos-references/iap-data-model-0000001736 ...
- 文档在线预览(一)通过将txt、word、pdf、ppt转成图片实现在线预览功能
@ 目录 一.前言 1.aspose 2 .poi + pdfbox 3 spire 二.将文件转换成图片,并生成到本地 1.将word文件转成图片 (1)使用aspose (2)使用pdfbox ( ...
- 优先队列的基本实现【数据结构与算法—TypeScript 实现】
笔记整理自 coderwhy 『TypeScript 高阶数据结构与算法』课程 特性 效率比普通队列高 每个出队元素拥有最高优先级 可以用 数组.链表 等数据结构实现,但是 堆结构 是最常用的实现方式 ...
- HarmonyOS跨进程通信—IPC与RPC通信开发指导
一.IPC与RPC通信概述 基本概念 IPC(Inter-Process Communication)与RPC(Remote Procedure Call)用于实现跨进程通信,不同的是前者使用Bi ...
- HarmonyOS应用侧与前端页面数据通道建立
一. 应用侧调用前端页面函数 应用侧可以通过runJavaScript()方法调用前端页面的JavaScript相关函数.在下面的示例中,点击应用侧的"runJavaScript" ...
- 详解K8s 镜像缓存管理kube-fledged
本文分享自华为云社区<K8s 镜像缓存管理 kube-fledged 认知>,作者: 山河已无恙. 我们知道 k8s 上的容器调度需要在调度的节点行拉取当前容器的镜像,在一些特殊场景中, ...
- 重走py 之路 ——普通操作与函数(三)
前言 本节主要介绍函数,但是函数是由操作组成的.那么就分为两部,一部分为操作一部分为函数. 正文 py世界中的操作. 操作 if: 在学习任何一门语言中,关系if,要关系另外一件事,那就是if是否只能 ...