Skip to content

Commit 33e50b4

Browse files
authored
Fix Event Hubs dedicated builder polluting shared section injection (#49254)
1 parent 13bd94b commit 33e50b4

8 files changed

Lines changed: 104 additions & 13 deletions

File tree

sdk/spring/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module.
1111
- 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))
1212
- `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))
1313
- 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))
14+
- 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))
1415

1516
#### Bugs Fixed
1617

18+
- 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))
1719
- 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))
1820

1921
### Spring Cloud Azure Stream Binder Service Bus

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/context/AzureContextUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ private AzureContextUtils() {
3434
public static final String EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME =
3535
"springCloudAzureEventHubsClientBuilderFactory";
3636

37+
/**
38+
* Event Hubs client builder bean name.
39+
*/
40+
public static final String EVENT_HUB_CLIENT_BUILDER_BEAN_NAME =
41+
"springCloudAzureEventHubsClientBuilder";
42+
3743
/**
3844
* Event Hubs consumer client builder factory bean name.
3945
*/

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfiguration.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.context.annotation.Configuration;
2323
import org.springframework.context.annotation.Import;
2424

25+
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME;
2526
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME;
2627

2728
/**
@@ -39,15 +40,15 @@ class AzureEventHubsClientBuilderConfiguration {
3940
this.eventHubsProperties = eventHubsProperties;
4041
}
4142

42-
@Bean
43-
@ConditionalOnMissingBean
43+
@Bean(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME)
44+
@ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME)
4445
EventHubClientBuilder eventHubClientBuilder(@Qualifier(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME)
4546
EventHubClientBuilderFactory factory) {
4647
return factory.build();
4748
}
4849

4950
@Bean(EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME)
50-
@ConditionalOnMissingBean
51+
@ConditionalOnMissingBean(name = EVENT_HUB_CLIENT_BUILDER_FACTORY_BEAN_NAME)
5152
EventHubClientBuilderFactory eventHubClientBuilderFactory(
5253
ObjectProvider<ServiceConnectionStringProvider<AzureServiceType.EventHubs>> connectionStringProviders,
5354
ObjectProvider<AzureServiceClientBuilderCustomizer<EventHubClientBuilder>> customizers) {

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.context.annotation.Configuration;
2525
import org.springframework.context.annotation.Import;
2626

27+
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME;
2728
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_BEAN_NAME;
2829
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CONSUMER_CLIENT_BUILDER_FACTORY_BEAN_NAME;
2930

@@ -43,12 +44,13 @@
4344
class AzureEventHubsConsumerClientConfiguration {
4445

4546
@ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.consumer", name = { "connection-string", "namespace", "event-hub-name" })
46-
@ConditionalOnBean(EventHubClientBuilder.class)
47+
@ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME)
4748
@Configuration(proxyBeanMethods = false)
4849
static class SharedConsumerConnectionConfiguration {
4950

5051
private final EventHubClientBuilder builder;
51-
SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties, EventHubClientBuilder builder) {
52+
SharedConsumerConnectionConfiguration(AzureEventHubsProperties properties,
53+
@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) {
5254
this.builder = builder;
5355

5456
PropertyMapper mapper = PropertyMapper.get();
@@ -64,7 +66,7 @@ EventHubConsumerAsyncClient eventHubConsumerAsyncClient() {
6466

6567
@Bean
6668
@ConditionalOnMissingBean
67-
EventHubConsumerClient eventHubConsumerClient(EventHubClientBuilder builder) {
69+
EventHubConsumerClient eventHubConsumerClient() {
6870
return this.builder.buildConsumerClient();
6971
}
7072
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfiguration.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.context.annotation.Bean;
2222
import org.springframework.context.annotation.Configuration;
2323

24+
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME;
2425
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_BEAN_NAME;
2526
import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.EVENT_HUB_PRODUCER_CLIENT_BUILDER_FACTORY_BEAN_NAME;
2627

@@ -35,18 +36,18 @@
3536
class AzureEventHubsProducerClientConfiguration {
3637

3738
@ConditionalOnMissingProperty(prefix = "spring.cloud.azure.eventhubs.producer", name = { "connection-string", "namespace", "event-hub-name" })
38-
@ConditionalOnBean(EventHubClientBuilder.class)
39+
@ConditionalOnBean(name = EVENT_HUB_CLIENT_BUILDER_BEAN_NAME)
3940
@Configuration(proxyBeanMethods = false)
4041
static class SharedProducerConnectionConfiguration {
4142
@Bean
4243
@ConditionalOnMissingBean
43-
EventHubProducerAsyncClient eventHubProducerAsyncClient(EventHubClientBuilder builder) {
44+
EventHubProducerAsyncClient eventHubProducerAsyncClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) {
4445
return builder.buildAsyncProducerClient();
4546
}
4647

4748
@Bean
4849
@ConditionalOnMissingBean
49-
EventHubProducerClient eventHubProducerClient(EventHubClientBuilder builder) {
50+
EventHubProducerClient eventHubProducerClient(@Qualifier(EVENT_HUB_CLIENT_BUILDER_BEAN_NAME) EventHubClientBuilder builder) {
5051
return builder.buildProducerClient();
5152
}
5253
}

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsClientBuilderConfigurationTests.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.azure.data.appconfiguration.ConfigurationClientBuilder;
77
import com.azure.messaging.eventhubs.EventHubClientBuilder;
88
import com.azure.spring.cloud.autoconfigure.implementation.TestBuilderCustomizer;
9+
import com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils;
910
import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties;
1011
import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsConnectionDetails;
1112
import com.azure.spring.cloud.core.provider.connectionstring.StaticConnectionStringProvider;
@@ -93,10 +94,24 @@ void userDefinedEventHubsClientBuilderProvidedShouldNotAutoconfigure() {
9394
"spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace")
9495
)
9596
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
96-
.withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new)
97+
.withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, EventHubClientBuilder::new)
9798
.run(context -> {
9899
assertThat(context).hasSingleBean(EventHubClientBuilder.class);
99-
assertThat(context).hasBean("user-defined-builder");
100+
assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME);
101+
});
102+
}
103+
104+
@Test
105+
void userDefinedEventHubsClientBuilderUnderCustomNameShouldNotSuppressAutoconfigure() {
106+
this.contextRunner
107+
.withPropertyValues(
108+
"spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace")
109+
)
110+
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
111+
.withBean("user-defined-builder", EventHubClientBuilder.class, EventHubClientBuilder::new)
112+
.run(context -> {
113+
assertThat(context).getBeanNames(EventHubClientBuilder.class)
114+
.containsExactlyInAnyOrder("user-defined-builder", AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME);
100115
});
101116
}
102117

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsConsumerClientConfigurationTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() {
6060
"spring.cloud.azure.eventhubs.consumer.consumer-group=" + consumerGroupName
6161
)
6262
.withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class)
63-
.withBean(EventHubClientBuilder.class, () -> clientBuilder)
63+
.withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder)
6464
.run(
6565
context -> {
6666
assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class);
@@ -71,6 +71,38 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() {
7171
);
7272
}
7373

74+
@Test
75+
void sharedConsumerInjectsRootBuilderWhenProducerHasDedicatedOverride() {
76+
// Regression for issue #49245: when both a global event-hub-name and a producer-only override exist,
77+
// the shared consumer should still bind to the root builder, not the producer's dedicated builder.
78+
contextRunner
79+
.withPropertyValues(
80+
"spring.cloud.azure.eventhubs.namespace=test-namespace",
81+
"spring.cloud.azure.eventhubs.event-hub-name=base-eventhub",
82+
"spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group",
83+
"spring.cloud.azure.eventhubs.producer.event-hub-name=override-eventhub"
84+
)
85+
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
86+
.withUserConfiguration(AzureEventHubsAutoConfiguration.class)
87+
.run(
88+
context -> {
89+
assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.SharedConsumerConnectionConfiguration.class);
90+
assertThat(context).doesNotHaveBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class);
91+
// Producer dedicated must be active so multiple EventHubClientBuilder beans coexist,
92+
// proving the shared consumer is selecting the root builder by qualifier rather than
93+
// succeeding by accident because only one builder bean exists.
94+
assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class);
95+
assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1);
96+
assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME);
97+
assertThat(context).hasSingleBean(EventHubConsumerClient.class);
98+
// Pin the shared consumer to the root builder: it must target base-eventhub,
99+
// never the producer-dedicated override-eventhub. This is the actual #49245 invariant.
100+
assertThat(context.getBean(EventHubConsumerClient.class).getEventHubName()).isEqualTo("base-eventhub");
101+
assertThat(context.getBean(EventHubConsumerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub");
102+
}
103+
);
104+
}
105+
74106
@Test
75107
void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() {
76108
contextRunner

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsProducerClientConfigurationTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() {
5858
"spring.cloud.azure.eventhubs.event-hub-name=" + eventHubName
5959
)
6060
.withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class)
61-
.withBean(EventHubClientBuilder.class, () -> clientBuilder)
61+
.withBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME, EventHubClientBuilder.class, () -> clientBuilder)
6262
.run(
6363
context -> {
6464
assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class);
@@ -69,6 +69,38 @@ void withGlobalEventHubConnectionSetShouldConfigureShared() {
6969
);
7070
}
7171

72+
@Test
73+
void sharedProducerInjectsRootBuilderWhenConsumerHasDedicatedOverride() {
74+
// Regression for issue #49245: when both a global event-hub-name and a consumer-only override exist,
75+
// the shared producer should still bind to the root builder, not the consumer's dedicated builder.
76+
contextRunner
77+
.withPropertyValues(
78+
"spring.cloud.azure.eventhubs.namespace=test-namespace",
79+
"spring.cloud.azure.eventhubs.event-hub-name=base-eventhub",
80+
"spring.cloud.azure.eventhubs.consumer.event-hub-name=override-eventhub",
81+
"spring.cloud.azure.eventhubs.consumer.consumer-group=test-consumer-group"
82+
)
83+
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
84+
.withUserConfiguration(AzureEventHubsAutoConfiguration.class)
85+
.run(
86+
context -> {
87+
assertThat(context).hasSingleBean(AzureEventHubsProducerClientConfiguration.SharedProducerConnectionConfiguration.class);
88+
assertThat(context).doesNotHaveBean(AzureEventHubsProducerClientConfiguration.DedicatedProducerConnectionConfiguration.class);
89+
// Consumer dedicated must be active so multiple EventHubClientBuilder beans coexist,
90+
// proving the shared producer is selecting the root builder by qualifier rather than
91+
// succeeding by accident because only one builder bean exists.
92+
assertThat(context).hasSingleBean(AzureEventHubsConsumerClientConfiguration.DedicatedConsumerConnectionConfiguration.class);
93+
assertThat(context.getBeansOfType(EventHubClientBuilder.class)).hasSizeGreaterThan(1);
94+
assertThat(context).hasBean(AzureContextUtils.EVENT_HUB_CLIENT_BUILDER_BEAN_NAME);
95+
assertThat(context).hasSingleBean(EventHubProducerClient.class);
96+
// Pin the shared producer to the root builder: it must target base-eventhub,
97+
// never the consumer-dedicated override-eventhub. This is the actual #49245 invariant.
98+
assertThat(context.getBean(EventHubProducerClient.class).getEventHubName()).isEqualTo("base-eventhub");
99+
assertThat(context.getBean(EventHubProducerAsyncClient.class).getEventHubName()).isEqualTo("base-eventhub");
100+
}
101+
);
102+
}
103+
72104
@Test
73105
void withDedicatedEvenHubConnectionSetShouldConfigureDedicated() {
74106
contextRunner

0 commit comments

Comments
 (0)