Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ s3Template.store(BUCKET, "person.json", p);
Person loadedPerson = s3Template.read(BUCKET, "person.json", Person.class);
----

By default, if Jackson is on the classpath, `S3Template` uses `ObjectMapper` based `Jackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
By default, if Jackson 3 is on the classpath, `S3Template` uses `JsonMapper` based `Jackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
If Jackson 3 is not on classpath and Jackson 2 is, `S3Template` uses `ObjectMapper` based `LegacyJackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
This behavior can be overwritten by providing custom bean of type `S3ObjectConverter`.

=== Determining S3 Objects Content Type
Expand Down
7 changes: 5 additions & 2 deletions docs/src/main/asciidoc/sns.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The starter automatically configures and registers a `SnsTemplate` bean providin
It supports sending notifications with payload of type:

* `String` - using `org.springframework.messaging.converter.StringMessageConverter`
* `Object` - which gets serialized to JSON using `org.springframework.messaging.converter.MappingJackson2MessageConverter` and Jackson's `com.fasterxml.jackson.databind.ObjectMapper` autoconfigured by Spring Boot.
* `Object` - which if Jackson 3 is on classpath gets serialized to JSON using `org.springframework.messaging.converter.JacksonJsonMessageConverter` and Jackson's `tools.jackson.databind.json.JsonMapper` autoconfigured by Spring Boot.
* `Object` - which if Jackson 3 is not on classpath but Jackson 2 is gets serialized to JSON using `org.springframework.messaging.converter.MappingJackson2MessageConverter` and Jackson's `com.fasterxml.jackson.databind.ObjectMapper` autoconfigured by Spring Boot.

Additionally, it exposes handful of methods supporting `org.springframework.messaging.Message`.

Expand Down Expand Up @@ -305,7 +306,9 @@ The `SnsInboundChannelAdapter` is an extension of `HttpRequestHandlingMessagingG
Its URL must be used from the AWS Management Console to add this endpoint as a subscriber to the SNS Topic.
However, before receiving any notification itself, this HTTP endpoint must confirm the subscription.

See `SnsInboundChannelAdapter` JavaDocs for more information.
If you want to use Jackson 3 with spring cloud aws integrations you should use `SnsInboundChannelAdapter`. If you are still using Jackson 2 you should use `LegacyJackson2SnsInboundChannelAdapter`.

See `SnsInboundChannelAdapter` and `LegacyJackson2SnsInboundChannelAdapter` JavaDocs for more information.

An important option of this adapter to consider is `handleNotificationStatus`.
This `boolean` flag indicates if the adapter should send `SubscriptionConfirmation/UnsubscribeConfirmation` message to the `output-channel` or not.
Expand Down
45 changes: 33 additions & 12 deletions docs/src/main/asciidoc/sqs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ For convenience, the `additionalInformation` parameters can be found as constant
Message conversion by default is handled by a `SqsMessagingMessageConverter` instance, which contains:

* `SqsHeaderMapper` for mapping headers to and from `messageAttributes`
* `CompositeMessageConverter` with a `StringMessageConverter` and a `MappingJackson2MessageConverter` for converting payloads to and from JSON.
* `CompositeMessageConverter` with a `StringMessageConverter` and a `JacksonJsonMessageConverter` for converting payloads to and from JSON.

NOTE: `SqsMessagingMessageConverter` is using Jackson 3 under the hood. If you are interested in using Jackson 2 you should be configuring `LegacyJackson2SqsMessagingMessageConverter`.
When `LegacyJackson2SqsMessagingMessageConverter` is used you have to configure `MappingJackson2MessageConverter`.

A custom `MessagingMessageConverter` implementation can be provided in the `SqsTemplate.builder()`:

