diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index e15247907a52..13874b75783e 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -25,6 +25,14 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. - Upgrade to Jackson 3 to align with Spring Boot 4 ([#49538](https://github.com/Azure/azure-sdk-for-java/issues/49538)). +### Spring Cloud Azure Service + +This section includes changes in `spring-cloud-azure-service` module. + +#### Bugs Fixed + +- Fixed the Service Bus producer, consumer and processor sub-client builders overwriting configuration already set on the underlying `ServiceBusClientBuilder` through an `AzureServiceClientBuilderCustomizer` (for example the `ClientOptions` carrying `TracingOptions`). The customizers are now applied to the underlying builder as the last step, so their configuration is preserved ([#49742](https://github.com/Azure/azure-sdk-for-java/issues/49742)). + ### Spring Messaging Azure This section includes changes in `spring-messaging-azure` module. diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/messaging/AzureMessagingListenerAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/messaging/AzureMessagingListenerAutoConfiguration.java index ee1c360a9459..3fc1d730e27c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/messaging/AzureMessagingListenerAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/messaging/AzureMessagingListenerAutoConfiguration.java @@ -49,8 +49,10 @@ public class AzureMessagingListenerAutoConfiguration { @Configuration @ConditionalOnBean(EventHubsProcessorFactory.class) static class EventHubsConfiguration { - @Bean(name = "azureEventHubsListenerContainerFactory") - @ConditionalOnMissingBean(name = "azureEventHubsListenerContainerFactory") + @Bean( + name = EventHubsListenerAnnotationBeanPostProcessor.DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + @ConditionalOnMissingBean( + name = EventHubsListenerAnnotationBeanPostProcessor.DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME) public MessageListenerContainerFactory azureEventHubsListenerContainerFactory( EventHubsProcessorFactory eventHubsProcessorFactory, ObjectProvider> messageConverterProvider) { @@ -73,8 +75,10 @@ static class EnableEventHubsConfiguration { @Configuration @ConditionalOnBean(ServiceBusProcessorFactory.class) static class ServiceBusConfiguration { - @Bean(name = "azureServiceBusListenerContainerFactory") - @ConditionalOnMissingBean(name = "azureServiceBusListenerContainerFactory") + @Bean( + name = ServiceBusListenerAnnotationBeanPostProcessor.DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + @ConditionalOnMissingBean( + name = ServiceBusListenerAnnotationBeanPostProcessor.DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME) public MessageListenerContainerFactory azureServiceBusListenerContainerFactory( ServiceBusProcessorFactory serviceBusProcessorFactory, ObjectProvider> messageConverterProvider) { diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/factory/AbstractServiceBusSubClientBuilderFactory.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/factory/AbstractServiceBusSubClientBuilderFactory.java index 060dfc07a26f..836a0550d186 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/factory/AbstractServiceBusSubClientBuilderFactory.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/factory/AbstractServiceBusSubClientBuilderFactory.java @@ -39,10 +39,11 @@ abstract class AbstractServiceBusSubClientBuilderFactory> serviceBusClientBuilderCustomizers; /** * Create a {@link AbstractServiceBusSubClientBuilderFactory} instance with the properties and the collection of - * @{link ServiceBusClientBuilder} customizers. + * {@link AzureServiceClientBuilderCustomizer} customizers. * @param properties the properties describing the service bus sub client, which could be a sender, a receiver or * a processor. * @param serviceClientBuilderCustomizers the collection of customizers for the service bus client builder. @@ -70,11 +71,16 @@ protected AbstractServiceBusSubClientBuilderFactory(ServiceBusClientBuilder serv this.serviceBusClientBuilder = serviceBusClientBuilder; this.shareServiceBusClientBuilder = true; this.serviceBusClientBuilderFactory = null; + // The shared ServiceBusClientBuilder is configured and customized by its own factory, so there are no + // customizers for this sub-client builder factory to re-apply. + this.serviceBusClientBuilderCustomizers = null; } else { this.serviceBusClientBuilderFactory = new ServiceBusClientBuilderFactory(properties); - if (serviceBusClientBuilderCustomizers != null) { - serviceBusClientBuilderCustomizers.forEach(this.serviceBusClientBuilderFactory::addBuilderCustomizer); - } + // Remember the customizers instead of adding them to the nested factory, so they are applied to the + // underlying ServiceBusClientBuilder as the last step of each build() call. This ensures they win over + // the property-derived configuration this factory redirects onto the shared builder (for example + // ClientOptions carrying TracingOptions). See #49742. + this.serviceBusClientBuilderCustomizers = serviceBusClientBuilderCustomizers; // Don't build yet - defer until first use when ApplicationContext is available this.serviceBusClientBuilder = null; this.shareServiceBusClientBuilder = false; @@ -90,6 +96,23 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } } + @Override + public T build() { + // super.build() creates the sub-client builder and, in the non-shared path, lazily builds and configures + // the underlying ServiceBusClientBuilder from the properties (including the Spring identifier). + // The customizers are deliberately not applied during that step. + T builder = super.build(); + // Apply the ServiceBusClientBuilder customizers as the last step of this build() call, + // so they take precedence over the property-derived configuration redirected onto the underlying builder. + // See #49742. + if (!isShareServiceBusClientBuilder() && this.serviceBusClientBuilderCustomizers != null) { + ServiceBusClientBuilder parentServiceBusClientBuilder = getServiceBusClientBuilder(); + this.serviceBusClientBuilderCustomizers.forEach(customizer -> + customizer.customize(parentServiceBusClientBuilder)); + } + return builder; + } + @Override protected void configureCredential(T builder) { // skip to avoid overriding the parent builder's credentials. diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/lifecycle/ServiceBusProcessorClientLifecycleManager.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/lifecycle/ServiceBusProcessorClientLifecycleManager.java index bc34275b242b..16aa4882f8c2 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/lifecycle/ServiceBusProcessorClientLifecycleManager.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/servicebus/lifecycle/ServiceBusProcessorClientLifecycleManager.java @@ -12,10 +12,12 @@ /** * Helper class to control the lifecycle of a {@link ServiceBusProcessorClient}. - * This class implements {@code SmartLifecycle} to auto start {@link ServiceBusProcessorClient} when the Spring Application Context starts, - * And stop the {@link ServiceBusProcessorClient} when the Spring Application Context stops. - * NOTE, there is not need to call {@link #start()} or {@link #stop()} explicitly, as the {@link ServiceBusProcessorClient} will be started and stopped automatically. - * And since the {@link ServiceBusProcessorClient} is a {@link AutoCloseable}, there is no need to call {@link ServiceBusProcessorClient#close()} explicitly. + * This class implements {@code SmartLifecycle} to automatically start {@link ServiceBusProcessorClient} when the + * Spring Application Context starts and stops it when the Spring Application Context stops. + * NOTE: There is no need to call {@link #start()} or {@link #stop()} explicitly, as the + * {@link ServiceBusProcessorClient} will be started and stopped automatically. + * Since {@link ServiceBusProcessorClient} is {@link AutoCloseable}, there is no need to call + * {@link ServiceBusProcessorClient#close()} explicitly. */ public class ServiceBusProcessorClientLifecycleManager implements SmartLifecycle { private final Logger logger = LoggerFactory.getLogger(ServiceBusProcessorClientLifecycleManager.class); diff --git a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/servicebus/factory/ServiceBusSenderClientBuilderFactoryTests.java b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/servicebus/factory/ServiceBusSenderClientBuilderFactoryTests.java index 06dd1c00391e..77c4084c6a03 100644 --- a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/servicebus/factory/ServiceBusSenderClientBuilderFactoryTests.java +++ b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/servicebus/factory/ServiceBusSenderClientBuilderFactoryTests.java @@ -3,10 +3,18 @@ package com.azure.spring.cloud.service.implementation.servicebus.factory; +import com.azure.core.util.ClientOptions; import com.azure.messaging.servicebus.ServiceBusClientBuilder; +import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer; import com.azure.spring.cloud.service.implementation.servicebus.properties.ServiceBusSenderClientTestProperties; import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.List; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doReturn; @@ -34,6 +42,29 @@ void queueConfigured() { verify(builder, times(1)).queueName("test-queue"); } + @Test + void serviceBusClientBuilderCustomizerAppliedAsLastStep() { + ServiceBusSenderClientTestProperties properties = createMinimalServiceProperties(); + properties.setShareServiceBusClientBuilder(false); + + ServiceBusClientBuilder rootBuilder = mock(ServiceBusClientBuilder.class); + @SuppressWarnings("unchecked") + AzureServiceClientBuilderCustomizer customizer = + mock(AzureServiceClientBuilderCustomizer.class); + + ServiceBusSenderClientBuilderFactory factory = + new CustomizerTestFactory(properties, Collections.singletonList(customizer), rootBuilder); + + factory.build(); + + InOrder inOrder = Mockito.inOrder(rootBuilder, customizer); + inOrder.verify(rootBuilder, atLeast(1)) + .clientOptions(ArgumentMatchers.any(ClientOptions.class)); + inOrder.verify(customizer, times(1)).customize(rootBuilder); + inOrder.verify(rootBuilder, Mockito.never()) + .clientOptions(ArgumentMatchers.any(ClientOptions.class)); + } + @Override protected ServiceBusSenderClientTestProperties createMinimalServiceProperties() { ServiceBusSenderClientTestProperties properties = new ServiceBusSenderClientTestProperties(); @@ -100,4 +131,24 @@ protected ServiceBusClientBuilder getServiceBusClientBuilder() { return this.serviceBusClientBuilder; } } + + static class CustomizerTestFactory extends ServiceBusSenderClientBuilderFactory { + private final ServiceBusClientBuilder serviceBusClientBuilder; + CustomizerTestFactory(ServiceBusSenderClientTestProperties properties, + List> customizers, + ServiceBusClientBuilder serviceBusClientBuilder) { + super(properties, customizers); + this.serviceBusClientBuilder = serviceBusClientBuilder; + } + + @Override + public ServiceBusClientBuilder.ServiceBusSenderClientBuilder createBuilderInstance() { + return mock(ServiceBusClientBuilder.ServiceBusSenderClientBuilder.class); + } + + @Override + protected ServiceBusClientBuilder getServiceBusClientBuilder() { + return this.serviceBusClientBuilder; + } + } } diff --git a/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListener.java b/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListener.java index 968b79e3c680..a3e6a6fc0bb1 100644 --- a/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListener.java +++ b/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListener.java @@ -20,7 +20,7 @@ * specified {@link #destination}. The {@link #containerFactory} identifies the * {@link MessageListenerContainerFactory} to use to build * the Azure listener container. If not set, a default container factory is - * assumed to be available with a bean name of {@code eventHubsListenerContainerFactory} + * assumed to be available with a bean name of {@code azureEventHubsListenerContainerFactory} * unless an explicit default has been provided through configuration. * *

Processing of {@code @EventHubsListener} annotations is performed by registering a diff --git a/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListenerAnnotationBeanPostProcessor.java b/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListenerAnnotationBeanPostProcessor.java index da40c1b3065f..715740a5f7fb 100644 --- a/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListenerAnnotationBeanPostProcessor.java +++ b/sdk/spring/spring-messaging-azure-eventhubs/src/main/java/com/azure/spring/messaging/eventhubs/implementation/core/annotation/EventHubsListenerAnnotationBeanPostProcessor.java @@ -42,7 +42,7 @@ public class EventHubsListenerAnnotationBeanPostProcessor extends AzureListenerAnnotationBeanPostProcessorAdapter { public static final String DEFAULT_EVENT_HUBS_LISTENER_ANNOTATION_BPP_BEAN_NAME = "eventHubsListenerAnnotationBeanPostProcessor"; - private static final String DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "azureEventHubsListenerContainerFactory"; + public static final String DEFAULT_EVENT_HUBS_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "azureEventHubsListenerContainerFactory"; /** * Construct an {@link EventHubsListenerAnnotationBeanPostProcessor} instance with default configuration. diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListener.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListener.java index 2de391e86e36..f1009cba2384 100644 --- a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListener.java +++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListener.java @@ -20,7 +20,7 @@ * specified {@link #destination}. The {@link #containerFactory} identifies the * {@link MessageListenerContainerFactory} to use to build * the Azure listener container. If not set, a default container factory is - * assumed to be available with a bean name of {@code serviceBusListenerContainerFactory} + * assumed to be available with a bean name of {@code azureServiceBusListenerContainerFactory} * unless an explicit default has been provided through configuration. * *

Processing of {@code @ServiceBusListener} annotations is performed by registering a diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListenerAnnotationBeanPostProcessor.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListenerAnnotationBeanPostProcessor.java index 10496af577f1..e9c7add84901 100644 --- a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListenerAnnotationBeanPostProcessor.java +++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/core/annotation/ServiceBusListenerAnnotationBeanPostProcessor.java @@ -43,7 +43,7 @@ public class ServiceBusListenerAnnotationBeanPostProcessor extends AzureListenerAnnotationBeanPostProcessorAdapter { public static final String DEFAULT_SERVICE_BUS_LISTENER_ANNOTATION_BPP_BEAN_NAME = "serviceBusListenerAnnotationBeanPostProcessor"; - private static final String DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "azureServiceBusListenerContainerFactory"; + public static final String DEFAULT_SERVICE_BUS_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "azureServiceBusListenerContainerFactory"; /** * Construct an {@link ServiceBusListenerAnnotationBeanPostProcessor} instance with default configuration. @@ -64,7 +64,7 @@ protected AzureListenerEndpoint createAndConfigureMethodListenerEndpoint(Service BeanFactory beanFactory, MessageHandlerMethodFactory messageHandlerMethodFactory) { MethodServiceBusListenerEndpoint endpoint = new MethodServiceBusListenerEndpoint(); - + endpoint.setBean(bean); endpoint.setMethod(method); endpoint.setBeanFactory(beanFactory); diff --git a/sdk/spring/spring-messaging-azure/src/main/java/com/azure/spring/messaging/implementation/config/AzureListenerEndpoint.java b/sdk/spring/spring-messaging-azure/src/main/java/com/azure/spring/messaging/implementation/config/AzureListenerEndpoint.java index 5ba0d9fafc5f..3a919fccce3a 100644 --- a/sdk/spring/spring-messaging-azure/src/main/java/com/azure/spring/messaging/implementation/config/AzureListenerEndpoint.java +++ b/sdk/spring/spring-messaging-azure/src/main/java/com/azure/spring/messaging/implementation/config/AzureListenerEndpoint.java @@ -8,8 +8,8 @@ import com.azure.spring.messaging.listener.MessageListenerContainer; /** - * Model for a Azure listener endpoint. Can be used against a {@link AzureListenerConfigurer AzureListenerConfigurer} to - * register endpoints programmatically. + * Model for an Azure listener endpoint. Can be used against a {@link AzureListenerConfigurer AzureListenerConfigurer} + * to register endpoints programmatically. */ public interface AzureListenerEndpoint {