diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 725fce466446..68c4ca2bd311 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.1.0 (Not released) + +### Spring Cloud Azure Autoconfigure +This section includes changes in `spring-cloud-azure-autoconfigure` module. + +#### Bugs Fixed + +- 2 `TokenCredential` bean found in AzureServiceBusMessagingAutoConfiguration. [#47470](https://github.com/Azure/azure-sdk-for-java/pull/47470) +- `spring.cloud.azure.eventhubs.credential.token-credential-bean-name` not take effect in AzureEventHubsMessagingAutoConfiguration. [#47470](https://github.com/Azure/azure-sdk-for-java/pull/47470) + ## 6.0.0 (2025-09-22) - This release is compatible with Spring Boot 3.5.0-3.5.5. (Note: 3.5.x (x>5) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2025.0.0. (Note: 2025.0.x(x>0) should be supported, but they aren't tested with this release.) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfiguration.java index 690d5c158937..5526f7e995a8 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfiguration.java @@ -3,10 +3,12 @@ package com.azure.spring.cloud.autoconfigure.implementation.eventhubs; +import com.azure.core.credential.TokenCredential; import com.azure.messaging.eventhubs.CheckpointStore; import com.azure.messaging.eventhubs.EventData; import com.azure.spring.cloud.autoconfigure.implementation.condition.ConditionalOnAnyProperty; import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsProperties; +import com.azure.spring.cloud.core.implementation.credential.resolver.AzureTokenCredentialResolver; import com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider; import com.azure.spring.cloud.core.service.AzureServiceType; import com.azure.spring.messaging.ConsumerIdentifier; @@ -27,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -37,6 +40,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME; import static com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils.copyAzureCommonProperties; /** @@ -83,10 +87,16 @@ static class ProcessorContainerConfiguration { @Bean @ConditionalOnMissingBean EventHubsProcessorFactory defaultEventHubsNamespaceProcessorFactory( - NamespaceProperties properties, CheckpointStore checkpointStore, - ObjectProvider> suppliers) { - return new DefaultEventHubsNamespaceProcessorFactory(checkpointStore, properties, + NamespaceProperties properties, + CheckpointStore checkpointStore, + ObjectProvider> suppliers, + ObjectProvider tokenCredentialResolvers, + @Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) ObjectProvider defaultTokenCredentials) { + DefaultEventHubsNamespaceProcessorFactory factory = new DefaultEventHubsNamespaceProcessorFactory(checkpointStore, properties, suppliers.getIfAvailable()); + factory.setDefaultCredential(defaultTokenCredentials.getIfAvailable()); + factory.setTokenCredentialResolver(tokenCredentialResolvers.getIfAvailable()); + return factory; } } @@ -98,8 +108,13 @@ static class EventHubsTemplateConfiguration { @ConditionalOnMissingBean EventHubsProducerFactory defaultEventHubsNamespaceProducerFactory( NamespaceProperties properties, - ObjectProvider> suppliers) { - return new DefaultEventHubsNamespaceProducerFactory(properties, suppliers.getIfAvailable()); + ObjectProvider> suppliers, + ObjectProvider tokenCredentialResolvers, + @Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) ObjectProvider defaultTokenCredentials) { + DefaultEventHubsNamespaceProducerFactory factory = new DefaultEventHubsNamespaceProducerFactory(properties, suppliers.getIfAvailable()); + factory.setDefaultCredential(defaultTokenCredentials.getIfAvailable()); + factory.setTokenCredentialResolver(tokenCredentialResolvers.getIfAvailable()); + return factory; } @Bean diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java index 102386fa6bd7..6a410336a85b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -44,6 +45,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static com.azure.spring.cloud.autoconfigure.implementation.context.AzureContextUtils.DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME; import static com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils.copyAzureCommonProperties; @@ -92,7 +94,7 @@ ServiceBusProcessorFactory defaultServiceBusNamespaceProcessorFactory( NamespaceProperties properties, ObjectProvider> suppliers, ObjectProvider tokenCredentialResolvers, - ObjectProvider defaultTokenCredentials, + @Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) ObjectProvider defaultTokenCredentials, ObjectProvider> clientBuilderCustomizers, ObjectProvider> processorClientBuilderCustomizers, ObjectProvider> sessionProcessorClientBuilderCustomizers) { @@ -115,7 +117,7 @@ ServiceBusConsumerFactory defaultServiceBusNamespaceConsumerFactory( NamespaceProperties properties, ObjectProvider> suppliers, ObjectProvider tokenCredentialResolvers, - ObjectProvider defaultTokenCredentials, + @Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) ObjectProvider defaultTokenCredentials, ObjectProvider> customizers, ObjectProvider> sessionReceiverCustomizers) { DefaultServiceBusNamespaceConsumerFactory factory = new DefaultServiceBusNamespaceConsumerFactory(properties, suppliers.getIfAvailable()); @@ -136,7 +138,7 @@ ServiceBusProducerFactory defaultServiceBusNamespaceProducerFactory( NamespaceProperties properties, ObjectProvider> suppliers, ObjectProvider tokenCredentialResolvers, - ObjectProvider defaultTokenCredentials, + @Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) ObjectProvider defaultTokenCredentials, ObjectProvider> clientBuilderCustomizers, ObjectProvider> senderClientBuilderCustomizers) { DefaultServiceBusNamespaceProducerFactory factory = new DefaultServiceBusNamespaceProducerFactory(properties, suppliers.getIfAvailable()); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfigurationTests.java index eea8bec890b2..87daf3997d50 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/eventhubs/AzureEventHubsMessagingAutoConfigurationTests.java @@ -3,9 +3,15 @@ package com.azure.spring.cloud.autoconfigure.implementation.eventhubs; +import com.azure.core.credential.TokenCredential; import com.azure.messaging.eventhubs.CheckpointStore; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureTokenCredentialAutoConfiguration; import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.configuration.TestCheckpointStore; +import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.properties.AzureEventHubsProperties; +import com.azure.spring.cloud.core.credential.AzureCredentialResolver; import com.azure.spring.messaging.eventhubs.core.EventHubsProcessorFactory; +import com.azure.spring.messaging.eventhubs.core.EventHubsProducerFactory; import com.azure.spring.messaging.eventhubs.core.EventHubsTemplate; import com.azure.spring.messaging.eventhubs.implementation.support.converter.EventHubsMessageConverter; import com.fasterxml.jackson.databind.ObjectMapper; @@ -14,10 +20,15 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.lang.reflect.Field; import static com.azure.spring.cloud.autoconfigure.implementation.eventhubs.EventHubsTestUtils.CONNECTION_STRING_FORMAT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; class AzureEventHubsMessagingAutoConfigurationTests { @@ -121,7 +132,7 @@ void withUserProvidedObjectMapper() { .withPropertyValues("spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace"), "spring.cloud.azure.message-converter.isolated-object-mapper=false") .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) - .withBean("userObjectMapper", ObjectMapper.class, () -> new ObjectMapper()) + .withBean("userObjectMapper", ObjectMapper.class, ObjectMapper::new) .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) .run(context -> { assertThat(context).hasBean("userObjectMapper"); @@ -130,4 +141,61 @@ void withUserProvidedObjectMapper() { }); } + @Test + void testCustomTokenCredentialConfiguration() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(CustomTokenCredentialConfiguration.class, + AzureTokenCredentialAutoConfiguration.class, + AzureGlobalPropertiesAutoConfiguration.class)) + .withBean(EventHubsMessageConverter.class, EventHubsMessageConverter::new) + .withPropertyValues( + "spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace"), + "spring.cloud.azure.eventhubs.credential.token-credential-bean-name=customTokenCredential" + ) + .withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class) + .run(context -> { + + // Verify that the properties contain the correct credential bean name + AzureEventHubsProperties eventHubsProperties = context.getBean(AzureEventHubsProperties.class); + assertThat(eventHubsProperties).isNotNull(); + assertThat(eventHubsProperties.getCredential()).isNotNull(); + assertThat(eventHubsProperties.getCredential().getTokenCredentialBeanName()) + .as("The token-credential-bean-name property should be set to customTokenCredential") + .isEqualTo("customTokenCredential"); + + // Verify that the custom token credential bean exists + assertThat(context).hasBean("customTokenCredential"); + TokenCredential customCredential = context.getBean("customTokenCredential", TokenCredential.class); + assertThat(customCredential).isNotNull(); + + // Verify the EventHubsProducerFactory has the tokenCredentialResolver configured + assertThat(context).hasSingleBean(EventHubsProducerFactory.class); + EventHubsProducerFactory producerFactory = context.getBean(EventHubsProducerFactory.class); + assertThat(producerFactory).isNotNull(); + + // Verify tokenCredentialResolver resolves to the custom credential + Field tokenCredentialResolverField = producerFactory.getClass().getDeclaredField("tokenCredentialResolver"); + tokenCredentialResolverField.setAccessible(true); + Object tokenCredentialResolver = tokenCredentialResolverField.get(producerFactory); + assertThat(tokenCredentialResolver).as("TokenCredentialResolver should be configured").isNotNull(); + + // Cast to AzureCredentialResolver and invoke resolve() to verify it returns customTokenCredential + @SuppressWarnings("unchecked") + AzureCredentialResolver resolver = + (AzureCredentialResolver) tokenCredentialResolver; + TokenCredential resolvedCredential = resolver.resolve(eventHubsProperties); + assertThat(resolvedCredential) + .as("The resolved credential should be the customTokenCredential bean") + .isSameAs(customCredential); + }); + } + + @Configuration + public static class CustomTokenCredentialConfiguration { + @Bean + public TokenCredential customTokenCredential() { + return mock(TokenCredential.class); + } + } + } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java index e1622836b4ad..0e60e03432c1 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java @@ -3,7 +3,13 @@ package com.azure.spring.cloud.autoconfigure.implementation.servicebus; +import com.azure.core.credential.TokenCredential; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.context.AzureTokenCredentialAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.implementation.servicebus.properties.AzureServiceBusProperties; +import com.azure.spring.cloud.core.credential.AzureCredentialResolver; import com.azure.spring.messaging.servicebus.core.ServiceBusProcessorFactory; +import com.azure.spring.messaging.servicebus.core.ServiceBusProducerFactory; import com.azure.spring.messaging.servicebus.core.ServiceBusTemplate; import com.azure.spring.messaging.servicebus.implementation.support.converter.ServiceBusMessageConverter; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,10 +18,15 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.lang.reflect.Field; import static com.azure.spring.cloud.autoconfigure.implementation.util.TestServiceBusUtils.CONNECTION_STRING_FORMAT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; class AzureServiceBusMessagingAutoConfigurationTests { @@ -117,7 +128,7 @@ void withUserProvidedObjectMapper() { .withPropertyValues("spring.cloud.azure.servicebus.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace"), "spring.cloud.azure.message-converter.isolated-object-mapper=false") .withUserConfiguration(AzureServiceBusPropertiesTestConfiguration.class) - .withBean("userObjectMapper", ObjectMapper.class, () -> new ObjectMapper()) + .withBean("userObjectMapper", ObjectMapper.class, ObjectMapper::new) .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) .run(context -> { assertThat(context).hasBean("userObjectMapper"); @@ -126,4 +137,63 @@ void withUserProvidedObjectMapper() { }); } + @Test + void testCustomTokenCredentialConfiguration() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(CustomTokenCredentialConfiguration.class, + AzureTokenCredentialAutoConfiguration.class, + AzureGlobalPropertiesAutoConfiguration.class)) + .withBean(ServiceBusMessageConverter.class, () -> mock(ServiceBusMessageConverter.class)) + .withPropertyValues( + "spring.cloud.azure.servicebus.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace"), + "spring.cloud.azure.servicebus.credential.token-credential-bean-name=customTokenCredential" + ) + .withUserConfiguration(AzureServiceBusPropertiesTestConfiguration.class) + .run(context -> { + + // Verify that the properties contain the correct credential bean name + AzureServiceBusProperties serviceBusProperties = context.getBean(AzureServiceBusProperties.class); + assertThat(serviceBusProperties).isNotNull(); + assertThat(serviceBusProperties.getCredential()).isNotNull(); + assertThat(serviceBusProperties.getCredential().getTokenCredentialBeanName()) + .as("The token-credential-bean-name property should be set to customTokenCredential") + .isEqualTo("customTokenCredential"); + + // Verify that the custom token credential bean exists + assertThat(context).hasBean("customTokenCredential"); + TokenCredential customCredential = context.getBean("customTokenCredential", TokenCredential.class); + assertThat(customCredential).isNotNull(); + + // Verify the ServiceBusProducerFactory has the tokenCredentialResolver configured + assertThat(context).hasSingleBean(ServiceBusProducerFactory.class); + ServiceBusProducerFactory producerFactory = context.getBean(ServiceBusProducerFactory.class); + assertThat(producerFactory).isNotNull(); + + // Verify tokenCredentialResolver resolves to the custom credential + Field tokenCredentialResolverField = + producerFactory.getClass().getDeclaredField("tokenCredentialResolver"); + tokenCredentialResolverField.setAccessible(true); + Object tokenCredentialResolver = tokenCredentialResolverField.get(producerFactory); + assertThat(tokenCredentialResolver) + .as("TokenCredentialResolver should be configured").isNotNull(); + + // Cast to AzureCredentialResolver and invoke resolve() to verify it returns customTokenCredential + @SuppressWarnings("unchecked") + AzureCredentialResolver resolver = + (AzureCredentialResolver) tokenCredentialResolver; + TokenCredential resolvedCredential = resolver.resolve(serviceBusProperties); + assertThat(resolvedCredential) + .as("The resolved credential should be the customTokenCredential bean") + .isSameAs(customCredential); + }); + } + + @Configuration + public static class CustomTokenCredentialConfiguration { + @Bean + public TokenCredential customTokenCredential() { + return mock(TokenCredential.class); + } + } + }