From 8da4304254313e7885fca3b7b2ae0f3ac2e11227 Mon Sep 17 00:00:00 2001 From: braskit Date: Thu, 17 Jul 2025 10:00:11 +0200 Subject: [PATCH 1/3] Initial suggestion for making JmsConnectionFactory configurable. --- .../jms/ServiceBusJmsAutoConfiguration.java | 27 +++++ ...eBusJmsConnectionFactoryConfiguration.java | 4 +- ...ServiceBusJmsConnectionFactoryFactory.java | 29 ++---- ...ServiceBusJmsConnectionFactoryFactory.java | 16 +++ ...msConnectionFactoryConfigurationTests.java | 98 ++++++++++++++++++- 5 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsConnectionFactoryFactory.java 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 5642251af150..9dc4aef3ba20 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,11 +3,18 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; +import static com.azure.spring.cloud.autoconfigure.implementation.util.SpringPasswordlessPropertiesUtils.enhancePasswordlessProperties; + +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.AzureServiceBusJmsConnectionFactoryCustomizer; +import com.azure.spring.cloud.autoconfigure.jms.AzureServiceBusJmsConnectionFactoryFactory; import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.core.implementation.util.ReflectionUtils; import jakarta.jms.Connection; @@ -19,6 +26,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.autoconfigure.jms.JmsAutoConfiguration; import org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration; @@ -31,6 +39,7 @@ import java.net.URI; import java.util.EnumMap; import java.util.Map; +import java.util.Properties; import java.util.function.BiFunction; /** @@ -59,6 +68,24 @@ AzureServiceBusJmsProperties serviceBusJmsProperties(AzureGlobalProperties azure return mergeAzureProperties(azureGlobalProperties, properties); } + @Bean + @ConditionalOnMissingBean + AzureServiceBusJmsConnectionFactoryFactory azureServiceBusJmsConnectionFactoryFactory(final AzureServiceBusJmsProperties properties) { + return () -> { + if (properties.isPasswordlessEnabled()) { + String hostName = + properties.getNamespace() + "." + properties.getProfile().getEnvironment().getServiceBusDomainName(); + Properties passwordlessProperties = properties.toPasswordlessProperties(); + enhancePasswordlessProperties(AzureServiceBusJmsProperties.PREFIX, properties, passwordlessProperties); + TokenCredentialProvider tokenCredentialProvider = TokenCredentialProvider.createDefault(new TokenCredentialProviderOptions(passwordlessProperties)); + TokenCredential tokenCredential = tokenCredentialProvider.get(); + return new ServiceBusJmsConnectionFactory(tokenCredential, hostName, new ServiceBusJmsConnectionFactorySettings()); + } else { + return new ServiceBusJmsConnectionFactory(properties.getConnectionString(), 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 53e4b15b4f50..c8f91db7f1a7 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; @@ -117,10 +118,11 @@ 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 new ServiceBusJmsConnectionFactoryFactory(serviceBusJmsProperties, factoryCustomizers.orderedStream().collect(Collectors.toList())) - .createConnectionFactory(ServiceBusJmsConnectionFactory.class); + .createConnectionFactory(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..a88993f2f956 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,17 +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.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; @@ -39,8 +37,9 @@ class ServiceBusJmsConnectionFactoryFactory { } } - T createConnectionFactory(Class factoryClass) { - T factory = createConnectionFactoryInstance(factoryClass); + ServiceBusJmsConnectionFactory createConnectionFactory( + AzureServiceBusJmsConnectionFactoryFactory instanceFactory) { + ServiceBusJmsConnectionFactory factory = createConnectionFactoryInstance(instanceFactory); setClientId(factory); setPrefetchPolicy(factory); customize(factory); @@ -65,24 +64,10 @@ private void setPrefetchPolicy(T fact String.valueOf(prefetchProperties.getTopicPrefetch())); } - private T createConnectionFactoryInstance(Class factoryClass) { + private ServiceBusJmsConnectionFactory createConnectionFactoryInstance(AzureServiceBusJmsConnectionFactoryFactory instanceFactory) { 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) { + return instanceFactory.createServiceBusJmsConnectionFactory(); + } catch (SecurityException | IllegalArgumentException ex) { throw new IllegalStateException("Unable to create JmsConnectionFactory", ex); } } 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..a51fce6c8b87 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsConnectionFactoryFactory.java @@ -0,0 +1,16 @@ +package com.azure.spring.cloud.autoconfigure.jms; + +import com.azure.servicebus.jms.ServiceBusJmsConnectionFactory; + +/** + * The interface used to define how the {@link ServiceBusJmsConnectionFactory} instance is created. + */ +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 10c9fe284a21..2672159c536b 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,23 +3,37 @@ 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.ConnectionStringBuilder; 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.jms.AzureServiceBusJmsConnectionFactoryFactory; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.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.Primary; import org.springframework.context.annotation.PropertySource; import org.springframework.jms.connection.CachingConnectionFactory; +import static com.azure.spring.cloud.autoconfigure.implementation.util.SpringPasswordlessPropertiesUtils.enhancePasswordlessProperties; import static com.azure.spring.cloud.autoconfigure.implementation.util.TestServiceBusUtils.CONNECTION_STRING_FORMAT; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; -class ServiceBusJmsConnectionFactoryConfigurationTests { +import java.util.Properties; + +public class ServiceBusJmsConnectionFactoryConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(AzureGlobalProperties.class, AzureGlobalProperties::new) @@ -140,9 +154,91 @@ void useCacheConnectionViaAdditionConfigurationFile(String pricingTier) { }); } + @ParameterizedTest + @ValueSource(strings = {"standard", "premium"}) + void useAlternativeConnectionFactoryForPoolConnection(String pricingTier) { + this.contextRunner + .withConfiguration(AutoConfigurations.of(AlternativeConnectionFactoryConfiguration.class)) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=" + pricingTier, + "spring.jms.servicebus.pool.enabled=true" + ) + .run(context -> { + assertThat(context).hasSingleBean(JmsPoolConnectionFactory.class); + JmsPoolConnectionFactory wrapper = context.getBean(JmsPoolConnectionFactory.class); + assertEquals(AlternativeConnectionFactory.class, wrapper.getConnectionFactory().getClass()); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"standard", "premium"}) + void useAlternativeConnectionFactoryForCacheConnection(String pricingTier) { + this.contextRunner + .withConfiguration(AutoConfigurations.of(AlternativeConnectionFactoryConfiguration.class)) + .withPropertyValues( + "spring.jms.servicebus.pricing-tier=" + pricingTier, + "spring.jms.servicebus.pool.enabled=false" + ) + .run(context -> { + assertThat(context).hasSingleBean(CachingConnectionFactory.class); + CachingConnectionFactory wrapper = context.getBean(CachingConnectionFactory.class); + assertEquals(AlternativeConnectionFactory.class, wrapper.getTargetConnectionFactory().getClass()); + }); + } + @Configuration @PropertySource("classpath:servicebus/additional.properties") static class AdditionalPropertySourceConfiguration { } + + @Configuration + static class AlternativeConnectionFactoryConfiguration { + + @Autowired + AzureServiceBusJmsProperties properties; + + @Bean + @Primary + AzureServiceBusJmsConnectionFactoryFactory connectionInstanceFactory() { + return () -> { + if (properties.isPasswordlessEnabled()) { + String hostName = + properties.getNamespace() + "." + properties.getProfile().getEnvironment().getServiceBusDomainName(); + Properties passwordlessProperties = properties.toPasswordlessProperties(); + enhancePasswordlessProperties(AzureServiceBusJmsProperties.PREFIX, properties, passwordlessProperties); + TokenCredentialProvider tokenCredentialProvider = TokenCredentialProvider.createDefault(new TokenCredentialProviderOptions(passwordlessProperties)); + TokenCredential tokenCredential = tokenCredentialProvider.get(); + return new AlternativeConnectionFactory(tokenCredential, hostName, new ServiceBusJmsConnectionFactorySettings()); + } else { + return new AlternativeConnectionFactory(properties.getConnectionString(), new ServiceBusJmsConnectionFactorySettings()); + } + }; + } + } + + // declare the "access chain" public or face a from checkstyle violation or a + // java.lang.NoSuchMethodException when running the tests from the command line + public static class AlternativeConnectionFactory extends ServiceBusJmsConnectionFactory { + + public AlternativeConnectionFactory() { + } + + public AlternativeConnectionFactory(String connectionString, ServiceBusJmsConnectionFactorySettings settings) { + super(connectionString, settings); + } + + public AlternativeConnectionFactory(ConnectionStringBuilder connectionStringBuilder, ServiceBusJmsConnectionFactorySettings settings) { + super(connectionStringBuilder, settings); + } + + public AlternativeConnectionFactory(String sasKeyName, String sasKey, String host, ServiceBusJmsConnectionFactorySettings settings) { + super(sasKey, sasKey, host, settings); + } + + public AlternativeConnectionFactory(TokenCredential credential, String host, ServiceBusJmsConnectionFactorySettings settings) { + super(credential, host, settings); + } + } } + From 73e21a4340ff23e2ba79035ab98cc0dd18e28fa2 Mon Sep 17 00:00:00 2001 From: braskit Date: Thu, 17 Jul 2025 10:18:39 +0200 Subject: [PATCH 2/3] Updated CHANGELOG.md. --- sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md index 55302c9d0c61..2fb251faa77f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md @@ -3,7 +3,8 @@ ## 5.23.0-beta.1 (Unreleased) ### Features Added - +- Create a new interface 'AzureServiceBusJmsConnectionFactoryFactory' that allows a user to specify how the JmsConnectionFactory should be created. +- ### Breaking Changes ### Bugs Fixed From afa5796fd6566f96c53dbf7e75d173cb4367720d Mon Sep 17 00:00:00 2001 From: braskit Date: Mon, 21 Jul 2025 10:37:02 +0200 Subject: [PATCH 3/3] Add missing copyright header. --- .../jms/AzureServiceBusJmsConnectionFactoryFactory.java | 3 +++ 1 file changed, 3 insertions(+) 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 index a51fce6c8b87..deb9050d255c 100644 --- 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 @@ -1,3 +1,6 @@ +// 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;