AWS S3 client
The s3-client-aws integration is independent of the experimental Kora S3 client. Add either artifact on its own, or use both when you need generated object operations and the full AWS SDK API.
AWS SDK client¶
The s3-client-aws artifact is a thin integration around the
AWS SDK for Java v2. It publishes the SDK's own
software.amazon.awssdk.services.s3.S3Client as a graph component, configures it from the Kora
configuration and routes its HTTP traffic through Kora's HttpClient so that timeouts and telemetry
stay consistent with the rest of the application.
It contains no @S3 annotations and no Kora S3 models — working with this artifact means working
with the AWS SDK API. Use it for everything the declarative contract does not cover: bucket
administration, object copying, presigned URLs, ACL and policy management, batch deletion.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Note that the group is io.koraframework, not io.koraframework.experimental — this artifact is not
an experimental module. Requires any HTTP client module to be added: the SDK's
own apache-client and netty-nio-client transports are excluded, and Kora supplies an
SdkHttpClient implementation backed by the graph's HttpClient.
Configuration¶
Configuration is read from the s3client.aws path. Basic parameters:
s3client.aws {
url = "http://localhost:9000" //(1)!
credentials {
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
}
region = "aws-global" //(4)!
}
URLof theS3storage (required, no default)S3access key (required, no default)S3secret key (required, no default)S3storage region (default:aws-global)
s3client:
aws:
url: "http://localhost:9000" #(1)!
credentials:
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
URLof theS3storage (required, no default)S3access key (required, no default)S3secret key (required, no default)S3storage region (default:aws-global)
Full Configuration
Complete configuration described in the AwsS3Config class (example values or default values are specified):
s3client.aws {
url = "http://localhost:9000" //(1)!
credentials {
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
}
region = "aws-global" //(4)!
addressStyle = "PATH" //(5)!
requestTimeout = "45s" //(6)!
checksumCalculationRequest = "WHEN_REQUIRED" //(7)!
checksumValidationResponse = "WHEN_REQUIRED" //(8)!
chunkedEncodingEnabled = true //(9)!
telemetry {
logging {
enabled = false //(10)!
}
metrics {
enabled = false //(11)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(12)!
tags = { // (13)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(14)!
attributes = { // (15)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
URLof theS3storage, passed to the SDK as an endpoint override (required, no default)S3access key (required, no default)S3secret key (required, no default)S3storage region (default:aws-global)- Object access style, can have values
PATHorVIRTUAL_HOSTED(default:PATH) - Maximum operation execution time, applied to the underlying
HTTPrequest (default:45s) - When request checksums are calculated, can have values
WHEN_SUPPORTEDorWHEN_REQUIRED(default:WHEN_REQUIRED) - When response checksums are validated, can have values
WHEN_SUPPORTEDorWHEN_REQUIRED(default:WHEN_REQUIRED) - Whether to use chunked encoding when signing object data during upload (default:
true) - Enables module logging (default:
false) - Enables module metrics (default:
false) - Configures SLO for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Configures metric tags (default:
{}) - Enables module tracing (default:
true) - Configures tracing attributes (default:
{})
s3client:
aws:
url: "http://localhost:9000" #(1)!
credentials:
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
addressStyle: "PATH" #(5)!
requestTimeout: "45s" #(6)!
checksumCalculationRequest: "WHEN_REQUIRED" #(7)!
checksumValidationResponse: "WHEN_REQUIRED" #(8)!
chunkedEncodingEnabled: true #(9)!
telemetry:
logging:
enabled: false #(10)!
metrics:
enabled: false #(11)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(12)!
tags: #(13)!
key1: value1
key2: value2
tracing:
enabled: true #(14)!
attributes: #(15)!
key1: value1
key2: value2
URLof theS3storage, passed to the SDK as an endpoint override (required, no default)S3access key (required, no default)S3secret key (required, no default)S3storage region (default:aws-global)- Object access style, can have values
PATHorVIRTUAL_HOSTED(default:PATH) - Maximum operation execution time, applied to the underlying
HTTPrequest (default:45s) - When request checksums are calculated, can have values
WHEN_SUPPORTEDorWHEN_REQUIRED(default:WHEN_REQUIRED) - When response checksums are validated, can have values
WHEN_SUPPORTEDorWHEN_REQUIRED(default:WHEN_REQUIRED) - Whether to use chunked encoding when signing object data during upload (default:
true) - Enables module logging (default:
false) - Enables module metrics (default:
false) - Configures SLO for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Configures metric tags (default:
{}) - Enables module tracing (default:
true) - Configures tracing attributes (default:
{})
The bucket name is not part of AwsS3Config: the SDK takes it on every request, so declare it in your
own @ConfigSource interface.
Usage¶
The SDK client is injected directly, without a tag:
@Component
public final class AwsS3Service {
private final S3Client s3Client; //(1)!
private final String bucket;
public AwsS3Service(S3Client s3Client, S3Config config) {
this.s3Client = s3Client;
this.bucket = config.bucket();
}
public PutObjectResponse putObject(String key, byte[] value) {
return s3Client.putObject(r -> r.bucket(bucket).key(key), RequestBody.fromBytes(value));
}
public ResponseInputStream<GetObjectResponse> getObject(String key) {
return s3Client.getObject(r -> r.bucket(bucket).key(key));
}
public ListObjectsV2Response listObjects(String prefix) {
return s3Client.listObjectsV2(r -> r.bucket(bucket).prefix(prefix).maxKeys(50));
}
public DeleteObjectsResponse deleteObjects(List<String> keys) { //(2)!
var identifiers = keys.stream()
.map(key -> ObjectIdentifier.builder().key(key).build())
.toList();
return s3Client.deleteObjects(r -> r.bucket(bucket).delete(d -> d.objects(identifiers)));
}
}
software.amazon.awssdk.services.s3.S3Client, the AWS SDK client- Batch deletion, which the declarative client does not generate
@Component
class AwsS3Service(
private val s3Client: S3Client, //(1)!
config: S3Config
) {
private val bucket: String = config.bucket()
fun putObject(key: String, value: ByteArray): PutObjectResponse =
s3Client.putObject({ it.bucket(bucket).key(key) }, RequestBody.fromBytes(value))
fun getObject(key: String): ResponseInputStream<GetObjectResponse> =
s3Client.getObject { it.bucket(bucket).key(key) }
fun listObjects(prefix: String): ListObjectsV2Response =
s3Client.listObjectsV2 { it.bucket(bucket).prefix(prefix).maxKeys(50) }
fun deleteObjects(keys: List<String>): DeleteObjectsResponse { //(2)!
val identifiers = keys.map { ObjectIdentifier.builder().key(it).build() }
return s3Client.deleteObjects { r -> r.bucket(bucket).delete { d -> d.objects(identifiers) } }
}
}
software.amazon.awssdk.services.s3.S3Client, the AWS SDK client- Batch deletion, which the declarative client does not generate
Failures are reported by the SDK's own exception hierarchy — NoSuchKeyException,
NoSuchBucketException, S3Exception — not by the Kora S3 exceptions.
Custom SDK behaviour is added with ExecutionInterceptor components: every
software.amazon.awssdk.core.interceptor.ExecutionInterceptor published under
@Tag(Tag.Factory.class) is registered on the client alongside Kora's own telemetry interceptor.
Using both artifacts¶
Both artifacts can live in one application: they occupy different packages and different configuration sections, so there is nothing to reconcile.
s3client.aws { //(1)!
url = ${S3_URL}
credentials {
accessKey = ${S3_ACCESS_KEY}
secretKey = ${S3_SECRET_KEY}
}
}
s3client.uploads { //(2)!
endpoint = ${S3_URL}
bucket = "uploads"
credentials {
accessKey = ${S3_ACCESS_KEY}
secretKey = ${S3_SECRET_KEY}
}
}
- Fixed path of the AWS SDK client
- Path declared in
@S3.Client("s3client.uploads")of the declarative client
s3client:
aws: #(1)!
url: ${S3_URL}
credentials:
accessKey: ${S3_ACCESS_KEY}
secretKey: ${S3_SECRET_KEY}
uploads: #(2)!
endpoint: ${S3_URL}
bucket: "uploads"
credentials:
accessKey: ${S3_ACCESS_KEY}
secretKey: ${S3_SECRET_KEY}
- Fixed path of the AWS SDK client
- Path declared in
@S3.Client("s3client.uploads")of the declarative client
Bucket administration¶
Creating a bucket or checking that it exists is not part of the @S3 contract, so it goes through the
AWS SDK client. The bucket name from @S3.Bucket ends up in a generated class rather than in a
component, so the initializer reads the same configuration path itself:
@Root //(1)!
@Component
public final class S3BucketInitializer implements Lifecycle {
private final S3Client s3Client; //(2)!
private final S3UploadsConfig config;
public S3BucketInitializer(S3Client s3Client, S3UploadsConfig config) {
this.s3Client = s3Client;
this.config = config;
}
@Override
public void init() {
var bucket = this.config.bucket();
try {
this.s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build());
} catch (NoSuchBucketException e) {
this.s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build());
}
}
@Override
public void release() {}
}
- Required: nothing depends on this component, so without
@Rootit is dropped from the graph software.amazon.awssdk.services.s3.S3Client
@Root //(1)!
@Component
class S3BucketInitializer(
private val s3Client: S3Client, //(2)!
private val config: S3UploadsConfig
) : Lifecycle {
override fun init() {
val bucket = config.bucket()
try {
s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build())
} catch (e: NoSuchBucketException) {
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build())
}
}
override fun release() {}
}
- Required: nothing depends on this component, so without
@Rootit is dropped from the graph software.amazon.awssdk.services.s3.S3Client
A Lifecycle component nobody depends on is dropped
Kora builds only the part of the graph that is reachable. A component that just prepares external
state and is not a dependency of anything is removed together with everything it pulled in,
which surfaces as a build error such as
interface software.amazon.awssdk.services.s3.S3Client wasn't found in graph.
Mark it @Root to keep it.