Expand All @@ -344,7 +347,20 @@ SqsOperations template = SqsTemplate
.builder()
.sqsAsyncClient(sqsAsyncClient)
.configureDefaultConverter(converter -> {
converter.setObjectMapper(objectMapper);
converter.setHeaderMapper(headerMapper);
converter.setPayloadTypeHeader("my-custom-type-header");
}
)
.buildSyncTemplate();
```

The Jackson 2 specific `LegacyJackson2SqsMessagingMessageConverter` instance can also be configured in the builder:

```java
SqsOperations template = SqsTemplate
.builder()
.sqsAsyncClient(sqsAsyncClient)
.configureLegacyJackson2DefaultConverter(converter -> {
converter.setHeaderMapper(headerMapper);
converter.setPayloadTypeHeader("my-custom-type-header");
}
Expand Down Expand Up @@ -1603,11 +1619,14 @@ public SqsMessageListenerContainerFactory<Object> defaultSqsListenerContainerFac
----
=== Message Conversion and Payload Deserialization

Payloads are automatically deserialized from `JSON` for `@SqsListener` annotated methods using a `MappingJackson2MessageConverter`.
Payloads are automatically deserialized from `JSON` for `@SqsListener` annotated methods using a `JacksonJsonMessageConverter` when Jackson 3 is on classpath. If there is no Jackson 3 on classpath and there is Jackson 2 on classpath `MappingJackson2MessageConverter` will be used.

NOTE: When using Spring Boot's auto-configuration, if there's a single `JsonMapper` in Spring Context, such object mapper will be used for converting messages.
This includes the one provided by Spring Boot's auto-configuration itself.
For configuring a different `JsonMapper`, see <<Global Configuration for @SqsListeners>>.

NOTE: When using Spring Boot's auto-configuration, if there's a single `ObjectMapper` in Spring Context, such object mapper will be used for converting messages.
NOTE: When Jackson 3 is not on classpath and only Jackson 2 is found, if there's a single `ObjectMapper` in Spring Context, such object mapper will be used for converting messages.
This includes the one provided by Spring Boot's auto-configuration itself.
For configuring a different `ObjectMapper`, see <<Global Configuration for @SqsListeners>>.

For manually created `MessageListeners`, `MessageInterceptor` and `ErrorHandler` components, or more fine-grained conversion such as using `interfaces` or `inheritance` in listener methods, type mapping is required for payload deserialization.

Expand Down Expand Up @@ -1642,7 +1661,9 @@ It is also possible not to include payload type information in the header by usi
More complex mapping can be achieved by using the `setPayloadTypeMapper` method, which overrides the default header-based mapping.
This method receives a `Function<Message<?>, Class<?>> payloadTypeMapper` that will be applied to incoming messages.

The default `MappingJackson2MessageConverter` can be replaced by using the `setPayloadMessageConverter` method.
The default `JacksonJsonMessageConverter` can be replaced by using the `setPayloadMessageConverter` method.

`MappingJackson2MessageConverter` can also be replaced by using the `setPayloadMessageConverter` method.

The framework also provides the `SqsHeaderMapper`, which implements the `HeaderMapper` interface and is invoked by the `SqsMessagingMessageConverter`.
To provide a different `HeaderMapper` implementation, use the `setHeaderMapper` method.
Expand All @@ -1669,7 +1690,7 @@ headerMapper.setAdditionalHeadersFunction(((sqsMessage, accessor) -> {
messageConverter.setHeaderMapper(headerMapper);

// Configure Payload Converter
MappingJackson2MessageConverter payloadConverter = new MappingJackson2MessageConverter();
JacksonJsonMessageConverter payloadConverter = new JacksonJsonMessageConverter();
payloadConverter.setPrettyPrint(true);
messageConverter.setPayloadMessageConverter(payloadConverter);

Expand Down Expand Up @@ -1925,22 +1946,22 @@ The following attributes can be configured in the registrar:
- `setMessageHandlerMethodFactory` - provide a different factory to be used to create the `invocableHandlerMethod` instances that wrap the listener methods.
- `setListenerContainerRegistry` - provide a different `MessageListenerContainerRegistry` implementation to be used to register the `MessageListenerContainers`
- `setMessageListenerContainerRegistryBeanName` - provide a different bean name to be used to retrieve the `MessageListenerContainerRegistry`
- `setObjectMapper` - set the `ObjectMapper` instance that will be used to deserialize payloads in listener methods.
- `setJacksonMessageConverterFactory` - set the `AbstractMessageConverterFactory` which will be used to construct `MessageConverter` and provide either `JsonMapper` for Jackson 3 or `ObjectMapper` for Jackson 2. Check `JacksonJsonMessageConverterFactory` and `LegacyJackson2MessageConverterFactory`
See <<Message Conversion and Payload Deserialization>> for more information on where this is used.
- `setValidator` - set the `Validator` instance that will be used for payload validation in listener methods.
- `manageMessageConverters` - gives access to the list of message converters that will be used to convert messages.
By default, `StringMessageConverter`, `SimpleMessageConverter` and `MappingJackson2MessageConverter` are used.
By default, `StringMessageConverter`, `SimpleMessageConverter` and `JacksonJsonMessageConverter` are used.

- `manageArgumentResolvers` - gives access to the list of argument resolvers that will be used to resolve the listener method arguments.
The order of resolvers is important - `PayloadMethodArgumentResolver` should generally be last since it's used as default.

A simple example would be:
A simple example for Jackson 3 would be:

[source, java]
----
@Bean
SqsListenerConfigurer configurer(ObjectMapper objectMapper) {
return registrar -> registrar.setObjectMapper(objectMapper);
SqsListenerConfigurer configurer(JacksonJsonMessageConverterFactory factory) {
return registrar -> registrar.setJacksonMessageConverterFactory(factory);
}
----

Expand Down
5 changes: 5 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
<artifactId>spring-cloud-aws-ses</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-sns</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@
import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import io.awspring.cloud.s3.InMemoryBufferingS3OutputStreamProvider;
import io.awspring.cloud.s3.Jackson2JsonS3ObjectConverter;
import io.awspring.cloud.s3.PropertiesS3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3ObjectConverter;
import io.awspring.cloud.s3.S3Operations;
import io.awspring.cloud.s3.S3OutputStreamProvider;
import io.awspring.cloud.s3.S3ProtocolResolver;
import io.awspring.cloud.s3.S3Template;
import io.awspring.cloud.s3.*;
import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
Expand All @@ -49,6 +45,7 @@
import software.amazon.awssdk.services.s3.S3ClientBuilder;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.encryption.s3.S3EncryptionClient;
import tools.jackson.databind.json.JsonMapper;

/**
* {@link AutoConfiguration} for {@link S3Client} and {@link S3ProtocolResolver}.
Expand Down Expand Up @@ -124,11 +121,50 @@ else if (awsProperties.getEndpoint() != null) {
return builder.build();
}

@Bean
@ConditionalOnMissingBean
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new InMemoryBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

@Conditional(S3EncryptionConditional.class)
@ConditionalOnClass(name = "software.amazon.encryption.s3.S3EncryptionClient")
@Configuration
public static class S3EncryptionConfiguration {

private static void configureEncryptionProperties(S3Properties properties,
ObjectProvider<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> aesProvider,
S3EncryptionClient.Builder builder) {
PropertyMapper propertyMapper = PropertyMapper.get();
var encryptionProperties = properties.getEncryption();

propertyMapper.from(encryptionProperties::isEnableDelayedAuthenticationMode)
.to(builder::enableDelayedAuthenticationMode);
propertyMapper.from(encryptionProperties::isEnableLegacyUnauthenticatedModes)
.to(builder::enableLegacyUnauthenticatedModes);
propertyMapper.from(encryptionProperties::isEnableMultipartPutObject).to(builder::enableMultipartPutObject);

if (!StringUtils.hasText(properties.getEncryption().getKeyId())) {
if (aesProvider.getIfAvailable() != null) {
builder.aesKey(aesProvider.getObject().generateSecretKey());
}
else {
builder.rsaKeyPair(rsaProvider.getObject().generateKeyPair());
}
}
else {
propertyMapper.from(encryptionProperties::getKeyId).to(builder::kmsKeyId);
}
}

@Bean
@ConditionalOnMissingBean
S3Client s3EncryptionClient(S3EncryptionClient.Builder s3EncryptionBuilder, S3ClientBuilder s3ClientBuilder) {
Expand All @@ -154,55 +190,28 @@ S3EncryptionClient.Builder s3EncrpytionClientBuilder(S3Properties properties,
configureEncryptionProperties(properties, rsaProvider, aesProvider, builder);
return builder;
}
}

private static void configureEncryptionProperties(S3Properties properties,
ObjectProvider<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> aesProvider,
S3EncryptionClient.Builder builder) {
PropertyMapper propertyMapper = PropertyMapper.get();
var encryptionProperties = properties.getEncryption();

propertyMapper.from(encryptionProperties::isEnableDelayedAuthenticationMode)
.to(builder::enableDelayedAuthenticationMode);
propertyMapper.from(encryptionProperties::isEnableLegacyUnauthenticatedModes)
.to(builder::enableLegacyUnauthenticatedModes);
propertyMapper.from(encryptionProperties::isEnableMultipartPutObject).to(builder::enableMultipartPutObject);
@Configuration
@AutoConfigureAfter(Jackson2JsonS3ObjectConverterConfiguration.class)
@ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
static class LegacyJackson2JsonS3ObjectConverterConfiguration {

if (!StringUtils.hasText(properties.getEncryption().getKeyId())) {
if (aesProvider.getIfAvailable() != null) {
builder.aesKey(aesProvider.getObject().generateSecretKey());
}
else {
builder.rsaKeyPair(rsaProvider.getObject().generateKeyPair());
}
}
else {
propertyMapper.from(encryptionProperties::getKeyId).to(builder::kmsKeyId);
}
@ConditionalOnMissingBean
@Bean
S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
return new LegacyJackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new));
}
}

@Bean
@ConditionalOnMissingBean
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}

@Configuration
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnClass(name = "tools.jackson.databind.json.JsonMapper")
static class Jackson2JsonS3ObjectConverterConfiguration {

@ConditionalOnMissingBean
@Bean
S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
return new Jackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new));
S3ObjectConverter s3ObjectConverter(Optional<JsonMapper> jsonMapper) {
return new Jackson2JsonS3ObjectConverter(jsonMapper.orElseGet(JsonMapper::new));
}
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new InMemoryBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}
}
Loading