diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index d1aaaffc57dc..e15247907a52 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -8,6 +8,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. #### Features Added +- Added `AzureServiceBusJmsConnectionFactoryFactory` to allow applications to customize how `ServiceBusJmsConnectionFactory` instances are created, including support for custom subclasses ([#49676](https://github.com/Azure/azure-sdk-for-java/pull/49676)). - Added support for constructing `AadB2cAuthorizationRequestResolver` with a custom `authorizationRequestBaseUri`, aligning Azure AD B2C authorization request resolution with the configurability already available for AAD. ([#49674](https://github.com/Azure/azure-sdk-for-java/pull/49674)) #### Breaking Changes diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java index 16dd582f6311..62e8c6128835 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java @@ -3,10 +3,15 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; +import com.azure.core.credential.TokenCredential; +import com.azure.identity.extensions.implementation.credential.TokenCredentialProviderOptions; +import com.azure.identity.extensions.implementation.credential.provider.TokenCredentialProvider; import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; +import com.azure.servicebus.jms.ServiceBusJmsConnectionFactorySettings; import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.implementation.resourcemanager.AzureServiceBusResourceManagerAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryCustomizer; import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.core.implementation.util.ReflectionUtils; @@ -19,6 +24,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.jms.autoconfigure.JmsAutoConfiguration; import org.springframework.boot.jms.autoconfigure.JndiConnectionFactoryAutoConfiguration; @@ -31,8 +37,11 @@ import java.net.URI; import java.util.EnumMap; import java.util.Map; +import java.util.Properties; import java.util.function.BiFunction; +import static com.azure.spring.cloud.autoconfigure.implementation.util.SpringPasswordlessPropertiesUtils.enhancePasswordlessProperties; + /** * {@link EnableAutoConfiguration Auto-configuration} for Azure Service Bus JMS support. * @@ -59,6 +68,34 @@ AzureServiceBusJmsProperties serviceBusJmsProperties(AzureGlobalProperties azure return mergeAzureProperties(azureGlobalProperties, properties); } + @Bean + @ConditionalOnMissingBean + AzureServiceBusJmsConnectionFactoryFactory azureServiceBusJmsConnectionFactoryFactory( + final AzureServiceBusJmsProperties properties) { + if (!properties.isPasswordlessEnabled()) { + return () -> new ServiceBusJmsConnectionFactory( + properties.getConnectionString(), + new ServiceBusJmsConnectionFactorySettings()); + } + + String hostName = + properties.getNamespace() + + "." + + properties.getProfile().getEnvironment().getServiceBusDomainName(); + Properties passwordlessProperties = properties.toPasswordlessProperties(); + enhancePasswordlessProperties(AzureServiceBusJmsProperties.PREFIX, properties, passwordlessProperties); + TokenCredentialProvider tokenCredentialProvider = TokenCredentialProvider.createDefault( + new TokenCredentialProviderOptions(passwordlessProperties)); + + return () -> { + TokenCredential tokenCredential = tokenCredentialProvider.get(); + return new ServiceBusJmsConnectionFactory( + tokenCredential, + hostName, + new ServiceBusJmsConnectionFactorySettings()); + }; + } + /** * Standard tier does not support the property "com.microsoft:is-client-provider", so remove it. */ diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfiguration.java index 67cc5a455358..7ac37ba648a8 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfiguration.java @@ -6,6 +6,7 @@ import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryCustomizer; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import jakarta.jms.ConnectionFactory; import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; import org.springframework.beans.BeansException; @@ -50,13 +51,15 @@ class ServiceBusJmsConnectionFactoryConfiguration { * * @param properties the Azure Service Bus JMS properties * @param customizers the list of customizers to apply + * @param instanceFactory the factory used to create the ServiceBusJmsConnectionFactory instance * @return a configured ServiceBusJmsConnectionFactory instance */ static ServiceBusJmsConnectionFactory createServiceBusJmsConnectionFactory( AzureServiceBusJmsProperties properties, - java.util.List customizers) { + java.util.List customizers, + AzureServiceBusJmsConnectionFactoryFactory instanceFactory) { return new ServiceBusJmsConnectionFactoryFactory(properties, customizers) - .createConnectionFactory(ServiceBusJmsConnectionFactory.class); + .createConnectionFactory(instanceFactory); } /** @@ -195,10 +198,13 @@ private void registerJmsPoolConnectionFactory(BeanDefinitionRegistry registry) { private ServiceBusJmsConnectionFactory createServiceBusJmsConnectionFactory() { AzureServiceBusJmsProperties serviceBusJmsProperties = beanFactory.getBean(AzureServiceBusJmsProperties.class); + AzureServiceBusJmsConnectionFactoryFactory instanceFactory = + beanFactory.getBean(AzureServiceBusJmsConnectionFactoryFactory.class); ObjectProvider factoryCustomizers = beanFactory.getBeanProvider(AzureServiceBusJmsConnectionFactoryCustomizer.class); return ServiceBusJmsConnectionFactoryConfiguration.createServiceBusJmsConnectionFactory( serviceBusJmsProperties, - factoryCustomizers.orderedStream().collect(Collectors.toList())); + factoryCustomizers.orderedStream().collect(Collectors.toList()), + instanceFactory); } } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java index b974a55f4c07..1fefca5ad631 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java @@ -3,44 +3,34 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; -import com.azure.core.credential.TokenCredential; -import com.azure.identity.extensions.implementation.credential.TokenCredentialProviderOptions; -import com.azure.identity.extensions.implementation.credential.provider.TokenCredentialProvider; import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; -import com.azure.servicebus.jms.ServiceBusJmsConnectionFactorySettings; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryCustomizer; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.List; -import java.util.Properties; - -import static com.azure.spring.cloud.autoconfigure.implementation.util.SpringPasswordlessPropertiesUtils.enhancePasswordlessProperties; class ServiceBusJmsConnectionFactoryFactory { private final AzureServiceBusJmsProperties properties; private final List factoryCustomizers; - private final TokenCredentialProvider tokenCredentialProvider; ServiceBusJmsConnectionFactoryFactory(AzureServiceBusJmsProperties properties, List factoryCustomizers) { Assert.notNull(properties, "Properties must not be null"); this.properties = properties; this.factoryCustomizers = (factoryCustomizers != null) ? factoryCustomizers : Collections.emptyList(); - if (properties.isPasswordlessEnabled()) { - Properties passwordlessProperties = properties.toPasswordlessProperties(); - enhancePasswordlessProperties(AzureServiceBusJmsProperties.PREFIX, properties, passwordlessProperties); - this.tokenCredentialProvider = TokenCredentialProvider.createDefault(new TokenCredentialProviderOptions(passwordlessProperties)); - } else { - this.tokenCredentialProvider = null; - } } - T createConnectionFactory(Class factoryClass) { - T factory = createConnectionFactoryInstance(factoryClass); + ServiceBusJmsConnectionFactory createConnectionFactory( + AzureServiceBusJmsConnectionFactoryFactory instanceFactory) { + Assert.notNull(instanceFactory, "AzureServiceBusJmsConnectionFactoryFactory must not be null"); + ServiceBusJmsConnectionFactory factory = createConnectionFactoryInstance(instanceFactory); + Assert.notNull( + factory, + "AzureServiceBusJmsConnectionFactoryFactory must create a non-null ServiceBusJmsConnectionFactory"); setClientId(factory); setPrefetchPolicy(factory); customize(factory); @@ -65,26 +55,9 @@ private void setPrefetchPolicy(T fact String.valueOf(prefetchProperties.getTopicPrefetch())); } - private T createConnectionFactoryInstance(Class factoryClass) { - try { - T factory; - if (properties.isPasswordlessEnabled()) { - String hostName = - properties.getNamespace() + "." + properties.getProfile().getEnvironment().getServiceBusDomainName(); - TokenCredential tokenCredential = tokenCredentialProvider.get(); - factory = factoryClass.getConstructor(TokenCredential.class, String.class, - ServiceBusJmsConnectionFactorySettings.class) - .newInstance(tokenCredential, hostName, - new ServiceBusJmsConnectionFactorySettings()); - } else { - factory = factoryClass.getConstructor(String.class, ServiceBusJmsConnectionFactorySettings.class) - .newInstance(properties.getConnectionString(), - new ServiceBusJmsConnectionFactorySettings()); - } - return factory; - } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - throw new IllegalStateException("Unable to create JmsConnectionFactory", ex); - } + private ServiceBusJmsConnectionFactory createConnectionFactoryInstance( + AzureServiceBusJmsConnectionFactoryFactory instanceFactory) { + return instanceFactory.createServiceBusJmsConnectionFactory(); } private void customize(ServiceBusJmsConnectionFactory connectionFactory) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsContainerConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsContainerConfiguration.java index b487df39a202..abfb825aed06 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsContainerConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsContainerConfiguration.java @@ -6,6 +6,7 @@ import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryCustomizer; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import jakarta.jms.ConnectionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,9 +69,10 @@ class ServiceBusJmsContainerConfiguration implements DisposableBean { private final AzureServiceBusJmsProperties azureServiceBusJMSProperties; private final ObjectProvider factoryCustomizers; + private final AzureServiceBusJmsConnectionFactoryFactory instanceFactory; private final Environment environment; private final JmsProperties jmsProperties; - + // Memoized dedicated listener container ConnectionFactory instances to avoid duplicates and enable lifecycle management // Use ConnectionFactory type instead of concrete types to avoid NoClassDefFoundError when optional dependencies are missing private volatile ConnectionFactory dedicatedCachingConnectionFactory; @@ -79,10 +81,12 @@ class ServiceBusJmsContainerConfiguration implements DisposableBean { ServiceBusJmsContainerConfiguration(AzureServiceBusJmsProperties azureServiceBusJMSProperties, ObjectProvider factoryCustomizers, + AzureServiceBusJmsConnectionFactoryFactory instanceFactory, Environment environment, JmsProperties jmsProperties) { this.azureServiceBusJMSProperties = azureServiceBusJMSProperties; this.factoryCustomizers = factoryCustomizers; + this.instanceFactory = instanceFactory; this.environment = environment; this.jmsProperties = jmsProperties; } @@ -199,7 +203,8 @@ private synchronized ServiceBusJmsConnectionFactory getOrCreateDedicatedServiceB private ServiceBusJmsConnectionFactory createServiceBusJmsConnectionFactory() { return ServiceBusJmsConnectionFactoryConfiguration.createServiceBusJmsConnectionFactory( azureServiceBusJMSProperties, - factoryCustomizers.orderedStream().collect(Collectors.toList())); + factoryCustomizers.orderedStream().collect(Collectors.toList()), + instanceFactory); } private void configureCommonListenerContainerFactory(DefaultJmsListenerContainerFactory jmsListenerContainerFactory) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsConnectionFactoryFactory.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsConnectionFactoryFactory.java new file mode 100644 index 000000000000..8095c3a3adcf --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsConnectionFactoryFactory.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.jms; + +import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; + +/** + * Defines how a {@link ServiceBusJmsConnectionFactory} instance is created. + *

