S3 клиент
Kora предоставляет два независимых артефакта для S3. Общего у них только протокол: разные координаты Maven, разные пакеты, разные секции конфигурации, разные API. Выберите тот, который соответствует желаемому способу работы с S3-совместимым объектным хранилищем, либо подключите оба, если нужны оба.
| Артефакт | Что предоставляет | Когда использовать |
|---|---|---|
io.koraframework.experimental:s3-client-kora |
Декларативные интерфейсы @S3.Client и императивный контракт S3Client, реализованные поверх собственного HTTP-клиента Kora |
Нужны сгенерированные на этапе компиляции клиенты с телеметрией для операций над объектами |
io.koraframework:s3-client-aws |
Клиент AWS SDK software.amazon.awssdk.services.s3.S3Client, опубликованный как компонент графа |
Нужна вся поверхность AWS SDK: администрирование бакетов, копирование, предподписанные URL, ACL |
Два разных типа S3Client
Оба артефакта предоставляют тип с именем S3Client, и они никак не связаны:
io.koraframework.s3.client.kora.S3Client — это императивный контракт Kora, а
software.amazon.awssdk.services.s3.S3Client — клиент AWS SDK. Импортируйте нужный —
их путаница приводит к непонятным ошибкам сборки графа «no factory found».
Если в classpath оказался не тот артефакт, компиляция падает с сообщениями вида
package S3 does not exist или package io.koraframework.s3.client.model does not exist:
аннотации @S3 и все типы моделей есть только в s3-client-kora.
Если нужен пошаговый разбор перед справочным описанием, смотрите S3.
Kora клиент¶
Экспериментальный модуль
Экспериментальный модуль полностью работает и протестирован, но требует дополнительной апробации и аналитики использования, поэтому API потенциально может претерпеть незначительные изменения до того, как станет полностью стабильным.
Артефакт s3-client-kora реализует REST API S3 напрямую поверх собственного HTTP-клиента Kora.
Он предоставляет декларативные интерфейсы @S3.Client, императивный контракт S3Client, модели запросов/ответов
и телеметрию. От AWS SDK он не зависит.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Требуется добавить любой модуль HTTP-клиента, поскольку S3-клиент выполняет запросы
через компонент HttpClient из графа — например
Apache HttpClient, OkHttp
или нативный JDK-клиент.
Конфигурация¶
Каждый декларативный клиент читает собственную конфигурацию S3ClientConfig по пути, объявленному в
@S3.Client. Основные параметры:
s3client.someClient {
endpoint = "http://localhost:9000" //(1)!
credentials {
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
}
region = "aws-global" //(4)!
}
- Адрес (
URL) хранилищаS3(обязательный, по умолчанию не указано) - Ключ доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Секрет доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Регион хранилища
S3(по умолчанию:aws-global)
s3client:
someClient:
endpoint: "http://localhost:9000" #(1)!
credentials:
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
- Адрес (
URL) хранилищаS3(обязательный, по умолчанию не указано) - Ключ доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Секрет доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Регион хранилища
S3(по умолчанию:aws-global)
Полная конфигурация
Полная конфигурация описана в классах S3ClientConfig и S3ClientConfigWithCredentials
(указаны примеры значений или значения по умолчанию):
s3client.someClient {
endpoint = "http://localhost:9000" //(1)!
credentials {
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
}
region = "aws-global" //(4)!
addressStyle = "PATH" //(5)!
requestTimeout = "45s" //(6)!
upload {
partSize = "5MiB" //(7)!
chunkSize = "64KiB" //(8)!
singlePartUploadLimit = "100MiB" //(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"
}
}
}
}
- Адрес (
URL) хранилищаS3(обязательный, по умолчанию не указано) - Ключ доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Секрет доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Регион хранилища
S3, используется при подписании запросов (по умолчанию:aws-global) - Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции, передаётся в нижележащий
HTTP-запрос (по умолчанию:45s) - Размер части при многочастной загрузке тела типа
InputStream(по умолчанию:5MiB) - Размер чанка кодирования
aws-chunked, когда телом являетсяContentWriter(по умолчанию:64KiB) - Размер объекта, начиная с которого многочастная загрузка предпочтительнее загрузки одним запросом (по умолчанию:
100MiB) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
s3client:
someClient:
endpoint: "http://localhost:9000" #(1)!
credentials:
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
addressStyle: "PATH" #(5)!
requestTimeout: "45s" #(6)!
upload:
partSize: "5MiB" #(7)!
chunkSize: "64KiB" #(8)!
singlePartUploadLimit: "100MiB" #(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
- Адрес (
URL) хранилищаS3(обязательный, по умолчанию не указано) - Ключ доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Секрет доступа к
S3(обязательный, если не каждый метод клиента принимает аргументS3Credentials) - Регион хранилища
S3, используется при подписании запросов (по умолчанию:aws-global) - Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции, передаётся в нижележащий
HTTP-запрос (по умолчанию:45s) - Размер части при многочастной загрузке тела типа
InputStream(по умолчанию:5MiB) - Размер чанка кодирования
aws-chunked, когда телом являетсяContentWriter(по умолчанию:64KiB) - Размер объекта, начиная с которого многочастная загрузка предпочтительнее загрузки одним запросом (по умолчанию:
100MiB) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Имя бакета (bucket) не входит в S3ClientConfig. Оно определяется отдельно через
@S3.Bucket и может указывать на любой путь конфигурации.
Метрики модуля описаны в разделе Справочник по метрикам.
Декларативный клиент¶
Для создания декларативного клиента используются специальные аннотации:
@S3.Client— помечает интерфейс как декларативный S3-клиент@S3.Bucket— объявляет, откуда берётся имя бакета@S3.Get— метод выполняет операцию получения объекта@S3.Head— метод выполняет операцию получения метаданных объекта@S3.List— метод выполняет операцию получения списка объектов@S3.Put— метод выполняет операцию добавления объекта@S3.Delete— метод выполняет операцию удаления объекта
Все аннотации находятся в пакете io.koraframework.s3.client.kora.annotation.
На один метод допускается ровно одна аннотация операции.
@S3.Client можно поставить только на интерфейс; классы процессор отклоняет с ошибкой
@S3.Client can only be applied to an interface.
Конфигурация клиента¶
Значение value аннотации @S3.Client — это путь конфигурации, из которого читается S3ClientConfig:
Путь клиента без значения
@S3.Client без значения не означает «корень конфигурации».
Процессор подставляет простое имя интерфейса, поэтому @S3.Client на
interface SomeClient читает конфигурацию по пути SomeClient. Всегда указывайте явный путь
вида @S3.Client("s3client.someClient") — так настройки каждого клиента остаются разделены,
а файл конфигурации — читаемым.
У каждого клиента своя секция конфигурации, поэтому два клиента в одном приложении могут работать с двумя разными хранилищами:
s3client {
documents {
endpoint = ${DOCUMENTS_S3_URL}
bucket = "documents"
credentials {
accessKey = ${DOCUMENTS_S3_ACCESS_KEY}
secretKey = ${DOCUMENTS_S3_SECRET_KEY}
}
}
avatars {
endpoint = ${AVATARS_S3_URL}
bucket = "avatars"
credentials {
accessKey = ${AVATARS_S3_ACCESS_KEY}
secretKey = ${AVATARS_S3_SECRET_KEY}
}
}
}
Бакет¶
Имя бакета никогда не берётся из конфигурации клиента неявно — оно всегда объявляется
через @S3.Bucket. Есть три способа его задать, они проверяются в таком порядке:
- Параметр метода с аннотацией
@S3.Bucket— бакет передаётся во время выполнения @S3.Bucket("config.path")на методе — бакет читается из конфигурации для этого метода@S3.Bucket("config.path")на интерфейсе — бакет читается из конфигурации для всех методов
Путь, начинающийся с точки, относителен пути клиента, объявленного в @S3.Client;
путь без ведущей точки — абсолютный путь в конфигурации.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket") //(1)!
public interface SomeClient {
@S3.Get
GetObjectResult fromDefaultBucket(String key);
@S3.Get
@S3.Bucket("s3client.archive.bucket") //(2)!
GetObjectResult fromArchiveBucket(String key);
@S3.Get
GetObjectResult fromRuntimeBucket(@S3.Bucket String bucket, String key); //(3)!
}
- Относительный путь: разрешается в
s3client.someClient.bucket - Абсолютный путь: разрешается в
s3client.archive.bucket - Бакет передаётся во время выполнения; параметр не входит в ключ объекта
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket") //(1)!
interface SomeClient {
@S3.Get
fun fromDefaultBucket(key: String): GetObjectResult
@S3.Get
@S3.Bucket("s3client.archive.bucket") //(2)!
fun fromArchiveBucket(key: String): GetObjectResult
@S3.Get
fun fromRuntimeBucket(@S3.Bucket bucket: String, key: String): GetObjectResult //(3)!
}
- Относительный путь: разрешается в
s3client.someClient.bucket - Абсолютный путь: разрешается в
s3client.archive.bucket - Бакет передаётся во время выполнения; параметр не входит в ключ объекта
Конфигурация для примера выше:
Бакет обязателен
Метод, у которого нет ни одного источника бакета, падает на этапе компиляции с ошибкой
S3 operation '...' has no bucket source. Аннотация @S3.Bucket без значения на типе или методе
падает с ошибкой S3 bucket config path is missing: пустое значение осмысленно только на параметре.
Имя бакета генерируется, а не внедряется
Имя бакета из конфигурации попадает в сгенерированный класс $SomeClient_BucketsConfig, а не в
внедряемый компонент. Код, которому нужно то же имя бакета в другом месте — например, инициализатор
бакета — читает тот же путь конфигурации самостоятельно через @ConfigSource, смотрите
Администрирование бакетов.
Учётные данные¶
По умолчанию учётные данные берутся из секции credentials конфигурации клиента.
Метод может вместо этого принимать параметр S3Credentials и подписывать им конкретный вызов:
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Get
GetObjectResult withConfigCredentials(String key); //(1)!
@S3.Get
GetObjectResult withCallCredentials(S3Credentials credentials, String key); //(2)!
}
- Использует
credentials { accessKey, secretKey }из конфигурации клиента - Использует учётные данные, переданные во время выполнения; параметр не входит в ключ объекта
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Get
fun withConfigCredentials(key: String): GetObjectResult //(1)!
@S3.Get
fun withCallCredentials(credentials: S3Credentials, key: String): GetObjectResult //(2)!
}
- Использует
credentials { accessKey, secretKey }из конфигурации клиента - Использует учётные данные, переданные во время выполнения; параметр не входит в ключ объекта
Значение S3Credentials создаётся статической фабрикой:
Тип конфигурации клиента выбирает процессор: если каждый метод интерфейса принимает параметр
S3Credentials, клиент привязывается к S3ClientConfig, и секция credentials не нужна вовсе.
Если хотя бы один метод её не принимает, клиент привязывается к S3ClientConfigWithCredentials,
и секция credentials { accessKey, secretKey } становится обязательной.
Получение файла¶
В разделе описана операция получения объекта с помощью декларативного S3-клиента.
Для указания операции предлагается использовать аннотацию @S3.Get.
Тип возвращаемого значения — либо GetObjectResult, то есть «сырой» ответ,
тело которого читается потоком, либо byte[] / ByteArray, и тогда клиент вычитывает всё тело в память
и закрывает ответ за вас.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Get //(1)!
GetObjectResult operation1(String key); //(2)!
@S3.Get
byte[] operation2(String key); //(3)!
@S3.Get("some-key") //(4)!
GetObjectResult operation3();
}
- Операция получения объекта
- Полный ответ с потоковым телом; закрывать его должен вызывающий код
- Объект целиком вычитывается в память, ответ закрывает сам клиент
- Ключ объекта можно указать в аннотации
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Get //(1)!
fun operation1(key: String): GetObjectResult //(2)!
@S3.Get
fun operation2(key: String): ByteArray //(3)!
@S3.Get("some-key") //(4)!
fun operation3(): GetObjectResult
}
- Операция получения объекта
- Полный ответ с потоковым телом; закрывать его должен вызывающий код
- Объект целиком вычитывается в память, ответ закрывает сам клиент
- Ключ объекта можно указать в аннотации
GetObjectResult является HttpClientResponse и потому Closeable. Чтение тела означает закрытие
и самого ответа, и тела:
Любой другой тип возвращаемого значения — ошибка компиляции:
S3 operation '@S3.Get' on method '...' has unsupported return type.
Шаблон ключа¶
Ключ можно задать в виде шаблона и подставлять в него аргументы метода; все ключевые аргументы метода должны быть частью шаблона.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Get("prefix-{key1}-{key2}-suffix") //(1)!
GetObjectResult operation(String key1, int key2); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все ключевые аргументы метода должны быть частью шаблона ключа
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Get("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int): GetObjectResult //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все ключевые аргументы метода должны быть частью шаблона ключа
Параметры, которые не являются частями ключа и потому никогда не попадают в шаблон:
- параметр с аннотацией
@S3.Bucket - параметр типа
S3Credentials - параметр типа аргументов —
GetObjectArgs,HeadObjectArgs,ListObjectsArgs,PutObjectArgs,DeleteObjectArgs - параметр тела —
byte[],ByteBuffer,InputStream,S3Client.ContentWriter
Без шаблона метод должен иметь ровно один ключевой параметр, и ключом становится его String.valueOf().
Неоднозначные случаи процессор отклоняет с явными сообщениями:
| Ситуация | Ошибка компиляции |
|---|---|
| Несколько ключевых параметров и нет шаблона | has N key parameters, but no key template |
| Нет ключевого параметра и нет константного ключа | has no object key |
| Шаблон ссылается на неизвестный параметр | references key template parameter '{x}', but the method has no matching key parameter |
| Шаблон без закрывающей фигурной скобки | has malformed key template ...: missing closing '}' |
| Коллекция или map в качестве параметра шаблона | uses '{x}' in the key template, but parameter 'x' is a collection or map |
| Коллекция в качестве единственного ключа | expects one object key, but parameter 'x' is a collection |
Необязательный ответ¶
Если отсутствие объекта — нормальный исход, а не ошибка, пометьте метод как nullable.
В Java используется аннотация @Nullable из JSpecify, в Kotlin — nullable-тип
возвращаемого значения. Такая операция возвращает null вместо выбрасывания S3ClientNoSuchKeyException.
То же самое работает для результатов byte[] / ByteArray операции @S3.Get.
Аргументы запроса¶
Операция может принимать объект аргументов, который несёт всё, что поддерживает
API S3 помимо бакета и ключа — условные заголовки, идентификатор версии, переопределение заголовков ответа,
параметры серверного шифрования и диапазон байт:
Объекты аргументов — изменяемые контейнеры с публичными полями:
Диапазон байт¶
GetObjectArgs и HeadObjectArgs принимают Range, который отображается в
HTTP-заголовок Range. Range — это sealed-интерфейс
с тремя фабриками:
| Фабрика | Смысл | Значение заголовка |
|---|---|---|
Range.fromTo(first, last) |
Байты с first по last включительно |
bytes=first-last |
Range.from(first) |
Байты с first до конца объекта |
bytes=first- |
Range.last(bytes) |
Последние bytes байт объекта |
bytes=-bytes |
var args = new GetObjectArgs();
args.range = Range.fromTo(0, 1023); //(1)!
try (var response = client.operation("video.mp4", args)) {
var range = response.contentRange(); //(2)!
}
- Первый килобайт объекта
ContentRange(firstPosition, lastPosition, completeLength), разобранный из заголовкаContent-Range
Метаданные¶
Операция @S3.Head получает метаданные объекта, не передавая его тело, поэтому она значительно дешевле
получения объекта. Единственный поддерживаемый тип ответа —
HeadObjectResult.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Head //(1)!
HeadObjectResult operation1(String key);
@S3.Head("some-key") //(2)!
HeadObjectResult operation2();
@S3.Head
HeadObjectResult operation3(String key, HeadObjectArgs args); //(3)!
}
- Операция получения метаданных
- Ключ объекта можно указать в аннотации
- Поддерживает ту же механику объекта аргументов, что и
@S3.Get
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Head //(1)!
fun operation1(key: String): HeadObjectResult
@S3.Head("some-key") //(2)!
fun operation2(): HeadObjectResult
@S3.Head
fun operation3(key: String, args: HeadObjectArgs): HeadObjectResult //(3)!
}
- Операция получения метаданных
- Ключ объекта можно указать в аннотации
- Поддерживает ту же механику объекта аргументов, что и
@S3.Get
У HEAD нет тела ответа
На HEAD-запрос отсутствующего объекта S3 отвечает кодом 404 и пустым телом, поэтому код ошибки
хранилища прочитать невозможно. И отсутствующий объект, и отсутствующий бакет проявляются как сбой вида
S3ClientNoSuchKeyException с errorCode равным NoSuchKey —
не используйте @S3.Head, чтобы отличить одно от другого.
Получение списка файлов¶
В разделе описана операция получения списка объектов с помощью декларативного S3-клиента.
Для указания операции предлагается использовать аннотацию @S3.List.
Префикс ключа может быть
константой в аннотации, шаблоном, построенным из аргументов метода, либо полем параметра
ListObjectsArgs.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.List
ListBucketResult operation1(String prefix); //(1)!
@S3.List("some-prefix-") //(2)!
ListBucketResult operation2();
@S3.List("prefix-{key}-") //(3)!
ListBucketResult operation3(String key);
}
- Префикс передан как аргумент метода, поскольку он не указан в аннотации
- Константный префикс, указанный в аннотации
- Префикс, построенный по шаблону
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.List
fun operation1(prefix: String): ListBucketResult //(1)!
@S3.List("some-prefix-") //(2)!
fun operation2(): ListBucketResult
@S3.List("prefix-{key}-") //(3)!
fun operation3(key: String): ListBucketResult
}
- Префикс передан как аргумент метода, поскольку он не указан в аннотации
- Константный префикс, указанный в аннотации
- Префикс, построенный по шаблону
Поддерживаемые типы возвращаемых значений:
| Тип возвращаемого значения | Описание |
|---|---|
ListBucketResult |
Одна страница списка вместе с keyCount, commonPrefixes и токеном продолжения |
List<ListBucketResult.ListBucketItem> |
Элементы одной страницы |
List<String> |
Ключи одной страницы |
Iterator<ListBucketResult.ListBucketItem> |
Ленивый итератор по всем страницам |
Iterator<String> |
Ленивый итератор по ключам всех страниц |
Любой другой тип возвращаемого значения — ошибка компиляции с перечислением ровно этих пяти вариантов.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.List
List<String> keys(String prefix); //(1)!
@S3.List
List<ListBucketResult.ListBucketItem> items(String prefix); //(2)!
}
- Только ключи объектов первой страницы
- Полные элементы первой страницы с размером,
ETagи временем изменения
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.List
fun keys(prefix: String): List<String> //(1)!
@S3.List
fun items(prefix: String): List<ListBucketResult.ListBucketItem> //(2)!
}
- Только ключи объектов первой страницы
- Полные элементы первой страницы с размером,
ETagи временем изменения
Операции получения списка нужен источник префикса
Без значения аннотации, без ключевого параметра и без параметра ListObjectsArgs процессор падает
с ошибкой S3 operation '...' has no object key. Если цель — перечислить весь бакет, передайте
ListObjectsArgs с явно пустым префиксом.
Шаблон префикса¶
Префикс также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все ключевые аргументы метода должны быть частью шаблона.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.List("prefix-{key1}-{key2}-") //(1)!
ListBucketResult operation(String key1, int key2);
}
- Шаблон, используемый для построения префикса: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках}
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.List("prefix-{key1}-{key2}-") //(1)!
fun operation(key1: String, key2: Int): ListBucketResult
}
- Шаблон, используемый для построения префикса: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках}
Аргументы списка¶
Параметр ListObjectsArgs полностью заменяет префикс, задаваемый аннотацией: объект передаётся
в хранилище как есть, поэтому префикс, размер страницы и токен продолжения берутся из него.
Ручная постраничная выборка через ListBucketResult:
var args = new ListObjectsArgs();
args.prefix = "2024/";
args.maxKeys = 100; //(1)!
ListBucketResult page;
do {
page = client.operation(args);
for (var item : page.items()) {
// ...
}
args.continuationToken = page.nextContinuationToken(); //(2)!
} while (args.continuationToken != null);
- Размер страницы; независимо от значения хранилище возвращает не более
1000ключей за запрос null, когда прочитана последняя страница
val args = ListObjectsArgs()
args.prefix = "2024/"
args.maxKeys = 100 //(1)!
do {
val page = client.operation(args)
for (item in page.items()) {
// ...
}
args.continuationToken = page.nextContinuationToken() //(2)!
} while (args.continuationToken != null)
- Размер страницы; независимо от значения хранилище возвращает не более
1000ключей за запрос null, когда прочитана последняя страница
Разделитель¶
Разделитель группирует ключи
с общим префиксом до указанного символа — именно так в S3 эмулируются «каталоги».
Он задаётся через ListObjectsArgs.delimiter, а сгруппированные префиксы возвращаются в
ListBucketResult.commonPrefixes():
var args = new ListObjectsArgs();
args.prefix = "documents/";
args.delimiter = "/"; //(1)!
var result = client.operation(args);
var directories = result.commonPrefixes(); //(2)!
var files = result.items(); //(3)!
- Сгруппировать всё, что находится ниже
documents/, по следующему/ - Псевдокаталоги вида
documents/2024/ - Объекты, лежащие непосредственно в
documents/
val args = ListObjectsArgs()
args.prefix = "documents/"
args.delimiter = "/" //(1)!
val result = client.operation(args)
val directories = result.commonPrefixes() //(2)!
val files = result.items() //(3)!
- Сгруппировать всё, что находится ниже
documents/, по следующему/ - Псевдокаталоги вида
documents/2024/ - Объекты, лежащие непосредственно в
documents/
Итераторы¶
Возврат Iterator полностью скрывает постраничную выборку: клиент запрашивает следующую страницу только
после того, как текущая исчерпана. Это рекомендуемый способ обойти большой бакет, не держа весь список
в памяти.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.List
Iterator<String> allKeys(String prefix); //(1)!
@S3.List
Iterator<ListBucketResult.ListBucketItem> allItems(ListObjectsArgs args); //(2)!
}
- Лениво перебирает ключи всех страниц
- Лениво перебирает элементы всех страниц
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.List
fun allKeys(prefix: String): Iterator<String> //(1)!
@S3.List
fun allItems(args: ListObjectsArgs): Iterator<ListBucketResult.ListBucketItem> //(2)!
}
- Лениво перебирает ключи всех страниц
- Лениво перебирает элементы всех страниц
Добавление файла¶
В разделе описана операция добавления объекта с помощью декларативного S3-клиента.
Для операции предлагается использовать аннотацию @S3.Put.
Метод должен объявлять ровно один параметр тела и возвращать либо String — ETag, сообщённый
хранилищем, — либо void / Unit.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Put
String operation1(String key, //(1)!
byte[] body); //(2)!
@S3.Put("some-key") //(3)!
void operation2(byte[] body); //(4)!
}
- Ключ объекта, по которому он будет добавлен в хранилище
- Тело объекта, которое будет загружено
- Ключ также можно указать в аннотации, если он статический
void, когдаETagне нужен
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Put
fun operation1(key: String, //(1)!
body: ByteArray): String //(2)!
@S3.Put("some-key") //(3)!
fun operation2(body: ByteArray) //(4)!
}
- Ключ объекта, по которому он будет добавлен в хранилище
- Тело объекта, которое будет загружено
- Ключ также можно указать в аннотации, если он статический
Unit, когдаETagне нужен
Любой другой тип возвращаемого значения падает на этапе компиляции с ошибкой
S3 operation '@S3.Put' on method '...' has unsupported return type, ожидающей String or void.
Тело файла¶
Требуется ровно один параметр тела, и его тип определяет способ загрузки:
| Тип тела | Тип в Kotlin | Стратегия загрузки |
|---|---|---|
byte[] |
ByteArray |
Одиночный PUT всего массива |
ByteBuffer |
ByteBuffer |
Одиночный PUT remaining() байт; heap-буфер используется напрямую, direct-буфер сначала копируется |
InputStream |
InputStream |
Многочастная загрузка, управляемая параметром upload.partSize |
S3Client.ContentWriter |
S3Client.ContentWriter |
Потоковый PUT с кодированием aws-chunked, размер чанка из upload.chunkSize |
ContentWriter — способ передать потоком тело известной длины, не материализуя его:
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Put("some-key")
String operation(S3Client.ContentWriter body);
}
var file = Path.of("/tmp/report.pdf");
var etag = client.operation(new S3Client.ContentWriter() {
@Override
public void write(OutputStream os) throws IOException { //(1)!
Files.copy(file, os);
}
@Override
public long length() { //(2)!
return Files.size(file);
}
});
- Вызывается клиентом во время записи тела запроса
- Точная длина содержимого, необходимая для подписания запроса
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Put("some-key")
fun operation(body: S3Client.ContentWriter): String
}
val file = Path.of("/tmp/report.pdf")
val etag = client.operation(object : S3Client.ContentWriter {
override fun write(os: OutputStream) { //(1)!
Files.copy(file, os)
}
override fun length(): Long = Files.size(file) //(2)!
})
- Вызывается клиентом во время записи тела запроса
- Точная длина содержимого, необходимая для подписания запроса
Тип тела
Метод @S3.Put без параметра тела падает с ошибкой has no upload body parameter, а метод более чем
с одним параметром тела — с ошибкой has N upload body parameters, but only one body parameter is supported.
Неподдерживаемый тип тела падает с ошибкой has unsupported body type.
Шаблон ключа¶
Ключ также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все ключевые аргументы метода должны быть частью шаблона.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Put("prefix-{key1}-{key2}-suffix") //(1)!
String operation(String key1, int key2, byte[] body); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Параметр тела никогда не входит в шаблон ключа
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Put("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int, body: ByteArray): String //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Параметр тела никогда не входит в шаблон ключа
Тип и кодировка содержимого¶
Content-Type, Content-Encoding и все остальные заголовки загрузки задаются через параметр
PutObjectArgs, а не через аннотацию:
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Put("prefix-{key}")
String operation(String key, PutObjectArgs args, byte[] body);
}
var args = new PutObjectArgs();
args.contentType = "image/jpeg"; //(1)!
args.contentEncoding = "gzip"; //(2)!
args.storageClass = "STANDARD_IA"; //(3)!
args.tagging = "project=reports&owner=team"; //(4)!
var etag = client.operation("avatar", args, bytes);
Content-TypeобъектаContent-Encodingобъекта- Класс хранения объекта
- Теги объекта в формате
URL-запроса
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Put("prefix-{key}")
fun operation(key: String, args: PutObjectArgs, body: ByteArray): String
}
val args = PutObjectArgs()
args.contentType = "image/jpeg" //(1)!
args.contentEncoding = "gzip" //(2)!
args.storageClass = "STANDARD_IA" //(3)!
args.tagging = "project=reports&owner=team" //(4)!
val etag = client.operation("avatar", args, bytes)
Content-TypeобъектаContent-Encodingобъекта- Класс хранения объекта
- Теги объекта в формате
URL-запроса
Многочастная загрузка¶
Тело типа InputStream загружается многочастной загрузкой без дополнительного кода. Клиент читает поток
кусками по upload.partSize; если весь поток укладывается в первый кусок, он отправляется одним PUT,
и многочастная загрузка вообще не начинается.
- Размер части для загрузки из
InputStream, по спецификацииS3должен быть от5MiBдо5GiB(по умолчанию:5MiB)
Параметр PutObjectArgs учитывается и здесь: его значения транслируются в аргументы
CreateMultipartUpload и CompleteMultipartUpload.
Если при чтении потока происходит IOException, клиент перевыбрасывает его как
S3ClientUnknownException.
Удаление файла¶
В разделе описана операция удаления объекта с помощью декларативного S3-клиента.
Для операции предлагается использовать аннотацию @S3.Delete. Тип возвращаемого значения должен быть
void / Unit.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Delete //(1)!
void operation1(String key); //(2)!
@S3.Delete("some-key") //(3)!
void operation2();
@S3.Delete
void operation3(String key, DeleteObjectArgs args); //(4)!
}
- Операция удаления объекта
- Ключ удаляемого объекта
- Ключ объекта можно указать в аннотации
- Идентификатор версии,
MFAи параметры условного удаления
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Delete //(1)!
fun operation1(key: String) //(2)!
@S3.Delete("some-key") //(3)!
fun operation2()
@S3.Delete
fun operation3(key: String, args: DeleteObjectArgs) //(4)!
}
- Операция удаления объекта
- Ключ удаляемого объекта
- Ключ объекта можно указать в аннотации
- Идентификатор версии,
MFAи параметры условного удаления
Удаление несуществующего объекта — как и объекта в несуществующем бакете — не является ошибкой:
S3 отвечает на такой DELETE успехом.
В декларативном клиенте нет пакетного удаления
@S3.Delete всегда генерирует запрос DeleteObject для одного объекта. Метод вида
void operation(List<String> keys) не поддерживается и падает на этапе компиляции, поскольку
коллекция не может быть ключом объекта. Пакетное удаление доступно через
императивный клиент — S3Client#deleteObjects — либо через
клиент AWS SDK.
Шаблон ключа¶
Ключ также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все ключевые аргументы метода должны быть частью шаблона.
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
public interface SomeClient {
@S3.Delete("prefix-{key1}-{key2}-suffix") //(1)!
void operation(String key1, int key2); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все ключевые аргументы метода должны быть частью шаблона ключа
@S3.Client("s3client.someClient")
@S3.Bucket(".bucket")
interface SomeClient {
@S3.Delete("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int) //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
String.valueOf(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все ключевые аргументы метода должны быть частью шаблона ключа
Пользовательская фабрика¶
Каждый сгенерированный клиент запрашивает из графа S3ClientFactory и создаёт через неё свой S3Client.
Атрибут factoryTag аннотации @S3.Client заставляет клиент запрашивать помеченную тегом фабрику
вместо фабрики по умолчанию — так одно приложение может держать клиенты на разных транспортах или
с разной настройкой телеметрии:
@S3.Client(value = "s3client.someClient", factoryTag = SomeClient.CustomFactory.class) //(1)!
@S3.Bucket(".bucket")
public interface SomeClient {
final class CustomFactory {} //(2)!
@S3.Get
GetObjectResult operation(String key);
}
- Сгенерированному модулю требуется
@Tag(SomeClient.CustomFactory.class) S3ClientFactory - В качестве маркера тега подходит любой класс
@KoraApp
public interface Application extends KoraS3ClientModule {
@Tag(SomeClient.CustomFactory.class)
default S3ClientFactory customS3ClientFactory(@Tag(CustomS3.class) HttpClient httpClient, //(1)!
S3ClientTelemetryFactory telemetryFactory) {
return (configPath, clientImpl, config) -> {
var telemetry = telemetryFactory.get(configPath, clientImpl, config.telemetry());
return new KoraS3Client(httpClient, config, telemetry);
};
}
}
- Отдельный
HTTP-клиент только для этогоS3-клиента
@S3.Client(value = "s3client.someClient", factoryTag = SomeClient.CustomFactory::class) //(1)!
@S3.Bucket(".bucket")
interface SomeClient {
class CustomFactory //(2)!
@S3.Get
fun operation(key: String): GetObjectResult
}
- Сгенерированному модулю требуется
@Tag(SomeClient.CustomFactory::class) S3ClientFactory - В качестве маркера тега подходит любой класс
@KoraApp
interface Application : KoraS3ClientModule {
@Tag(SomeClient.CustomFactory::class)
fun customS3ClientFactory(@Tag(CustomS3::class) httpClient: HttpClient, //(1)!
telemetryFactory: S3ClientTelemetryFactory): S3ClientFactory =
S3ClientFactory { configPath, clientImpl, config ->
val telemetry = telemetryFactory.get(configPath, clientImpl, config.telemetry())
KoraS3Client(httpClient, config, telemetry)
}
}
- Отдельный
HTTP-клиент только для этогоS3-клиента
Без factoryTag используется фабрика S3ClientFactory по умолчанию из KoraS3ClientModule: она берёт
HttpClient из графа и подключает телеметрию модуля.
Сигнатуры¶
Доступные из коробки сигнатуры методов декларативного S3-клиента:
Под T подразумевается тип возвращаемого значения.
T myMethod()
Под T подразумевается тип возвращаемого значения, либо T?, либо Unit.
fun myMethod(): T
S3-клиенты синхронные. Сигнатур с CompletionStage, CompletableFuture, Mono или Flux нет,
а suspend-функция отклоняется процессором символов с явной ошибкой
Suspend methods are not supported by the S3 client generator. Выполняйте блокирующие операции
параллельно средствами платформы, например на виртуальных потоках.
Модели¶
Все типы моделей находятся в пакете io.koraframework.s3.client.kora.model и его подпакетах request / response.
GetObjectResult¶
Результат операции получения объекта. Расширяет HttpClientResponse, поэтому предоставляет
«сырой» HTTP-ответ и требует закрытия:
| Метод | Описание |
|---|---|
int code() |
Код статуса HTTP ответа |
HttpHeaders headers() |
Заголовки HTTP-ответа |
HttpBodyInput body() |
Тело объекта, читается через asInputStream() |
ContentRange contentRange() |
Разобранный заголовок Content-Range, осмысленный только для ranged-запроса |
void close() |
Освобождает ответ и нижележащее соединение |
ContentRange — это record с полями firstPosition, lastPosition и completeLength.
Метод contentRange() выбрасывает IllegalArgumentException, если в ответе нет заголовка Content-Range.
HeadObjectResult¶
Результат операции получения метаданных, record с лениво разбираемыми заголовками:
| Метод | Описание |
|---|---|
String bucket() |
Бакет, которому принадлежит объект |
String key() |
Ключ объекта |
long size() |
Размер объекта в байтах |
HttpHeaders headers() |
Все заголовки ответа |
String etag() |
Значение заголовка ETag |
String versionId() |
Значение заголовка x-amz-version-id |
Instant lastModified() |
Разобранный заголовок Last-Modified либо null, если заголовка нет |
ListBucketResult¶
Результат операции получения списка:
| Метод | Описание |
|---|---|
List<ListBucketItem> items() |
Объекты текущей страницы |
int keyCount() |
Количество ключей, возвращённых на этой странице |
List<String> commonPrefixes() |
Сгруппированные префиксы, если использовался разделитель, иначе null |
String nextContinuationToken() |
Токен следующей страницы либо null на последней странице |
ListBucketResult.ListBucketItem — это record, описывающий один объект:
| Компонент | Описание |
|---|---|
String bucket() |
Бакет, которому принадлежит объект |
String key() |
Ключ объекта |
String etag() |
ETag объекта |
String checksumType() |
Тип контрольной суммы, сообщённый хранилищем |
String checksumAlgorithm() |
Алгоритм контрольной суммы, сообщённый хранилищем |
Instant lastModified() |
Время последнего изменения |
long size() |
Размер объекта в байтах |
String storageClass() |
Класс хранения, null если не сообщён |
ListBucketItemOwner owner() |
Владелец (displayName, id), null если не задан fetchOwner |
Аргументы запроса¶
Объекты аргументов — обычные изменяемые классы с публичными полями, по одному на параметр запроса S3.
Их можно передавать в любую подходящую декларативную операцию или в императивный клиент.
| Тип | Значимые поля |
|---|---|
GetObjectArgs |
range, versionId, partNumber, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince, responseContentType, responseContentDisposition, responseContentEncoding, responseContentLanguage, responseCacheControl, responseExpires, sseCustomerAlgorithm, sseCustomerKey, checksumMode, requestPayer, expectedBucketOwner |
HeadObjectArgs |
Тот же набор полей, что и у GetObjectArgs |
ListObjectsArgs |
prefix, delimiter, maxKeys, continuationToken, startAfter, fetchOwner, optionalObjectAttributes, requestPayer, expectedBucketOwner |
PutObjectArgs |
contentType, contentEncoding, contentDisposition, contentLanguage, cacheControl, expires, acl, storageClass, tagging, ifMatch, ifNoneMatch, serverSideEncryption, sseCustomerAlgorithm, sseCustomerKey, sseKmsKeyId, sseKmsEncryptionContext, bucketKeyEnabled, objectLockMode, objectLockRetainUntilDate, objectLockLegalHoldStatus, grantRead, grantReadAcp, grantWriteAcp, grantFullControl, websiteRedirectLocation, requestPayer, expectedBucketOwner |
DeleteObjectArgs |
versionId, mfa, bypassGovernanceRetention, ifMatch, ifMatchLastModifiedTime, ifMatchSize, requestPayer, expectedBucketOwner |
Типы для многочастной загрузки — CreateMultipartUploadArgs, UploadPartArgs, CompleteMultipartUploadArgs,
AbortMultipartUploadArgs, ListPartsArgs и ListMultipartUploadsArgs — устроены так же и используются
императивным клиентом.
Методы CreateMultipartUploadArgs.from(PutObjectArgs) и CompleteMultipartUploadArgs.from(PutObjectArgs)
преобразуют аргументы загрузки для многочастного сценария — именно это делает сгенерированный клиент
для тела типа InputStream.
Императивный клиент¶
io.koraframework.s3.client.kora.S3Client — это низкоуровневый контракт, на котором построен каждый
декларативный клиент. Он покрывает весь объектный API плюс многочастные загрузки, принимает credentials,
bucket и key явно в каждом вызове и полезен для операций, которые декларативный контракт не выражает —
пакетное удаление или многочастная загрузка, управляемая вручную.
Модуль не публикует компонент S3Client: создайте его из внедряемой фабрики S3ClientFactory.
@KoraApp
public interface Application extends KoraS3ClientModule {
default S3ClientConfigWithCredentials someS3ClientConfig(Config config, //(1)!
ConfigValueMapper<S3ClientConfigWithCredentials> mapper) {
return mapper.mapOrThrow(config.get("s3client.someClient"));
}
default S3Client someS3Client(S3ClientFactory factory, //(2)!
S3ClientConfigWithCredentials config) {
return factory.create(config);
}
}
- Читает ту же структуру
S3ClientConfig, что читал бы декларативный клиент - Фабрика подключает
HttpClientи телеметрию из графа
@KoraApp
interface Application : KoraS3ClientModule {
fun someS3ClientConfig(config: Config, //(1)!
mapper: ConfigValueMapper<S3ClientConfigWithCredentials>): S3ClientConfigWithCredentials =
mapper.mapOrThrow(config.get("s3client.someClient"))
fun someS3Client(factory: S3ClientFactory, //(2)!
config: S3ClientConfigWithCredentials): S3Client =
factory.create(config)
}
- Читает ту же структуру
S3ClientConfig, что читал бы декларативный клиент - Фабрика подключает
HttpClientи телеметрию из графа
Операции¶
| Метод | Описание |
|---|---|
headObject(credentials, bucket, key[, args]) |
Метаданные объекта, выбрасывает S3ClientNoSuchKeyException, если объекта нет |
headObjectOptional(credentials, bucket, key) |
Метаданные объекта либо null, если объекта нет |
getObject(credentials, bucket, key[, args]) |
Объект вместе с телом, выбрасывает исключение, если объекта нет |
getObjectOptional(credentials, bucket, key) |
Объект вместе с телом либо null, если объекта нет |
putObject(credentials, bucket, key[, args], data, off, len) |
Загружает диапазон байт массива, возвращает ETag |
putObject(credentials, bucket, key[, args], contentWriter) |
Потоковая загрузка aws-chunked, возвращает ETag |
deleteObject(credentials, bucket, key[, args]) |
Удаляет один объект |
deleteObjects(credentials, bucket, keys) |
Удаляет до 1000 объектов одним запросом |
listObjectsV2(credentials, bucket, args) |
Одна страница списка |
listObjectsV2Iterator(credentials, bucket, args) |
Ленивый итератор, подгружающий страницы по мере потребления |
createMultipartUpload(credentials, bucket, key[, args]) |
Начинает многочастную загрузку, возвращает её идентификатор |
uploadPart(credentials, bucket, key, uploadId, partNumber, ...) |
Загружает одну часть, возвращает UploadedPart |
listParts(credentials, bucket, key, uploadId, ...) |
Перечисляет уже загруженные части |
completeMultipartUpload(credentials, bucket, key, uploadId, parts[, args]) |
Собирает объект из частей, возвращает ETag |
abortMultipartUpload(credentials, bucket, key, uploadId[, args]) |
Прерывает многочастную загрузку и освобождает место, занятое её частями |
listMultipartUploads(credentials, bucket, args) |
Перечисляет начатые, но не завершённые многочастные загрузки |
@Component
public final class SomeService {
private final S3Client s3;
private final S3Credentials credentials;
public SomeService(S3Client s3, S3ClientConfigWithCredentials config) {
this.s3 = s3;
this.credentials = config.credentials();
}
public byte[] download(String bucket, String key) throws IOException {
try (var response = s3.getObject(credentials, bucket, key); //(1)!
var body = response.body();
var stream = body.asInputStream()) {
return stream.readAllBytes();
}
}
public void deleteAll(String bucket, List<String> keys) {
s3.deleteObjects(credentials, bucket, keys); //(2)!
}
}
- Выбрасывает
S3ClientNoSuchKeyException, если объект отсутствует - Выбрасывает
S3ClientDeleteException, если хранилище сообщило о сбоях по отдельным объектам
@Component
class SomeService(
private val s3: S3Client,
config: S3ClientConfigWithCredentials
) {
private val credentials = config.credentials()
fun download(bucket: String, key: String): ByteArray {
s3.getObject(credentials, bucket, key).use { response -> //(1)!
response.body().asInputStream().use { stream ->
return stream.readAllBytes()
}
}
}
fun deleteAll(bucket: String, keys: List<String>) {
s3.deleteObjects(credentials, bucket, keys) //(2)!
}
}
- Выбрасывает
S3ClientNoSuchKeyException, если объект отсутствует - Выбрасывает
S3ClientDeleteException, если хранилище сообщило о сбоях по отдельным объектам
Многочастная загрузка¶
Многочастная загрузка, управляемая вручную — именно её генерирует декларативный клиент для тела
типа InputStream:
var uploadId = s3.createMultipartUpload(credentials, bucket, key); //(1)!
var parts = new ArrayList<UploadedPart>();
try {
var buffer = new byte[5 * 1024 * 1024];
for (int number = 1; ; number++) {
var read = source.readNBytes(buffer, 0, buffer.length);
if (read > 0) {
parts.add(s3.uploadPart(credentials, bucket, key, uploadId, number, buffer, 0, read)); //(2)!
}
if (read < buffer.length) {
break;
}
}
var etag = s3.completeMultipartUpload(credentials, bucket, key, uploadId, parts); //(3)!
} catch (Exception e) {
s3.abortMultipartUpload(credentials, bucket, key, uploadId); //(4)!
throw e;
}
- Начинает загрузку и возвращает её идентификатор
- Нумерация частей начинается с
1; каждая часть, кроме последней, должна быть не меньше5MiB - Собирает объект и возвращает его
ETag - Освобождает место, занятое уже загруженными частями
val uploadId = s3.createMultipartUpload(credentials, bucket, key) //(1)!
val parts = ArrayList<UploadedPart>()
try {
val buffer = ByteArray(5 * 1024 * 1024)
var number = 1
while (true) {
val read = source.readNBytes(buffer, 0, buffer.size)
if (read > 0) {
parts.add(s3.uploadPart(credentials, bucket, key, uploadId, number, buffer, 0, read)) //(2)!
}
if (read < buffer.size) {
break
}
number++
}
val etag = s3.completeMultipartUpload(credentials, bucket, key, uploadId, parts) //(3)!
} catch (e: Exception) {
s3.abortMultipartUpload(credentials, bucket, key, uploadId) //(4)!
throw e
}
- Начинает загрузку и возвращает её идентификатор
- Нумерация частей начинается с
1; каждая часть, кроме последней, должна быть не меньше5MiB - Собирает объект и возвращает его
ETag - Освобождает место, занятое уже загруженными частями
Исключения¶
Если операция клиента завершается неудачей, выбрасывается одно из исключений S3 из пакета
io.koraframework.s3.client.kora.exception. Все они наследуются от абстрактного S3ClientException,
который, в свою очередь, расширяет RuntimeException, поэтому их обработка необязательна и не проверяется компилятором.
Иерархия исключений:
RuntimeException
└── S3ClientException
├── S3ClientResponseException
│ └── S3ClientErrorException
│ └── S3ClientNoSuchKeyException
├── S3ClientDeleteException
└── S3ClientUnknownException
Пример обработки:
@Component
public final class SomeService {
private final SomeClient client;
public SomeService(SomeClient client) {
this.client = client;
}
public void call(String key) {
try {
client.deleteObject(key);
} catch (S3ClientNoSuchKeyException e) {
// Object is missing: e.getErrorCode() is NoSuchKey
} catch (S3ClientErrorException e) {
// Storage reported an error document: e.getErrorCode(), e.getErrorMessage(), e.getRequestId()
} catch (S3ClientResponseException e) {
// Unexpected HTTP status without a parsable error document: e.getHttpCode()
} catch (S3ClientException e) {
// Any other client failure
}
}
}
@Component
class SomeService(
private val client: SomeClient
) {
fun call(key: String) {
try {
client.deleteObject(key)
} catch (e: S3ClientNoSuchKeyException) {
// Object is missing: e.errorCode is NoSuchKey
} catch (e: S3ClientErrorException) {
// Storage reported an error document: e.errorCode, e.errorMessage, e.requestId
} catch (e: S3ClientResponseException) {
// Unexpected HTTP status without a parsable error document: e.httpCode
} catch (e: S3ClientException) {
// Any other client failure
}
}
}
S3ClientNoSuchKeyException¶
Выбрасывается, когда запрошенный объект не существует, а операция не является
необязательной. Расширяет S3ClientErrorException, поэтому getErrorCode() возвращает NoSuchKey.
Рекомендации:
- Сделайте результат
@S3.Getили@S3.Headнеобязательным, если отсутствие объекта — нормальный исход - Проверьте бакет, полученный через
@S3.Bucket, и запрошенный ключ - Помните, что
HEAD-запрос к отсутствующему бакету тоже сообщаетNoSuchKey, потому что у ответа нет тела
S3ClientDeleteException¶
Выбрасывается методом S3Client#deleteObjects, когда хранилище сообщает о сбоях по отдельным объектам.
Предоставляет список отдельных сбоев:
| Метод | Описание |
|---|---|
List<Error> getErrors() |
Сбои по каждому объекту, каждый с key(), code(), message() |
Рекомендации:
- Изучите
getErrors(), чтобы определить, какие объекты не удалось обработать и почему - Повторите неудавшиеся ключи отдельно, если сбой временный
S3ClientErrorException¶
Выбрасывается, когда хранилище ответило статусом ошибки и разбираемым документом ошибки S3.
| Метод | Описание |
|---|---|
int getHttpCode() |
Код статуса HTTP ответа |
String getErrorCode() |
Код ошибки хранилища (например, AccessDenied) |
String getErrorMessage() |
Сообщение об ошибке хранилища |
String getRequestId() |
Идентификатор запроса, сообщённый хранилищем, может быть null |
Рекомендации:
- Логируйте
getErrorCode(),getErrorMessage()иgetRequestId()для диагностики — именно идентификатор запроса нужен операторам хранилища SignatureDoesNotMatchиInvalidAccessKeyIdозначают, что неверны учётные данные илиregion
S3ClientResponseException¶
Базовый класс для всех сбоев, несущих статус HTTP: хранилище ответило, но либо неожиданным статусом,
либо телом, которое не является документом ошибки S3.
| Метод | Описание |
|---|---|
int getHttpCode() |
Код статуса HTTP ответа |
Рекомендации:
403обычно означает неверные учётные данные или отсутствующую политику- Включите логирование клиента и телеметрию
HTTP-клиента, чтобы изучить нижележащие запрос и ответ
S3ClientUnknownException¶
Выбрасывается, когда операция завершилась неудачей до или вне обмена по HTTP — IOException при чтении
загружаемого потока, сбой транспорта, ошибка разбора XML. Исходный сбой всегда доступен как cause.
S3ClientException¶
Абстрактный базовый класс всех исключений выше. Ловите его последней веткой catch, чтобы единообразно
обработать любую ошибку хранилища или клиента.
Клиент AWS SDK¶
Артефакт s3-client-aws — это тонкая интеграция вокруг
AWS SDK for Java v2. Он публикует собственный клиент SDK
software.amazon.awssdk.services.s3.S3Client как компонент графа, настраивает его из конфигурации Kora
и направляет его HTTP-трафик через HttpClient Kora, чтобы таймауты и телеметрия оставались
согласованными с остальным приложением.
В нём нет аннотаций @S3 и нет моделей Kora S3 — работа с этим артефактом означает работу
с API AWS SDK. Используйте его для всего, что не покрывает декларативный контракт: администрирование
бакетов, копирование объектов, предподписанные URL, управление ACL и политиками, пакетное удаление.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Обратите внимание, что группа здесь io.koraframework, а не io.koraframework.experimental — этот
артефакт не является экспериментальным модулем. Требуется добавить любой модуль
HTTP-клиента: собственные транспорты SDK apache-client и netty-nio-client
исключены, а Kora предоставляет реализацию SdkHttpClient поверх HttpClient из графа.
Конфигурация¶
Конфигурация читается по пути s3client.aws. Основные параметры:
s3client.aws {
url = "http://localhost:9000" //(1)!
credentials {
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
}
region = "aws-global" //(4)!
}
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
s3client:
aws:
url: "http://localhost:9000" #(1)!
credentials:
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
Полная конфигурация
Полная конфигурация описана в классе AwsS3Config (указаны примеры значений или значения по умолчанию):
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"
}
}
}
}
URLхранилищаS3, передаётся в SDK как переопределение endpoint (обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции, применяется к нижележащему
HTTP-запросу (по умолчанию:45s) - Когда вычисляются контрольные суммы запроса, может иметь значения
WHEN_SUPPORTEDилиWHEN_REQUIRED(по умолчанию:WHEN_REQUIRED) - Когда проверяются контрольные суммы ответа, может иметь значения
WHEN_SUPPORTEDилиWHEN_REQUIRED(по умолчанию:WHEN_REQUIRED) - Использовать ли частичное (chunked) кодирование при подписании данных объекта во время загрузки (по умолчанию:
true) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
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
URLхранилищаS3, передаётся в SDK как переопределение endpoint (обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции, применяется к нижележащему
HTTP-запросу (по умолчанию:45s) - Когда вычисляются контрольные суммы запроса, может иметь значения
WHEN_SUPPORTEDилиWHEN_REQUIRED(по умолчанию:WHEN_REQUIRED) - Когда проверяются контрольные суммы ответа, может иметь значения
WHEN_SUPPORTEDилиWHEN_REQUIRED(по умолчанию:WHEN_REQUIRED) - Использовать ли частичное (chunked) кодирование при подписании данных объекта во время загрузки (по умолчанию:
true) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Имя бакета не входит в AwsS3Config: SDK принимает его в каждом запросе, поэтому объявите его
в собственном интерфейсе @ConfigSource.
Использование¶
Клиент SDK внедряется напрямую, без тега:
@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, клиент AWS SDK- Пакетное удаление, которое декларативный клиент не генерирует
@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, клиент AWS SDK- Пакетное удаление, которое декларативный клиент не генерирует
Об ошибках сообщает собственная иерархия исключений SDK — NoSuchKeyException,
NoSuchBucketException, S3Exception — а не исключения Kora S3.
Пользовательское поведение SDK добавляется компонентами ExecutionInterceptor: каждый
software.amazon.awssdk.core.interceptor.ExecutionInterceptor, опубликованный под
@Tag(Tag.Factory.class), регистрируется на клиенте рядом с собственным перехватчиком телеметрии Kora.
Использование обоих артефактов¶
Оба артефакта могут жить в одном приложении: они занимают разные пакеты и разные секции конфигурации, поэтому согласовывать нечего.
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}
}
}
- Фиксированный путь клиента AWS SDK
- Путь, объявленный в
@S3.Client("s3client.uploads")декларативного клиента
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}
- Фиксированный путь клиента AWS SDK
- Путь, объявленный в
@S3.Client("s3client.uploads")декларативного клиента
Администрирование бакетов¶
Создание бакета или проверка его существования не входят в контракт @S3, поэтому выполняются через
клиент AWS SDK. Имя бакета из @S3.Bucket попадает в сгенерированный класс, а не в компонент, поэтому
инициализатор читает тот же путь конфигурации самостоятельно:
@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() {}
}
- Обязательно: от этого компонента никто не зависит, поэтому без
@Rootон будет выброшен из графа 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() {}
}
- Обязательно: от этого компонента никто не зависит, поэтому без
@Rootон будет выброшен из графа software.amazon.awssdk.services.s3.S3Client
Компонент Lifecycle, от которого никто не зависит, выбрасывается
Kora собирает только достижимую часть графа. Компонент, который лишь подготавливает внешнее состояние
и ни от чего не является зависимостью, удаляется вместе со всем, что он за собой тянул,
и это проявляется как ошибка сборки вида
interface software.amazon.awssdk.services.s3.S3Client wasn't found in graph.
Пометьте его как @Root, чтобы он остался.
Тестирование¶
Декларативные клиенты можно тестировать с помощью @KoraAppTest вместе с реальным
S3-совместимым хранилищем, запущенным в контейнере Testcontainers —
удобный вариант Minio. Параметры подключения к хранилищу передаются в конфигурацию приложения
через системные свойства:
@TestcontainersMinio(
mode = ContainerMode.PER_RUN,
bucket = @Bucket(value = SomeClientTests.BUCKET, create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD))
@KoraAppTest(Application.class)
class SomeClientTests implements KoraAppTestConfigModifier {
static final String BUCKET = "simple";
@ConnectionMinio
private MinioConnection minioConnection;
@TestComponent
private SomeClient client;
@Override
public KoraConfigModification config() {
return KoraConfigModification
.ofSystemProperty("S3_URL", minioConnection.params().uri().toString())
.withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey())
.withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey())
.withSystemProperty("S3_BUCKET", BUCKET);
}
@Test
void putAndGet() throws IOException {
var value = "value".getBytes(StandardCharsets.UTF_8);
client.putObject("k1", value);
try (var found = client.getObject("k1"); var body = found.body().asInputStream()) {
assertArrayEquals(value, body.readAllBytes());
}
}
@Test
void getMissing() {
assertThrows(S3ClientNoSuchKeyException.class, () -> client.getObject("k2"));
}
}
@TestcontainersMinio(
mode = ContainerMode.PER_RUN,
bucket = Bucket(value = [SomeClientTests.BUCKET], create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD))
@KoraAppTest(Application::class)
class SomeClientTests : KoraAppTestConfigModifier {
@ConnectionMinio
lateinit var minioConnection: MinioConnection
@TestComponent
lateinit var client: SomeClient
override fun config(): KoraConfigModification = KoraConfigModification
.ofSystemProperty("S3_URL", minioConnection.params().uri().toString())
.withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey())
.withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey())
.withSystemProperty("S3_BUCKET", BUCKET)
@Test
fun putAndGet() {
val value = "value".toByteArray(StandardCharsets.UTF_8)
client.putObject("k1", value)
client.getObject("k1").use { found ->
found.body().asInputStream().use { body ->
assertArrayEquals(value, body.readAllBytes())
}
}
}
@Test
fun getMissing() {
assertThrows(S3ClientNoSuchKeyException::class.java) { client.getObject("k2") }
}
companion object {
const val BUCKET = "simple"
}
}
Конфигурация приложения, которую использует такой тест, читает эти системные свойства: