SpringBoot从2.3.12升级到2.7.0时KeyVault配置启动报错
问题背景
现有SpringBoot应用原运行版本为2.3.12.RELEASE,因该版本存在安全漏洞需完成版本升级。实际升级至2.7.0、2.5.12、2.5.13等版本时,应用启动直接失败,核心报错为KeyVault属性源配置失败,完整错误栈如下:
java.lang.IllegalStateException: Failed to configure KeyVault property source at com.microsoft.azure.keyvault.spring.KeyVaultEnvironmentPostProcessorHelper.addKeyVaultPropertySource(KeyVaultEnvironmentPostProcessorHelper.java:110) at com.microsoft.azure.keyvault.spring.KeyVaultEnvironmentPostProcessor.postProcessEnvironment(KeyVaultEnvironmentPostProcessor.java:47) at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEnvironmentPreparedEvent(EnvironmentPostProcessorApplicationListener.java:102) at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEvent(EnvironmentPostProcessorApplicationListener.java:87) at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:131) at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:85) at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:66) at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120) at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:114) at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:65) at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:344) at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) at com.honeywell.CaaS.GatewayManagerAPI.webapi.SpringBootMain.main(SpringBootMain.java:51) Caused by: java.lang.RuntimeException: Max retries 3 times exceeded. Error Details: class java.util.Collections$SingletonList cannot be cast to class java.lang.String (java.util.Collections$SingletonList and java.lang.String are in module java.base of loader 'bootstrap') at com.azure.core.http.policy.RetryPolicy.lambda$attemptAsync$1(RetryPolicy.java:127) at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:172) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:172) at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:192) at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:259) at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:258) at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:258) at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onError(FluxPeekFuseable.java:553) at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onError(FluxPeekFuseable.java:553) at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onError(FluxMapFuseable.java:340) at reactor.core.publisher.MonoCompletionStage.lambda$subscribe$0(MonoCompletionStage.java:77) at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859) at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837) at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1705) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183) Suppressed: java.lang.Exception: #block terminated with an error at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99) at reactor.core.publisher.Mono.block(Mono.java:1707) at com.azure.security.keyvault.secrets.SecretClient.getSecretWithResponse(SecretClient.java:171) at com.azure.security.keyvault.secrets.SecretClient.getSecret(SecretClient.java:150) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) at com.microsoft.azure.keyvault.spring.KeyVaultOperation.refreshProperties(KeyVaultOperation.java:139) at com.microsoft.azure.keyvault.spring.KeyVaultOperation.<init>(KeyVaultOperation.java:72) at com.microsoft.azure.keyvault.spring.KeyVaultEnvironmentPostProcessorHelper.addKeyVaultPropertySource(KeyVaultEnvironmentPostProcessorHelper.java:90) at com.microsoft.azure.keyvault.spring.KeyVaultEnvironmentPostProcessor.postProcessEnvironment(KeyVaultEnvironmentPostProcessor.java:47) at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEnvironmentPreparedEvent(EnvironmentPostProcessorApplicationListener.java:102) at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEvent(EnvironmentPostProcessorApplicationListener.java:87) at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:131) at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:85) at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:66) at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120) at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:114) at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:65) at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:344) at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) at com.honeywell.CaaS.GatewayManagerAPI.webapi.SpringBootMain.main(SpringBootMain.java:51) Caused by: java.lang.ClassCastException: class java.util.Collections$SingletonList cannot be cast to class java.lang.String (java.util.Collections$SingletonList and java.lang.String are in module java.base of loader 'bootstrap') at com.nimbusds.oauth2.sdk.util.URLUtils.serializeParameters(URLUtils.java:105) at com.microsoft.aad.msal4j.TokenRequestExecutor.createOauthHttpRequest(TokenRequestExecutor.java:51) at com.microsoft.aad.msal4j.TokenRequestExecutor.executeTokenRequest(TokenRequestExecutor.java:36) at com.microsoft.aad.msal4j.AbstractClientApplicationBase.acquireTokenCommon(AbstractClientApplicationBase.java:117) at com.microsoft.aad.msal4j.AcquireTokenByAuthorizationGrantSupplier.execute(AcquireTokenByAuthorizationGrantSupplier.java:63) at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:59) at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:17) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700) ... 6 common frames omitted
根因分析
报错的核心触发点是最底层的类型转换异常:Collections$SingletonList无法强转为String,触发位置在Nimbus OAuth2 SDK的URLUtils.serializeParameters方法,本质是依赖版本不兼容:
- SpringBoot 2.4及以上版本升级了内置依赖管理的
nimbus-oauth2-oidc-sdk版本,新版本SDK调整了URLUtils.serializeParameters的入参格式,将参数Map的value类型从String改为List<String> - 旧版本的Azure KeyVault Spring Starter依赖的
msal4j(微软AAD认证库)版本是按照老版本Nimbus SDK的API编写的,没有适配新的参数类型,在请求AAD获取KeyVault访问Token时触发类型强转错误,重试3次失败后就会抛出KeyVault配置失败的启动异常。
该问题和SpringBoot小版本无关,只要SpringBoot版本≥2.4,且使用的Azure KeyVault Starter、msal4j版本未做适配,就会稳定复现。
排查思路
- 优先提取错误栈最底层的根异常,不要被上层的KeyVault配置失败、重试超限等表象误导,先定位具体的代码报错点
- 查看应用依赖树,确认类路径下
nimbus-oauth2-oidc-sdk、msal4j、azure-keyvault-spring-boot-starter三个依赖的实际加载版本 - 核对当前使用的Azure KeyVault Starter官方兼容矩阵,确认其支持的SpringBoot版本、Nimbus版本范围,和当前升级后的SpringBoot依赖版本做比对
解决方案
- 方案一(推荐):升级Azure KeyVault依赖到适配版本。将旧的
com.microsoft.azuregroupId下的老版KeyVault Starter,替换为官方后续维护的com.azure.spring:spring-cloud-azure-starter-keyvault-secrets,选择和目标SpringBoot版本匹配的依赖版本即可,新版本已经完成Nimbus SDK的API适配,不需要额外调整业务配置 - 方案二:如果暂时无法替换KeyVault Starter,可在项目依赖管理中强制将
msal4j版本升级到1.11.0及以上,该版本及以后已经适配了高版本Nimbus SDK的参数格式,改动量最小 - 方案三(不推荐):在依赖管理中强制将
nimbus-oauth2-oidc-sdk版本降级到SpringBoot 2.3.x默认携带的8.x版本。注意:该操作可能导致Spring Security OAuth2、Spring Cloud等依赖高版本Nimbus的组件出现兼容问题,非极端场景不要使用
内容的提问来源于stack exchange,提问作者Sugato
相关产品推荐
相关产品推荐

