Skip to content
Merged
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
39 changes: 39 additions & 0 deletions docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,45 @@ If `DiskBufferingS3OutputStream` behavior does not fit your needs, you can imple

Possible alternative implementations can use multi-part upload (for example with https://github.com/CI-CMG/aws-s3-outputstream[aws-s3-outputstream library)] or https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/transfer/s3/S3TransferManager.html[S3TransferManager].

=== Using S3Template

Spring Cloud AWS provides a higher abstraction on the top of `S3Client` providing methods for the most common use cases when working with S3.

On the top of self-explanatory methods for creating and deleting buckets, `S3Template` provides a simple methods for uploading and downloading files:

[source,java]
----
@Autowired
private S3Template s3Template;

InputStream is = ...
// uploading file without metadata
s3Template.upload(BUCKET, "file.txt", is);

// uploading file with metadata
s3Template.upload(BUCKET, "file.txt", is, ObjectMetadata.builder().contentType("text/plain").build());
----

`S3Template` also allows storing & retrieving Java objects.

[source,java]
----
Person p = new Person("John", "Doe");
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.
This behavior can be overwritten by providing custom bean of type `S3ObjectConverter`.

=== Determining S3 Objects Content Type

All S3 objects stored in S3 through `S3Template`, `S3Resource` or `S3OutputStream` automatically get set a `contentType` property on the S3 object metadata, based on the S3 object key (file name).

By default, `PropertiesS3ObjectContentTypeResolver` - a component supporting over 800 file extensions is responsible for content type resolution.
If this content type resolution does not meet your needs, you can provide a custom bean of type `S3ObjectContentTypeResolver` which will be automatically used in all components responsible for uploading files.

=== Configuration

The Spring Boot Starter for S3 provides the following configuration options:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
*/
package io.awspring.cloud.autoconfigure.s3;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.s3.DiskBufferingS3OutputStreamProvider;
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.crossregion.CrossRegionS3Client;
import java.util.Optional;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.ConditionalOnMissingClass;
Expand Down Expand Up @@ -64,8 +73,18 @@ S3ClientBuilder s3ClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfi

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

@Bean
@ConditionalOnMissingBean(S3Operations.class)
@ConditionalOnBean(S3ObjectConverter.class)
S3Template s3Template(S3Client s3Client, S3OutputStreamProvider s3OutputStreamProvider,
S3ObjectConverter s3ObjectConverter) {
return new S3Template(s3Client, s3OutputStreamProvider, s3ObjectConverter);
}

private S3Configuration s3ServiceConfiguration() {
Expand Down Expand Up @@ -105,4 +124,15 @@ S3Client s3Client(S3ClientBuilder s3ClientBuilder) {

}

@Configuration
@ConditionalOnClass(ObjectMapper.class)
static class Jackson2JsonS3ObjectConverterConfiguration {

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,29 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.awspring.cloud.autoconfigure.ConfiguredAwsClient;
import io.awspring.cloud.autoconfigure.core.AwsAutoConfiguration;
import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration;
import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration;
import io.awspring.cloud.s3.DiskBufferingS3OutputStreamProvider;
import io.awspring.cloud.s3.ObjectMetadata;
import io.awspring.cloud.s3.S3ObjectConverter;
import io.awspring.cloud.s3.S3OutputStream;
import io.awspring.cloud.s3.S3OutputStreamProvider;
import io.awspring.cloud.s3.S3Template;
import io.awspring.cloud.s3.crossregion.CrossRegionS3Client;
import java.io.IOException;
import java.net.URI;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
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.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3ClientBuilder;

Expand Down Expand Up @@ -82,68 +87,141 @@ void autoconfigurationIsNotTriggeredWhenS3ModuleIsNotOnClasspath() {
});
}

@Test
void byDefaultCreatesCrossRegionS3Client() {
this.contextRunner
.run(context -> assertThat(context).getBean(S3Client.class).isInstanceOf(CrossRegionS3Client.class));
}
@Nested
class S3ClientTests {
@Test
void byDefaultCreatesCrossRegionS3Client() {
contextRunner.run(
context -> assertThat(context).getBean(S3Client.class).isInstanceOf(CrossRegionS3Client.class));
}

@Test
void s3ClientCanBeOverwritten() {
this.contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> {
assertThat(context).hasSingleBean(S3Client.class);
assertThat(context).getBean(S3Client.class).isNotInstanceOf(CrossRegionS3Client.class);
});
}
@Test
void s3ClientCanBeOverwritten() {
contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> {
assertThat(context).hasSingleBean(S3Client.class);
assertThat(context).getBean(S3Client.class).isNotInstanceOf(CrossRegionS3Client.class);
});
}

@Test
void byDefaultCreatesDiskBufferingS3OutputStreamProvider() {
this.contextRunner.run(context -> assertThat(context).hasSingleBean(DiskBufferingS3OutputStreamProvider.class));
@Test
void createsStandardClientWhenCrossRegionModuleIsNotInClasspath() {
contextRunner.withClassLoader(new FilteredClassLoader(CrossRegionS3Client.class)).run(context -> {
assertThat(context).doesNotHaveBean(CrossRegionS3Client.class);
assertThat(context).hasSingleBean(S3Client.class);
});
}
}

