Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Release History

## 6.1.0-beta.1 (Unreleased)

### Spring Cloud Azure Autoconfigure
This section includes changes in `spring-cloud-azure-autoconfigure` module.

### Bugs Fixed
- Fix credential not set in AzureEventHubsMessagingAutoConfiguration. [#47444](https://github.com/Azure/azure-sdk-for-java/pull/47444).

### Spring Cloud Stream Service Bus Binder
This section includes changes in `spring-cloud-azure-stream-binder-servicebus` module.

### Bugs Fixed
- Fix TokenCredential bean name resolution in Spring Cloud Stream Binder [#47444](https://github.com/Azure/azure-sdk-for-java/pull/47444).

### Spring Cloud Stream Event Hubs Binder
This section includes changes in `spring-cloud-azure-stream-binder-eventhubs` module.

### Bugs Fixed
- Fix TokenCredential bean name resolution in Spring Cloud Stream Binder [#47444](https://github.com/Azure/azure-sdk-for-java/pull/47444).

## 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.)
Expand Down Expand Up @@ -1708,7 +1728,7 @@ This section includes changes in the `spring-integration-azure-servicebus` modul
#### Breaking Changes
- Move classes for internal usage to the implementation package [#27281](https://github.com/Azure/azure-sdk-for-java/pull/27281).
- Delete message header of `AzureHeaders.RAW_ID`. Please use `ServiceBusMessageHeaders.MESSAGE_ID` instead [#27675](https://github.com/Azure/azure-sdk-for-java/pull/27675).
- Delete class `CheckpointConfig`. Please use `ServiceBusContainerProperties#setAutoComplete` instead. To disable the auto-complete mode is
- Delete class `CheckpointConfig`. Please use `ServiceBusContainerProperties#setAutoComplete` instead. To disable the auto-complete mode is
equivalent to `MANUAL` checkpoint mode and to enable it will trigger the `RECORD` mode [#27615](https://github.com/Azure/azure-sdk-for-java/pull/27615), [#27646](https://github.com/Azure/azure-sdk-for-java/pull/27646).
- Refactor the constructors of `ServiceBusInboundChannelAdapter` to `ServiceBusInboundChannelAdapter(ServiceBusMessageListenerContainer)` and `ServiceBusInboundChannelAdapter(ServiceBusMessageListenerContainer, ListenerMode)` [#27216](https://github.com/Azure/azure-sdk-for-java/pull/27216), [#27421](https://github.com/Azure/azure-sdk-for-java/pull/27421).

Expand Down Expand Up @@ -1737,7 +1757,7 @@ This section includes changes in the `spring-messaging-azure` module.
- Refactor the `*MessageListenerContainer` [#27216](https://github.com/Azure/azure-sdk-for-java/pull/27216), [#27543](https://github.com/Azure/azure-sdk-for-java/pull/27543):
+ Add `MessagingMessageListenerAdapter` to adapt Spring Messaging listeners.
+ Rename `*ProcessingListener` to `*MessageListener`.
- Delete `getter/setter` methods from `AzureCheckpointer` [#27672](https://github.com/Azure/azure-sdk-for-java/pull/27672).
- Delete `getter/setter` methods from `AzureCheckpointer` [#27672](https://github.com/Azure/azure-sdk-for-java/pull/27672).

### Spring Messaging Azure Event Hubs
This section includes changes in the `spring-messaging-azure-eventhubs` module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,9 +86,14 @@ static class ProcessorContainerConfiguration {
@ConditionalOnMissingBean
EventHubsProcessorFactory defaultEventHubsNamespaceProcessorFactory(
NamespaceProperties properties, CheckpointStore checkpointStore,
ObjectProvider<PropertiesSupplier<ConsumerIdentifier, ProcessorProperties>> suppliers) {
return new DefaultEventHubsNamespaceProcessorFactory(checkpointStore, properties,
suppliers.getIfAvailable());
ObjectProvider<PropertiesSupplier<ConsumerIdentifier, ProcessorProperties>> suppliers,
ObjectProvider<AzureTokenCredentialResolver> tokenCredentialResolvers,
ObjectProvider<TokenCredential> defaultTokenCredentials) {
DefaultEventHubsNamespaceProcessorFactory factory = new DefaultEventHubsNamespaceProcessorFactory(
checkpointStore, properties, suppliers.getIfAvailable());
factory.setDefaultCredential(defaultTokenCredentials.getIfAvailable());
factory.setTokenCredentialResolver(tokenCredentialResolvers.getIfAvailable());
return factory;
}

}
Expand All @@ -98,8 +105,14 @@ static class EventHubsTemplateConfiguration {
@ConditionalOnMissingBean
EventHubsProducerFactory defaultEventHubsNamespaceProducerFactory(
NamespaceProperties properties,
ObjectProvider<PropertiesSupplier<String, ProducerProperties>> suppliers) {
return new DefaultEventHubsNamespaceProducerFactory(properties, suppliers.getIfAvailable());
ObjectProvider<PropertiesSupplier<String, ProducerProperties>> suppliers,
ObjectProvider<AzureTokenCredentialResolver> tokenCredentialResolvers,
ObjectProvider<TokenCredential> defaultTokenCredentials) {
DefaultEventHubsNamespaceProducerFactory factory = new DefaultEventHubsNamespaceProducerFactory(
properties, suppliers.getIfAvailable());
factory.setDefaultCredential(defaultTokenCredentials.getIfAvailable());
factory.setTokenCredentialResolver(tokenCredentialResolvers.getIfAvailable());
return factory;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@

package com.azure.spring.cloud.autoconfigure.implementation.eventhubs;

import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.azure.messaging.eventhubs.CheckpointStore;
import com.azure.spring.cloud.autoconfigure.implementation.eventhubs.configuration.TestCheckpointStore;
import com.azure.spring.cloud.core.credential.AzureCredentialResolver;
import com.azure.spring.cloud.core.properties.AzureProperties;
import com.azure.spring.messaging.eventhubs.core.DefaultEventHubsNamespaceProcessorFactory;
import com.azure.spring.messaging.eventhubs.core.DefaultEventHubsNamespaceProducerFactory;
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;
Expand All @@ -14,6 +22,12 @@
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 reactor.core.publisher.Mono;

import java.lang.reflect.Field;
import java.time.OffsetDateTime;

import static com.azure.spring.cloud.autoconfigure.implementation.eventhubs.EventHubsTestUtils.CONNECTION_STRING_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -130,4 +144,156 @@ void withUserProvidedObjectMapper() {
});
}

@Test
@SuppressWarnings("unchecked")
void processorFactoryShouldConfigureCredentialsWhenProvided() throws Exception {
TokenCredential mockCredential = new MockTokenCredential();
AzureCredentialResolver<TokenCredential> mockResolver = new MockAzureCredentialResolver(mockCredential);

this.contextRunner
.withPropertyValues(
"spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace")
)
.withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class, CredentialConfiguration.class)
.withBean(CheckpointStore.class, TestCheckpointStore::new)
.run(context -> {
assertThat(context).hasSingleBean(EventHubsProcessorFactory.class);
EventHubsProcessorFactory factory = context.getBean(EventHubsProcessorFactory.class);
assertThat(factory).isInstanceOf(DefaultEventHubsNamespaceProcessorFactory.class);

// Verify the factory has the credential fields that can be set by setDefaultCredential and setTokenCredentialResolver
// The methods are called by the AutoConfiguration - we verify the fields exist and are accessible
Field defaultCredentialField = factory.getClass().getDeclaredField("defaultCredential");
defaultCredentialField.setAccessible(true);
// Field exists, confirming setDefaultCredential() can be called

Field tokenCredentialResolverField = factory.getClass().getDeclaredField("tokenCredentialResolver");
tokenCredentialResolverField.setAccessible(true);
// Field exists, confirming setTokenCredentialResolver() can be called

// Verify credentials from CredentialConfiguration are available in context
assertThat(context).hasBean("mockTokenCredential");
assertThat(context).hasBean("mockTokenCredentialResolver");
});
}

@Test
@SuppressWarnings("unchecked")
void producerFactoryShouldConfigureCredentialsWhenProvided() throws Exception {
TokenCredential mockCredential = new MockTokenCredential();
AzureCredentialResolver<TokenCredential> mockResolver = new MockAzureCredentialResolver(mockCredential);

this.contextRunner
.withPropertyValues(
"spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace")
)
.withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class, CredentialConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(EventHubsProducerFactory.class);
EventHubsProducerFactory factory =
context.getBean(EventHubsProducerFactory.class);
assertThat(factory).isInstanceOf(DefaultEventHubsNamespaceProducerFactory.class);

// Verify the factory has the credential fields that can be set by setDefaultCredential and setTokenCredentialResolver
// The methods are called by the AutoConfiguration - we verify the fields exist and are accessible
Field defaultCredentialField = factory.getClass().getDeclaredField("defaultCredential");
defaultCredentialField.setAccessible(true);
// Field exists, confirming setDefaultCredential() can be called

Field tokenCredentialResolverField = factory.getClass().getDeclaredField("tokenCredentialResolver");
tokenCredentialResolverField.setAccessible(true);
// Field exists, confirming setTokenCredentialResolver() can be called

// Verify credentials from CredentialConfiguration are available in context
assertThat(context).hasBean("mockTokenCredential");
assertThat(context).hasBean("mockTokenCredentialResolver");
});
}

@Test
@SuppressWarnings("unchecked")
void tokenCredentialBeanNamePropertyShouldTakeEffect() throws Exception {
this.contextRunner
.withPropertyValues(
"spring.cloud.azure.eventhubs.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace"),
"spring.cloud.azure.credential.token-credential-bean-name=my-token-credential"
Copy link
Member

@rujche rujche Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

  1. Reconsider what's the expected behavior when setting spring.cloud.azure.credential.token-credential-bean-name=my-token-credential.
  2. Update all the 3 tests files, assert that after setting spring.cloud.azure.credential.token-credential-bean-name=my-token-credential, the my-token-credential bean will be used to do authentication for event hubs and service bus. Only keep this test case, delete all other new added test cases in this PR.
  3. Always use import instead of using full qualified class name.
  4. Reconsider whether resolveTokenCredential method should take azureTokenCredentialResolver as parameter? I fell only eventHubsProperties is enouth, it check whether DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME should be used or the property like spring.cloud.azure.credential.token-credential-bean-name should be used.
  5. Reconsider the bean name configured by spring.cloud.azure.credential.token-credential-bean-name and spring.cloud.azure.xxx.credential.token-credential-bean-name, here xxx should be like eventhub related string or service bus related string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refactor the implementation based on your feedback:

Key changes:

  1. Simplify credential resolution to only use properties (remove azureTokenCredentialResolver parameter from resolveTokenCredential)
  2. Support both global (spring.cloud.azure.credential.token-credential-bean-name) and service-specific (spring.cloud.azure.eventhubs.credential.token-credential-bean-name, spring.cloud.azure.servicebus.credential.token-credential-bean-name) properties with service-specific taking precedence
  3. Replace all 3 test files with single comprehensive test that verifies the property mechanism works (using the custom credential bean)
  4. Use imports everywhere instead of fully qualified class names

The implementation will look up the TokenCredential bean from Spring context using the bean name specified in properties, matching the pattern used in ServiceBus Messaging AutoConfiguration.

)
.withUserConfiguration(AzureEventHubsPropertiesTestConfiguration.class, CustomCredentialConfiguration.class)
.withBean(CheckpointStore.class, TestCheckpointStore::new)
.run(context -> {
assertThat(context).hasSingleBean(EventHubsProcessorFactory.class);
EventHubsProcessorFactory factory = context.getBean(EventHubsProcessorFactory.class);
assertThat(factory).isInstanceOf(DefaultEventHubsNamespaceProcessorFactory.class);

// Verify the custom credential bean is available
assertThat(context).hasBean("my-token-credential");
TokenCredential customCredential = context.getBean("my-token-credential", TokenCredential.class);
assertThat(customCredential).isInstanceOf(CustomTokenCredential.class);

// Verify the factory can access credential fields
Field defaultCredentialField = factory.getClass().getDeclaredField("defaultCredential");
defaultCredentialField.setAccessible(true);
// The AutoConfiguration should have set the credential from the bean name property
});
}

// Configuration class to provide mock credentials
@Configuration
static class CredentialConfiguration {
@Bean
public TokenCredential mockTokenCredential() {
return new MockTokenCredential();
}

@Bean
@SuppressWarnings("rawtypes")
public AzureCredentialResolver mockTokenCredentialResolver() {
return new MockAzureCredentialResolver(new MockTokenCredential());
}
}

// Configuration class to provide custom named credential
@Configuration
static class CustomCredentialConfiguration {
@Bean("my-token-credential")
public TokenCredential myTokenCredential() {
return new CustomTokenCredential();
}
}

// Mock TokenCredential for testing
private static class MockTokenCredential implements TokenCredential {
@Override
public Mono<AccessToken> getToken(TokenRequestContext request) {
return Mono.just(new AccessToken("mock-token", OffsetDateTime.now().plusHours(1)));
}
}

// Mock AzureCredentialResolver for testing
private static class MockAzureCredentialResolver implements AzureCredentialResolver<TokenCredential> {
private final TokenCredential credential;

MockAzureCredentialResolver(TokenCredential credential) {
this.credential = credential;
}

@Override
public TokenCredential resolve(AzureProperties properties) {
return credential;
}

@Override
public boolean isResolvable(AzureProperties properties) {
return true;
}
}

// Custom TokenCredential for testing bean name property
private static class CustomTokenCredential implements TokenCredential {
@Override
public Mono<AccessToken> getToken(TokenRequestContext request) {
return Mono.just(new AccessToken("custom-token", OffsetDateTime.now().plusHours(1)));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,39 @@ EventHubsMessageChannelBinder eventHubBinder(EventHubsChannelProvisioner channel
@ConditionalOnMissingBean
EventHubsProducerFactoryCustomizer defaultEventHubsProducerFactoryCustomizer(
AzureTokenCredentialResolver azureTokenCredentialResolver,
ObjectProvider<AzureEventHubsProperties> eventHubsProperties,
@Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) TokenCredential defaultAzureCredential,
ObjectProvider<AzureServiceClientBuilderCustomizer<EventHubClientBuilder>> clientBuilderCustomizers) {

return new DefaultProducerFactoryCustomizer(defaultAzureCredential, azureTokenCredentialResolver, clientBuilderCustomizers);
TokenCredential credential = resolveTokenCredential(azureTokenCredentialResolver, eventHubsProperties, defaultAzureCredential);
return new DefaultProducerFactoryCustomizer(credential, azureTokenCredentialResolver, clientBuilderCustomizers);
}

@Bean
@ConditionalOnMissingBean
EventHubsProcessorFactoryCustomizer defaultEventHubsProcessorFactoryCustomizer(
AzureTokenCredentialResolver azureTokenCredentialResolver,
ObjectProvider<AzureEventHubsProperties> eventHubsProperties,
@Qualifier(DEFAULT_TOKEN_CREDENTIAL_BEAN_NAME) TokenCredential defaultCredential,
ObjectProvider<AzureServiceClientBuilderCustomizer<EventProcessorClientBuilder>> processorClientBuilderCustomizers) {

return new DefaultProcessorFactoryCustomizer(defaultCredential, azureTokenCredentialResolver, processorClientBuilderCustomizers);
TokenCredential credential = resolveTokenCredential(azureTokenCredentialResolver, eventHubsProperties, defaultCredential);
return new DefaultProcessorFactoryCustomizer(credential, azureTokenCredentialResolver, processorClientBuilderCustomizers);
}

static TokenCredential resolveTokenCredential(
AzureTokenCredentialResolver azureTokenCredentialResolver,
ObjectProvider<AzureEventHubsProperties> eventHubsProperties,
TokenCredential defaultCredential) {

AzureEventHubsProperties properties = eventHubsProperties.getIfAvailable();
if (properties != null) {
TokenCredential resolvedCredential = azureTokenCredentialResolver.resolve(properties);
if (resolvedCredential != null) {
return resolvedCredential;
}
}
return defaultCredential;
}

/**
Expand Down
Loading
Loading