From 39027671c3052470feae1da75a0ebaf7c9403537 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 15:31:32 +0800 Subject: [PATCH 1/8] Fix Event Hubs dedicated builder polluting shared section injection (#49245) The root EventHubClientBuilder used a type-based @ConditionalOnMissingBean, so when either DedicatedConsumerConnectionConfiguration or DedicatedProducerConnectionConfiguration activated, its named-but-EventHubClientBuilder-typed bean suppressed the root builder. The opposite section's SharedConsumer/SharedProducerConnectionConfiguration then satisfied its @ConditionalOnBean(EventHubClientBuilder.class) against the dedicated bean and injected it via type, so the shared client targeted the wrong event hub. - Name the root builder springCloudAzureEventHubsClientBuilder and use @ConditionalOnMissingBean(name = ...) so dedicated builders no longer suppress it. - Switch SharedConsumer/SharedProducerConnectionConfiguration to @ConditionalOnBean(name = ...) and inject EventHubClientBuilder via @Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME). - Add regression tests for asymmetric (one-side dedicated, one-side shared) configurations in both consumer and producer test classes. --- sdk/spring/CHANGELOG.md | 1 + .../context/AzureContextUtils.java | 6 +++ ...reEventHubsClientBuilderConfiguration.java | 5 +- ...eEventHubsConsumerClientConfiguration.java | 8 ++-- ...eEventHubsProducerClientConfiguration.java | 7 +-- ...tHubsConsumerClientConfigurationTests.java | 47 ++++++++++++++++++- ...tHubsProducerClientConfigurationTests.java | 47 ++++++++++++++++++- 7 files changed, 111 insertions(+), 10 deletions(-) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 7c0d1465cdf8..6a3f7f6aa6a1 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -13,6 +13,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. #### Bugs Fixed +- Fixed Event Hubs autoconfiguration where a dedicated `EventHubClientBuilder` registered by `consumer`-only or `producer`-only sub-level overrides (`connection-string` / `namespace` / `event-hub-name`) suppressed the root builder and got injected into the opposite shared section, causing the shared client to target the other section's event hub. The root builder is now registered under bean name `springCloudAzureEventHubsClientBuilder` with a name-based `@ConditionalOnMissingBean`, and the shared consumer/producer sections gate on and inject that specific bean via `@Qualifier`. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) - Fixed JDBC/Azure Database and Redis passwordless connection scope defaulting using the wrong `azure.scopes` value for Azure China and Azure US Government when `spring.cloud.azure.profile.cloud-type` is set to `azure_china` or `azure_us_government`. The scopes are now correctly derived from the merged cloud type. ([#47096](https://github.com/Azure/azure-sdk-for-java/issues/47096)) ### Spring Cloud Azure Stream Binder Service Bus diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java index 381b8cf6de90..11f68e8ddb93 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java @@ -34,6 +34,12 @@ private AzureContextUtils() { public static final String EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME = "springCloudAzureEventHubsClientBuilderFactory"; + /** + * Event Hubs client builder bean name. + */ + public static final String EVENT_HUB_CLIENT_BUILDER_BEAN_NAME = + "springCloudAzureEventHubsClientBuilder"; + /** * Event Hubs consumer client builder factory bean name. */ diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java index 43f98426b7ea..b4cad7e2347b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME; /** @@ -39,8 +40,8 @@ class AzureEventHubsClientBuilderConfiguration { this.eventHubsProperties = eventHubsProperties; } - @Bean - @ConditionalOnMissingBean + @Bean(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) + @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory factory) { return factory.build(); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java index db612454ac19..6009bc13ef14 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_FACTORY_BEAN_NAME; @@ -43,12 +44,13 @@ class AzureEventHubsConsumerClientConfiguration { @ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.consumer", name = { "connection-string", "namespace", "event-hub-name" }) - @ConditionalOnBean(EventHubClientBuilder.class) + @ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @Configuration(proxyBeanMethods = false) static class SharedConsumerConnectionConfiguration { private final EventHubClientBuilder builder; - SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties, EventHubClientBuilder builder) { + SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties, + @Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { this.builder = builder; PropertyMapper mapper = PropertyMapper.get(); @@ -64,7 +66,7 @@ EventHubConsumerAsyncClient eventHubConsumerAsyncClient() { @Bean @ConditionalOnMissingBean - EventHubConsumerClient eventHubConsumerClient(EventHubClientBuilder builder) { + EventHubConsumerClient eventHubConsumerClient() { return this.builder.buildConsumerClient(); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java index 821a7f572502..c23e9eb14ed4 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_FACTORY_BEAN_NAME; @@ -35,18 +36,18 @@ class AzureEventHubsProducerClientConfiguration { @ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.producer", name = { "connection-string", "namespace", "event-hub-name" }) - @ConditionalOnBean(EventHubClientBuilder.class) + @ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @Configuration(proxyBeanMethods = false) static class SharedProducerConnectionConfiguration { @Bean @ConditionalOnMissingBean - EventHubProducerAsyncClient eventHubProducerAsyncClient(EventHubClientBuilder builder) { + EventHubProducerAsyncClient eventHubProducerAsyncClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { return builder.buildAsyncProducerClient(); } @Bean @ConditionalOnMissingBean - EventHubProducerClient eventHubProducerClient(EventHubClientBuilder builder) { + EventHubProducerClient eventHubProducerClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) { return builder.buildProducerClient(); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java index d3ac5255fd62..d4d52c8fd570 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java @@ -60,7 +60,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { "spring.cloud.azure.eventhubs.consumer.consumer-group=" + consumerGroupName ) .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) - .withBean(EventHubClientBuilder.class, () -> clientBuilder) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder) .run( context -> { assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); @@ -71,6 +71,51 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } + @Test + void producerOnlyDedicatedOverrideShouldNotActivateSharedConsumer() { + // Regression for issue #49245: producer dedicated builder must not satisfy the shared consumer condition, + // nor be injected into the shared consumer path. Without sub-level overrides on the consumer side and + // without the global event-hub-name, the consumer config simply should not activate. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.producer.event-hub-name=override-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.class); + assertThat(context).doesNotHaveBean(EventHubConsumerClient.class); + assertThat(context).doesNotHaveBean(EventHubConsumerAsyncClient.class); + } + ); + } + + @Test + void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() { + // Regression for issue #49245: when both a global event-hub-name and a producer-only override exist, + // the shared consumer should still bind to the root builder, not the producer's dedicated builder. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.event-hub-name=base-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group", + "spring.cloud.azure.eventhubs.producer.event-hub-name=override-eventhub" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.SharedConsumerConnectionConfiguration.class); + assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + assertThat(context).hasSingleBean(EventHubConsumerClient.class); + } + ); + } + @Test void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() { contextRunner diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java index b7d6b30f8411..9ca37704d36c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java @@ -58,7 +58,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { "spring.cloud.azure.eventhubs.event-hub-name=" + eventHubName ) .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) - .withBean(EventHubClientBuilder.class, () -> clientBuilder) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder) .run( context -> { assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); @@ -69,6 +69,51 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } + @Test + void consumerOnlyDedicatedOverrideShouldNotActivateSharedProducer() { + // Regression for issue #49245: a consumer-dedicated builder must not satisfy the shared producer condition. + // Without sub-level overrides on the producer side and without a global event-hub-name, the producer + // config simply should not activate. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.consumer.event-hub-name=override-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.class); + assertThat(context).doesNotHaveBean(EventHubProducerClient.class); + assertThat(context).doesNotHaveBean(EventHubProducerAsyncClient.class); + } + ); + } + + @Test + void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() { + // Regression for issue #49245: when both a global event-hub-name and a consumer-only override exist, + // the shared producer should still bind to the root builder, not the consumer's dedicated builder. + contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.namespace=test-namespace", + "spring.cloud.azure.eventhubs.event-hub-name=base-eventhub", + "spring.cloud.azure.eventhubs.consumer.event-hub-name=override-eventhub", + "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withUserConfiguration(AzureEventHubsAutoConfiguration.class) + .run( + context -> { + assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.SharedProducerConnectionConfiguration.class); + assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + assertThat(context).hasSingleBean(EventHubProducerClient.class); + } + ); + } + @Test void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() { contextRunner From a85e947c2bbbe27cfbf07112559ecbc9545f0bc2 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 15:45:45 +0800 Subject: [PATCH 2/8] Address PR review: name-based factory condition and stronger asymmetric regression assertions - Make root EventHubClientBuilderFactory @ConditionalOnMissingBean name-based, consistent with the dedicated factory beans, so a user-provided differently-named factory does not suppress the named one. - Strengthen the four asymmetric regression tests to assert the opposite-side DedicatedXxxConnectionConfiguration is actually activated, and assert multiple EventHubClientBuilder beans coexist in the shared-injection cases, so the tests truly exercise the failure mode the fix targets. --- .../AzureEventHubsClientBuilderConfiguration.java | 2 +- .../AzureEventHubsConsumerClientConfigurationTests.java | 9 +++++++++ .../AzureEventHubsProducerClientConfigurationTests.java | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java index b4cad7e2347b..073e53744e04 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java @@ -48,7 +48,7 @@ EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_ } @Bean(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) - @ConditionalOnMissingBean + @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory eventHubClientBuilderFactory( ObjectProvider> connectionStringProviders, ObjectProvider> customizers) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java index d4d52c8fd570..d14c01be5fcb 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java @@ -89,6 +89,10 @@ void producerOnlyDedicatedOverrideShouldNotActivateSharedConsumer() { assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.class); assertThat(context).doesNotHaveBean(EventHubConsumerClient.class); assertThat(context).doesNotHaveBean(EventHubConsumerAsyncClient.class); + // The producer dedicated path must actually be activated for this test to exercise the + // "asymmetric, dedicated builder bean exists" scenario the bug fix targets. + assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME); } ); } @@ -110,6 +114,11 @@ void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() { context -> { assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.SharedConsumerConnectionConfiguration.class); assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + // Producer dedicated must be active so multiple EventHubClientBuilder beans coexist, + // proving the shared consumer is selecting the root builder by qualifier rather than + // succeeding by accident because only one builder bean exists. + assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); assertThat(context).hasSingleBean(EventHubConsumerClient.class); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java index 9ca37704d36c..3aaada10df7d 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java @@ -87,6 +87,10 @@ void consumerOnlyDedicatedOverrideShouldNotActivateSharedProducer() { assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.class); assertThat(context).doesNotHaveBean(EventHubProducerClient.class); assertThat(context).doesNotHaveBean(EventHubProducerAsyncClient.class); + // The consumer dedicated path must actually be activated for this test to exercise the + // "asymmetric, dedicated builder bean exists" scenario the bug fix targets. + assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME); } ); } @@ -108,6 +112,11 @@ void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() { context -> { assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.SharedProducerConnectionConfiguration.class); assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); + // Consumer dedicated must be active so multiple EventHubClientBuilder beans coexist, + // proving the shared producer is selecting the root builder by qualifier rather than + // succeeding by accident because only one builder bean exists. + assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); + assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); assertThat(context).hasSingleBean(EventHubProducerClient.class); } From 866f79db8ad15acccaaf4b47aa1e78cc14b0abf0 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 16:08:04 +0800 Subject: [PATCH 3/8] Preserve type-based user-override for root EventHubClientBuilder The previous name-based @ConditionalOnMissingBean broke backward compatibility: a user-defined EventHubClientBuilder bean registered under a custom name no longer suppressed the auto-configured root builder. Add a custom condition that ignores only the three reserved auto-config bean names (root, consumer-dedicated, producer-dedicated) so any other EventHubClientBuilder bean is treated as a user override and suppresses auto-configuration, while still allowing the dedicated builders to coexist with the root one. --- ...reEventHubsClientBuilderConfiguration.java | 2 + ...DefinedEventHubClientBuilderCondition.java | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java index 073e53744e04..79e7d17ec429 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -42,6 +43,7 @@ class AzureEventHubsClientBuilderConfiguration { @Bean(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) + @Conditional(OnMissingUserDefinedEventHubClientBuilderCondition.class) EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory factory) { return factory.build(); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java new file mode 100644 index 000000000000..3d29d1efa521 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.eventhubs; + +import com.azure.messaging.eventhubs.EventHubClientBuilder; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import java.util.Set; + +/** + * Matches when no user-defined {@link EventHubClientBuilder} bean is present in the context. + * + *

Beans registered under the well-known auto-configuration bean names + * ({@link AzureContextUtils#EVENT_HUB_CLIENT_BUILDER_BEAN_NAME}, + * {@link AzureContextUtils#EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME}, + * {@link AzureContextUtils#EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME}) are not considered + * user-defined: the first is the root builder itself; the latter two are dedicated builders + * created by the consumer/producer auto-configurations and must not suppress the root builder. + */ +class OnMissingUserDefinedEventHubClientBuilderCondition extends SpringBootCondition implements ConfigurationCondition { + + private static final Set RESERVED_BUILDER_BEAN_NAMES = Set.of( + AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, + AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME, + AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME + ); + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String[] beanNames = context.getBeanFactory().getBeanNamesForType(EventHubClientBuilder.class, true, false); + for (String name : beanNames) { + if (!RESERVED_BUILDER_BEAN_NAMES.contains(name)) { + return ConditionOutcome.noMatch("found user-defined EventHubClientBuilder bean: " + name); + } + } + return ConditionOutcome.match("no user-defined EventHubClientBuilder bean found"); + } +} From f033e8ce022ef74c5a8d62bf10574863dd06189b Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 16:19:39 +0800 Subject: [PATCH 4/8] Include ancestor contexts in user-defined builder check Spring Boot's @ConditionalOnMissingBean defaults to SearchStrategy.ALL, which walks the entire bean factory hierarchy. Use BeanFactoryUtils.beanNamesForTypeIncludingAncestors so the custom condition matches that behavior and detects a user-defined EventHubClientBuilder bean registered in a parent ApplicationContext. --- .../OnMissingUserDefinedEventHubClientBuilderCondition.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java index 3d29d1efa521..8fe7fadfd286 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java @@ -5,6 +5,7 @@ import com.azure.messaging.eventhubs.EventHubClientBuilder; import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.context.annotation.ConditionContext; @@ -38,7 +39,8 @@ public ConfigurationPhase getConfigurationPhase() { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - String[] beanNames = context.getBeanFactory().getBeanNamesForType(EventHubClientBuilder.class, true, false); + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + context.getBeanFactory(), EventHubClientBuilder.class, true, false); for (String name : beanNames) { if (!RESERVED_BUILDER_BEAN_NAMES.contains(name)) { return ConditionOutcome.noMatch("found user-defined EventHubClientBuilder bean: " + name); From 22c894fe516dc3468a64dad66f0aec77f5999153 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 16:35:29 +0800 Subject: [PATCH 5/8] CHANGELOG: note Event Hubs shared section name-based builder wiring --- sdk/spring/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index b44f34861bea..893564fe4b48 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -11,6 +11,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. - AAD resource server now requires `spring.cloud.azure.active-directory.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted and will cause application startup to fail with an `IllegalArgumentException`. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - `AadAuthenticationFilter` now enables explicit audience validation by default. The filter will verify that the JWT's `aud` (audience) claim matches either `spring.cloud.azure.active-directory.credential.client-id` or `spring.cloud.azure.active-directory.app-id-uri`. Tokens issued for other applications will be rejected with `BadJWTException`. This prevents cross-application token reuse and aligns with OAuth2/OIDC security best practices. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - B2C resource server now requires `spring.cloud.azure.active-directory.b2c.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted. In addition, default token validation is hardened to enforce tenant-bound `tid`, stricter `aud` validation, and B2C-only trusted issuers. ([#49252](https://github.com/Azure/azure-sdk-for-java/pull/49252)) +- Event Hubs shared consumer/producer auto-configuration now wires the root `EventHubClientBuilder` by bean name (`springCloudAzureEventHubsClientBuilder`) instead of by type. Users who provide their own `EventHubClientBuilder` bean and want it to be injected into the auto-configured shared `EventHubConsumerClient`/`EventHubProducerClient` must register it under that bean name; a user-supplied builder under a different name will still suppress the auto-configured root builder, but will not be wired into the shared clients. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) #### Bugs Fixed From 7f619d535211cc1d92cfc4d41cc6bbc69700d4f3 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 16:44:53 +0800 Subject: [PATCH 6/8] Adopt purely name-based user-override contract for EventHubClientBuilder Remove the custom OnMissingUserDefinedEventHubClientBuilderCondition and rely solely on @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME). The override contract is now uniformly name-based: to replace the auto-configured root builder (and have shared EventHubConsumerClient/EventHubProducerClient pick up the override) the user must register their bean under springCloudAzureEventHubsClientBuilder. A user-supplied bean under any other name no longer suppresses the auto-configured root and is not wired into the shared clients. Update userDefinedEventHubsClientBuilderProvidedShouldNotAutoconfigure to register under the reserved name, add a new userDefinedEventHubsClientBuilderUnderCustomNameShouldNotSuppressAutoconfigure asserting the new coexistence behavior, and tighten the CHANGELOG breaking-change entry. --- sdk/spring/CHANGELOG.md | 2 +- ...reEventHubsClientBuilderConfiguration.java | 2 - ...DefinedEventHubClientBuilderCondition.java | 51 ------------------- ...ntHubsClientBuilderConfigurationTests.java | 19 ++++++- 4 files changed, 18 insertions(+), 56 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 893564fe4b48..740e7b1eba02 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -11,7 +11,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. - AAD resource server now requires `spring.cloud.azure.active-directory.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted and will cause application startup to fail with an `IllegalArgumentException`. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - `AadAuthenticationFilter` now enables explicit audience validation by default. The filter will verify that the JWT's `aud` (audience) claim matches either `spring.cloud.azure.active-directory.credential.client-id` or `spring.cloud.azure.active-directory.app-id-uri`. Tokens issued for other applications will be rejected with `BadJWTException`. This prevents cross-application token reuse and aligns with OAuth2/OIDC security best practices. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033)) - B2C resource server now requires `spring.cloud.azure.active-directory.b2c.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted. In addition, default token validation is hardened to enforce tenant-bound `tid`, stricter `aud` validation, and B2C-only trusted issuers. ([#49252](https://github.com/Azure/azure-sdk-for-java/pull/49252)) -- Event Hubs shared consumer/producer auto-configuration now wires the root `EventHubClientBuilder` by bean name (`springCloudAzureEventHubsClientBuilder`) instead of by type. Users who provide their own `EventHubClientBuilder` bean and want it to be injected into the auto-configured shared `EventHubConsumerClient`/`EventHubProducerClient` must register it under that bean name; a user-supplied builder under a different name will still suppress the auto-configured root builder, but will not be wired into the shared clients. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) +- Event Hubs auto-configuration now identifies the root `EventHubClientBuilder` by bean name (`springCloudAzureEventHubsClientBuilder`) instead of by type. To override the auto-configured root builder (and have shared `EventHubConsumerClient`/`EventHubProducerClient` use your bean), register the bean under the name `springCloudAzureEventHubsClientBuilder`. A user-supplied `EventHubClientBuilder` bean under a different name will no longer suppress the auto-configured root builder and will not be wired into the shared clients. ([#49245](https://github.com/Azure/azure-sdk-for-java/issues/49245)) #### Bugs Fixed diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java index 79e7d17ec429..073e53744e04 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java @@ -19,7 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -43,7 +42,6 @@ class AzureEventHubsClientBuilderConfiguration { @Bean(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) @ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) - @Conditional(OnMissingUserDefinedEventHubClientBuilderCondition.class) EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME) EventHubClientBuilderFactory factory) { return factory.build(); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java deleted file mode 100644 index 8fe7fadfd286..000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/OnMissingUserDefinedEventHubClientBuilderCondition.java +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.implementation.eventhubs; - -import com.azure.messaging.eventhubs.EventHubClientBuilder; -import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.ConfigurationCondition; -import org.springframework.core.type.AnnotatedTypeMetadata; - -import java.util.Set; - -/** - * Matches when no user-defined {@link EventHubClientBuilder} bean is present in the context. - * - *

Beans registered under the well-known auto-configuration bean names - * ({@link AzureContextUtils#EVENT_HUB_CLIENT_BUILDER_BEAN_NAME}, - * {@link AzureContextUtils#EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME}, - * {@link AzureContextUtils#EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME}) are not considered - * user-defined: the first is the root builder itself; the latter two are dedicated builders - * created by the consumer/producer auto-configurations and must not suppress the root builder. - */ -class OnMissingUserDefinedEventHubClientBuilderCondition extends SpringBootCondition implements ConfigurationCondition { - - private static final Set RESERVED_BUILDER_BEAN_NAMES = Set.of( - AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, - AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME, - AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME - ); - - @Override - public ConfigurationPhase getConfigurationPhase() { - return ConfigurationPhase.REGISTER_BEAN; - } - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( - context.getBeanFactory(), EventHubClientBuilder.class, true, false); - for (String name : beanNames) { - if (!RESERVED_BUILDER_BEAN_NAMES.contains(name)) { - return ConditionOutcome.noMatch("found user-defined EventHubClientBuilder bean: " + name); - } - } - return ConditionOutcome.match("no user-defined EventHubClientBuilder bean found"); - } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java index 62d554eb6741..7dcfb3e6efbd 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java @@ -6,6 +6,7 @@ import com.azure.data.appconfiguration.ConfigurationClientBuilder; import com.azure.messaging.eventhubs.EventHubClientBuilder; import com.azure.spring.cloud.autoconfigure.implementation.TestBuilderCustomizer; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils; import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsConnectionDetails; import com.azure.spring.cloud.core.provider.connectionstring.StaticConnectionStringProvider; @@ -93,10 +94,24 @@ void userDefinedEventHubsClientBuilderProvidedShouldNotAutoconfigure() { "spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace") ) .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) - .withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new) + .withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, EventHubClientBuilder::new) .run(context -> { assertThat(context).hasSingleBean(EventHubClientBuilder.class); - assertThat(context).hasBean("user-defined-builder"); + assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); + }); + } + + @Test + void userDefinedEventHubsClientBuilderUnderCustomNameShouldNotSuppressAutoconfigure() { + this.contextRunner + .withPropertyValues( + "spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace") + ) + .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) + .withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new) + .run(context -> { + assertThat(context).getBeanNames(EventHubClientBuilder.class) + .containsExactlyInAnyOrder("user-defined-builder", AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); }); } From e5e9b28e1992c2d893fa4e6e723b3a39085f49b3 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 16:58:43 +0800 Subject: [PATCH 7/8] Remove redundant asymmetric activation-only tests Copilot review (PR #49254) flagged two asymmetric regression tests that only assert non-activation of the opposite shared section when its event-hub-name property is absent. Because the opposite AzureEventHubs{Consumer,Producer}ClientConfiguration is gated by @ConditionalOnAnyProperty, it cannot load in those scenarios regardless of the builder-pollution fix, so the tests are false positives that would pass even on buggy code. The true regression scenarios remain covered by sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride and sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride, which set the global event-hub-name and verify the shared section binds to the root builder. --- ...tHubsConsumerClientConfigurationTests.java | 26 ------------------- ...tHubsProducerClientConfigurationTests.java | 26 ------------------- 2 files changed, 52 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java index d14c01be5fcb..90cedc8721bf 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java @@ -71,32 +71,6 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } - @Test - void producerOnlyDedicatedOverrideShouldNotActivateSharedConsumer() { - // Regression for issue #49245: producer dedicated builder must not satisfy the shared consumer condition, - // nor be injected into the shared consumer path. Without sub-level overrides on the consumer side and - // without the global event-hub-name, the consumer config simply should not activate. - contextRunner - .withPropertyValues( - "spring.cloud.azure.eventhubs.namespace=test-namespace", - "spring.cloud.azure.eventhubs.producer.event-hub-name=override-eventhub", - "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" - ) - .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) - .withUserConfiguration(AzureEventHubsAutoConfiguration.class) - .run( - context -> { - assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.class); - assertThat(context).doesNotHaveBean(EventHubConsumerClient.class); - assertThat(context).doesNotHaveBean(EventHubConsumerAsyncClient.class); - // The producer dedicated path must actually be activated for this test to exercise the - // "asymmetric, dedicated builder bean exists" scenario the bug fix targets. - assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class); - assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME); - } - ); - } - @Test void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() { // Regression for issue #49245: when both a global event-hub-name and a producer-only override exist, diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java index 3aaada10df7d..7498802a068c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java @@ -69,32 +69,6 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() { ); } - @Test - void consumerOnlyDedicatedOverrideShouldNotActivateSharedProducer() { - // Regression for issue #49245: a consumer-dedicated builder must not satisfy the shared producer condition. - // Without sub-level overrides on the producer side and without a global event-hub-name, the producer - // config simply should not activate. - contextRunner - .withPropertyValues( - "spring.cloud.azure.eventhubs.namespace=test-namespace", - "spring.cloud.azure.eventhubs.consumer.event-hub-name=override-eventhub", - "spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group" - ) - .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) - .withUserConfiguration(AzureEventHubsAutoConfiguration.class) - .run( - context -> { - assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.class); - assertThat(context).doesNotHaveBean(EventHubProducerClient.class); - assertThat(context).doesNotHaveBean(EventHubProducerAsyncClient.class); - // The consumer dedicated path must actually be activated for this test to exercise the - // "asymmetric, dedicated builder bean exists" scenario the bug fix targets. - assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class); - assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME); - } - ); - } - @Test void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() { // Regression for issue #49245: when both a global event-hub-name and a consumer-only override exist, From 636b159b3493a0aa45fb5f519e06652f08b9622c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 May 2026 17:27:32 +0800 Subject: [PATCH 8/8] Strengthen #49245 regression tests with explicit event-hub-name assertions Per Copilot review on PR #49254: the existing sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride and sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride tests only verified that multiple EventHubClientBuilder beans coexist and that a single shared client is created. They did not directly assert that the shared client was built from the root (base-eventhub) builder rather than the opposite section's dedicated (override-eventhub) builder, so a future regression of type-based wiring could still pass. Add EventHubProducerClient.getEventHubName() / EventHubConsumerClient.getEventHubName() (and the async client counterparts) assertions equal to base-eventhub to pin the actual #49245 invariant. --- .../AzureEventHubsConsumerClientConfigurationTests.java | 4 ++++ .../AzureEventHubsProducerClientConfigurationTests.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java index 90cedc8721bf..3c0543741940 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java @@ -95,6 +95,10 @@ void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() { assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); assertThat(context).hasSingleBean(EventHubConsumerClient.class); + // Pin the shared consumer to the root builder: it must target base-eventhub, + // never the producer-dedicated override-eventhub. This is the actual #49245 invariant. + assertThat(context.getBean(EventHubConsumerClient.class).getEventHubName()).isEqualTo("base-eventhub"); + assertThat(context.getBean(EventHubConsumerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub"); } ); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java index 7498802a068c..dd0b681bf229 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java @@ -93,6 +93,10 @@ void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() { assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1); assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME); assertThat(context).hasSingleBean(EventHubProducerClient.class); + // Pin the shared producer to the root builder: it must target base-eventhub, + // never the consumer-dedicated override-eventhub. This is the actual #49245 invariant. + assertThat(context.getBean(EventHubProducerClient.class).getEventHubName()).isEqualTo("base-eventhub"); + assertThat(context.getBean(EventHubProducerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub"); } ); }