+ * Provide this interface as a Spring bean to customize creation of + * {@link ServiceBusJmsConnectionFactory} (or a subclass). + *

+ *

+ * The factory can be invoked multiple times in one application context + * (for example sender and listener container paths). Implementations should + * return a new {@link ServiceBusJmsConnectionFactory} instance per invocation, + * or otherwise ensure the returned instance is safe to share. + *

+ */ +@FunctionalInterface +public interface AzureServiceBusJmsConnectionFactoryFactory { + + /** + * Creates an instance of {@link ServiceBusJmsConnectionFactory} or a subclass thereof. + * + * @return an instance of {@link ServiceBusJmsConnectionFactory} + */ + ServiceBusJmsConnectionFactory createServiceBusJmsConnectionFactory(); +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfigurationTests.java index b31906dba3f7..7c5394807d25 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryConfigurationTests.java @@ -3,8 +3,11 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; +import com.azure.core.credential.TokenCredential; import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; +import com.azure.servicebus.jms.ServiceBusJmsConnectionFactorySettings; import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.Destination; @@ -19,6 +22,7 @@ import org.springframework.boot.jms.autoconfigure.JmsAutoConfiguration; 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 org.springframework.context.annotation.PropertySource; import org.springframework.jms.connection.CachingConnectionFactory; @@ -180,6 +184,72 @@ void fallbackToServiceBusConnectionFactoryWhenNoCachingOrPoolClassesPresent(Stri }); } + @Test + void useCustomServiceBusJmsConnectionFactoryClassForServiceBusFactory() { + this.contextRunner + .withUserConfiguration(CustomConnectionFactoryClassConfiguration.class) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=premium", + "spring.jms.servicebus.pool.enabled=false", + "spring.jms.cache.enabled=false" + ) + .run(context -> { + assertThat(context).hasSingleBean(ServiceBusJmsConnectionFactory.class); + assertThat(context.getBean(ServiceBusJmsConnectionFactory.class)) + .isInstanceOf(CustomServiceBusJmsConnectionFactory.class); + }); + } + + @Test + void useCustomServiceBusJmsConnectionFactoryClassForPasswordlessServiceBusFactory() { + this.contextRunner + .withUserConfiguration(CustomConnectionFactoryClassConfiguration.class) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=premium", + "spring.jms.servicebus.passwordless-enabled=true", + "spring.jms.servicebus.namespace=test-namespace", + "spring.jms.servicebus.pool.enabled=false", + "spring.jms.cache.enabled=false" + ) + .run(context -> { + assertThat(context).hasSingleBean(ServiceBusJmsConnectionFactory.class); + assertThat(context.getBean(ServiceBusJmsConnectionFactory.class)) + .isInstanceOf(CustomServiceBusJmsConnectionFactory.class); + }); + } + + @Test + void useCustomServiceBusJmsConnectionFactoryClassForCachingFactory() { + this.contextRunner + .withUserConfiguration(CustomConnectionFactoryClassConfiguration.class) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=premium", + "spring.jms.cache.enabled=true" + ) + .run(context -> { + assertThat(context).hasSingleBean(CachingConnectionFactory.class); + CachingConnectionFactory cachingConnectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(cachingConnectionFactory.getTargetConnectionFactory()) + .isInstanceOf(CustomServiceBusJmsConnectionFactory.class); + }); + } + + @Test + void useCustomServiceBusJmsConnectionFactoryClassForPoolingFactory() { + this.contextRunner + .withUserConfiguration(CustomConnectionFactoryClassConfiguration.class) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=premium", + "spring.jms.servicebus.pool.enabled=true" + ) + .run(context -> { + assertThat(context).hasSingleBean(JmsPoolConnectionFactory.class); + JmsPoolConnectionFactory poolConnectionFactory = context.getBean(JmsPoolConnectionFactory.class); + assertThat(poolConnectionFactory.getConnectionFactory()) + .isInstanceOf(CustomServiceBusJmsConnectionFactory.class); + }); + } + @Test void cachingConnectionFactoryReusesSameProducerForSameDestination() throws Exception { // Create mock objects for JMS components @@ -276,4 +346,27 @@ private Session createServiceBusJmsSession(Session innerSession) throws Exceptio static class AdditionalPropertySourceConfiguration { } + + @Configuration + static class CustomConnectionFactoryClassConfiguration { + @Bean + AzureServiceBusJmsConnectionFactoryFactory connectionFactoryFactory() { + return () -> new CustomServiceBusJmsConnectionFactory( + String.format(CONNECTION_STRING_FORMAT, "test-namespace"), + new ServiceBusJmsConnectionFactorySettings()); + } + } + + static class CustomServiceBusJmsConnectionFactory extends ServiceBusJmsConnectionFactory { + CustomServiceBusJmsConnectionFactory( + String connectionString, + ServiceBusJmsConnectionFactorySettings settings) { + super(connectionString, settings); + } + + CustomServiceBusJmsConnectionFactory(TokenCredential tokenCredential, String host, + ServiceBusJmsConnectionFactorySettings settings) { + super(tokenCredential, host, settings); + } + } }