@Test
void customS3OutputStreamProviderCanBeConfigured() {
this.contextRunner.withUserConfiguration(CustomS3OutputStreamProviderConfiguration.class)
.run(context -> assertThat(context).hasSingleBean(CustomS3OutputStreamProvider.class));
@Nested
class OutputStreamProviderTests {
@Test
void byDefaultCreatesDiskBufferingS3OutputStreamProvider() {
contextRunner.run(context -> assertThat(context).hasSingleBean(DiskBufferingS3OutputStreamProvider.class));
}

@Test
void customS3OutputStreamProviderCanBeConfigured() {
contextRunner.withUserConfiguration(CustomS3OutputStreamProviderConfiguration.class)
.run(context -> assertThat(context).hasSingleBean(CustomS3OutputStreamProvider.class));
}
}

@Test
void createsStandardClientWhenCrossRegionModuleIsNotInClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(CrossRegionS3Client.class)).run(context -> {
assertThat(context).doesNotHaveBean(CrossRegionS3Client.class);
assertThat(context).hasSingleBean(S3Client.class);
});
@Nested
class EndpointConfigurationTests {
@Test
void withCustomEndpoint() {
contextRunner.withPropertyValues("spring.cloud.aws.s3.endpoint:http://localhost:8090").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090"));
assertThat(client.isEndpointOverridden()).isTrue();
});
}

@Test
void withCustomGlobalEndpoint() {
contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090"));
assertThat(client.isEndpointOverridden()).isTrue();
});
}

@Test
void withCustomGlobalEndpointAndS3Endpoint() {
contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090",
"spring.cloud.aws.s3.endpoint:http://localhost:9999").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:9999"));
assertThat(client.isEndpointOverridden()).isTrue();
});
}
}

@Test
void withCustomEndpoint() {
this.contextRunner.withPropertyValues("spring.cloud.aws.s3.endpoint:http://localhost:8090").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090"));
assertThat(client.isEndpointOverridden()).isTrue();
});
@Nested
class S3TemplateAutoConfigurationTests {

@Test
void withJacksonOnClasspathAutoconfiguresObjectConverter() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(S3ObjectConverter.class);
assertThat(context).hasSingleBean(S3Template.class);
});
}

@Test
void withoutJacksonOnClasspathDoesNotConfigureObjectConverter() {
contextRunner.withClassLoader(new FilteredClassLoader(ObjectMapper.class)).run(context -> {
assertThat(context).doesNotHaveBean(S3ObjectConverter.class);
assertThat(context).doesNotHaveBean(S3Template.class);
});
}

@Test
void usesCustomObjectMapperBean() {
contextRunner.withUserConfiguration(CustomJacksonConfiguration.class).run(context -> {
S3ObjectConverter bean = context.getBean(S3ObjectConverter.class);
ObjectMapper objectMapper = (ObjectMapper) ReflectionTestUtils.getField(bean, "objectMapper");
assertThat(objectMapper).isEqualTo(context.getBean("customObjectMapper"));
});
}

@Test
void usesCustomS3ObjectConverter() {
contextRunner
.withUserConfiguration(CustomJacksonConfiguration.class, CustomS3ObjectConverterConfiguration.class)
.run(context -> {
S3ObjectConverter s3ObjectConverter = context.getBean(S3ObjectConverter.class);
S3ObjectConverter customS3ObjectConverter = (S3ObjectConverter) context
.getBean("customS3ObjectConverter");
assertThat(s3ObjectConverter).isEqualTo(customS3ObjectConverter);

S3Template s3Template = context.getBean(S3Template.class);

S3ObjectConverter converter = (S3ObjectConverter) ReflectionTestUtils.getField(s3Template,
"s3ObjectConverter");
assertThat(converter).isEqualTo(customS3ObjectConverter);
});
}
}

@Test
void withCustomGlobalEndpoint() {
this.contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:8090"));
assertThat(client.isEndpointOverridden()).isTrue();
});
@Configuration(proxyBeanMethods = false)
static class CustomJacksonConfiguration {
@Bean
ObjectMapper customObjectMapper() {
return new ObjectMapper();
}
}

@Test
void withCustomGlobalEndpointAndS3Endpoint() {
this.contextRunner.withPropertyValues("spring.cloud.aws.endpoint:http://localhost:8090",
"spring.cloud.aws.s3.endpoint:http://localhost:9999").run(context -> {
S3ClientBuilder builder = context.getBean(S3ClientBuilder.class);
ConfiguredAwsClient client = new ConfiguredAwsClient(builder.build());
assertThat(client.getEndpoint()).isEqualTo(URI.create("http://localhost:9999"));
assertThat(client.isEndpointOverridden()).isTrue();
});
@Configuration(proxyBeanMethods = false)
static class CustomS3ObjectConverterConfiguration {

@Bean
S3ObjectConverter customS3ObjectConverter() {
return mock(S3ObjectConverter.class);
}
}

@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -172,7 +250,6 @@ static class CustomS3OutputStreamProvider implements S3OutputStreamProvider {
public S3OutputStream create(String bucket, String key, @Nullable ObjectMetadata metadata) throws IOException {
return null;
}

}

}
Loading