S3 клиент
Экспериментальный модуль
Экспериментальный модуль полностью работает и протестирован, но требует дополнительной апробации и аналитики использования, поэтому API потенциально может претерпеть незначительные изменения до того, как станет полностью стабильным.
Модуль предоставляет слой абстракции для работы с S3-совместимым объектным хранилищем:
можно создавать декларативные S3-клиенты с помощью аннотаций либо внедрять готовые к использованию императивные клиенты.
Декларативный клиент удобен для типовых операций с объектами и ключами, тогда как императивный клиент полезен, когда операциями
нужно управлять напрямую в коде.
Если нужен пошаговый разбор перед справочным описанием, смотрите S3.
AWS¶
Реализация S3-клиента основана на библиотеке AWS.
Компоненты, доступные для внедрения:
- Императивные Kora S3-клиенты
S3Clientсинхронный AWS S3-клиентS3AsyncClientасинхронный AWS S3-клиентS3AsyncClientс тегом@Tag(MultipartUpload.class)асинхронный AWS S3-клиент для пакетной загрузки
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Требуется добавить любой модуль HTTP-клиента.
Конфигурация¶
Основные параметры конфигурации S3 клиента:
s3client {
url = "http://localhost:9000" //(1)!
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
region = "aws-global" //(4)!
}
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
s3client:
url: "http://localhost:9000" #(1)!
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
Полная конфигурация
Пример полной конфигурации, описанной в классах AwsS3ClientConfig и S3Config (указаны примеры значений или значения по умолчанию):
s3client {
aws {
addressStyle = "PATH" //(1)!
requestTimeout = "45s" //(2)!
checksumValidationEnabled = false //(3)!
chunkedEncodingEnabled = true //(4)!
upload {
bufferSize = "32MiB" //(5)!
partSize = "8MiB" //(6)!
}
}
url = "http://localhost:9000" //(7)!
accessKey = "someKey" //(8)!
secretKey = "someSecret" //(9)!
region = "aws-global" //(10)!
telemetry {
logging {
enabled = false //(11)!
}
metrics {
enabled = true //(12)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(13)!
tags = { // (14)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(15)!
attributes = { // (16)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
- Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции (по умолчанию:
45s) - Проверять ли контрольную сумму MD5 перед загрузкой и при получении из
AWS(по умолчанию:false) - Использовать ли частичное (chunked) кодирование при подписании данных файла во время загрузки в
AWS(по умолчанию:true) - Максимальный размер буфера для загрузки файлов (по умолчанию:
32MiB) - Максимальный размер части файла при загрузке одного файла (по умолчанию:
8MiB) URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настройка SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
s3client:
aws:
addressStyle: "PATH" #(1)!
requestTimeout: "45s" #(2)!
checksumValidationEnabled: false #(3)!
chunkedEncodingEnabled: true #(4)!
upload:
bufferSize: "32MiB" #(5)!
partSize: "8MiB" #(6)!
url: "http://localhost:9000" #(7)!
accessKey: "someKey" #(8)!
secretKey: "someSecret" #(9)!
region: "aws-global" #(10)!
telemetry:
logging:
enabled: false #(11)!
metrics:
enabled: true #(12)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(13)!
tags: #(14)!
key1: value1
key2: value2
tracing:
enabled: true #(15)!
attributes: #(16)!
key1: value1
key2: value2
- Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции (по умолчанию:
45s) - Проверять ли контрольную сумму MD5 перед загрузкой и при получении из
AWS(по умолчанию:false) - Использовать ли частичное (chunked) кодирование при подписании данных файла во время загрузки в
AWS(по умолчанию:true) - Максимальный размер буфера для загрузки файлов (по умолчанию:
32MiB) - Максимальный размер части файла при загрузке одного файла (по умолчанию:
8MiB) URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настройка SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Метрики модуля описаны в разделе Справочник по метрикам.
Формат ответа¶
При использовании модуля AWS можно возвращать специальные форматы ответа, специфичные для библиотеки AWS:
Для операций @S3.Get, получающих объект или метаданные, отсутствие объекта можно описать в типе ответа.
В Java поддерживаются Optional<S3Object>, Optional<S3ObjectMeta>, Optional<GetObjectResponse>,
Optional<ResponseInputStream<GetObjectResponse>> и Optional<HeadObjectResponse>.
В Kotlin для этого используются nullable-типы ответа: S3Object?, S3ObjectMeta?, GetObjectResponse?,
ResponseInputStream<GetObjectResponse>? и HeadObjectResponse?.
Minio¶
Реализация S3-клиента основана на библиотеке Minio.
Учитывайте, что реализация использует OkHttp, написанную на Kotlin, и её зависимости.
Компоненты, доступные для внедрения:
- Императивные Kora S3-клиенты
MinioClientсинхронный Minio S3-клиентMinioAsyncClientасинхронный Minio S3-клиент
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Можно добавить зависимость модуля OkHttp, иначе будет автоматически создан стандартный HTTP-клиент.
Конфигурация¶
Основные параметры конфигурации Minio S3 клиента:
s3client {
url = "http://localhost:9000" //(1)!
accessKey = "someKey" //(2)!
secretKey = "someSecret" //(3)!
region = "aws-global" //(4)!
}
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
s3client:
url: "http://localhost:9000" #(1)!
accessKey: "someKey" #(2)!
secretKey: "someSecret" #(3)!
region: "aws-global" #(4)!
URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global)
Полная конфигурация
Пример полной конфигурации, описанной в классах MinioS3ClientConfig и S3Config (указаны примеры значений или значения по умолчанию):
s3client {
minio {
addressStyle = "PATH" //(1)!
requestTimeout = "45s" //(2)!
upload {
partSize = "8MiB" //(3)!
}
}
url = "http://localhost:9000" //(4)!
accessKey = "someKey" //(5)!
secretKey = "someSecret" //(6)!
region = "aws-global" //(7)!
telemetry {
logging {
enabled = false //(8)!
}
metrics {
enabled = true //(9)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(10)!
tags = { // (11)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(12)!
attributes = { // (13)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
- Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции (по умолчанию:
45s) - Максимальный размер части файла при загрузке одного файла (по умолчанию:
8MiB) URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настройка SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
s3client:
minio:
addressStyle: "PATH" #(1)!
requestTimeout: "45s" #(2)!
upload:
partSize: "8MiB" #(3)!
url: "http://localhost:9000" #(4)!
accessKey: "someKey" #(5)!
secretKey: "someSecret" #(6)!
region: "aws-global" #(7)!
telemetry:
logging:
enabled: false #(8)!
metrics:
enabled: true #(9)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(10)!
tags: #(11)!
key1: value1
key2: value2
tracing:
enabled: true #(12)!
attributes: #(13)!
key1: value1
key2: value2
- Стиль доступа к объектам, может иметь значения
PATHилиVIRTUAL_HOSTED(по умолчанию:PATH) - Максимальное время выполнения операции (по умолчанию:
45s) - Максимальный размер части файла при загрузке одного файла (по умолчанию:
8MiB) URLхранилищаS3(обязательный, по умолчанию не указано)- Ключ доступа к
S3(обязательный, по умолчанию не указано) - Секрет доступа к
S3(обязательный, по умолчанию не указано) - Регион хранилища
S3(по умолчанию:aws-global) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настройка SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Декларативный клиент¶
Для создания декларативного клиента предлагается использовать специальные аннотации:
@S3.Client- указывает, что интерфейс является декларативным S3-клиентом@S3.Get- указывает, что метод выполняет операцию получения файла/метаданных@S3.List- указывает, что метод выполняет операцию получения списка файлов/метаданных@S3.Put- указывает, что метод выполняет операцию добавления файла@S3.Delete- указывает, что метод выполняет операцию удаления файла
Конфигурация клиента¶
Конфигурация конкретной реализации @S3.Client:
@S3.Client без аргументов эквивалентна @S3.Client(""): значение value аннотации пустое,
и S3ClientConfig будет считана из пустого пути через Config.get("").
На практике обычно лучше указывать явный путь, например @S3.Client("s3client.someClient"),
чтобы конфигурация bucket была отделена от других клиентов.
Конфигурация для случая пути s3client.someClient, описанная в классе S3ClientConfig:
Получение файла¶
В разделе описана операция получения файла/метаданных с помощью декларативного S3-клиента.
Для указания операции предлагается использовать аннотацию @S3.Get.
Метаданные¶
Операция получения файла по ключу может возвращать либо полный файл S3Object вместе с данными,
либо облегчённую версию в виде метаданных файла S3ObjectMeta без данных;
этот способ значительно быстрее, поскольку не возвращает данные файла.
Шаблон ключа¶
Ключ также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все аргументы метода должны быть частью составного ключа.
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.Get("prefix-{key1}-{key2}-suffix") //(1)!
S3Object operation(String key1, int key2); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.Get("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int): S3Object //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа
Несколько ключей¶
Также можно получать несколько файлов по ключам — либо как полные объекты с данными (S3Object),
либо как облегчённые метаданные без данных объекта (S3ObjectMeta).
Необязательный ответ¶
Если отсутствие файла не должно приводить к S3NotFoundException, результат @S3.Get можно сделать необязательным.
Для стандартных типов Kora в Java используются Optional<S3Object> и Optional<S3ObjectMeta>;
модуль AWS также поддерживает Optional<GetObjectResponse>,
Optional<ResponseInputStream<GetObjectResponse>> и Optional<HeadObjectResponse>.
В Kotlin для тех же случаев используются nullable-типы ответа.
Получение списка файлов¶
В разделе описана операция получения списка файлов/метаданных с помощью декларативного S3-клиента.
Для указания операции предлагается использовать аннотацию @S3.List.
Можно указать префикс ключа, чтобы выбрать ключи, соответствующие этому префиксу,
а также задать ограничение на выборку файлов с помощью параметра limit аннотации @S3.List.
Значение limit должно находиться в диапазоне 1..1000, значение по умолчанию — 1000.
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.List
S3ObjectList operation1(String prefix); //(1)!
@S3.List("some-prefix-") //(2)!
S3ObjectList operation2();
@S3.List(limit = 100) //(3)!
S3ObjectList operation3();
}
- префикс можно передать как аргумент метода, если он не указан в аннотации
- префикс можно указать в аннотации
- Можно указать ограничение выборки файлов для операции получения списка через
limit; допустимый диапазон —1..1000, значение по умолчанию —1000
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.List
fun operation1(prefix: String): S3ObjectList //(1)!
@S3.List("some-prefix-") //(2)!
fun operation2(): S3ObjectList
@S3.List(limit = 100) //(3)!
fun operation3(): S3ObjectList
}
- префикс можно передать как аргумент метода, если он не указан в аннотации
- префикс можно указать в аннотации
- Можно указать ограничение выборки файлов для операции получения списка через
limit; допустимый диапазон —1..1000, значение по умолчанию —1000
Метаданные¶
Операция получения списка может возвращать либо полный список файлов S3ObjectList вместе с данными,
либо облегчённую версию в виде метаданных файлов S3ObjectMetaList без данных;
этот способ значительно быстрее, поскольку не возвращает данные файлов.
Шаблон префикса¶
Префикс также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все аргументы метода должны быть частью составного ключа.
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.List("prefix-{key1}-{key2}-") //(1)!
S3ObjectList operation(String key1, int key2);
}
- Шаблон, используемый для построения префикса: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках}
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.List("prefix-{key1}-{key2}-") //(1)!
fun operation(key1: String, key2: Int): S3ObjectList
}
- Шаблон, используемый для построения префикса: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках}
Разделитель¶
Можно указать разделитель для префикса ключа, чтобы отфильтровать результат получения списка:
Добавление файла¶
В разделе описана операция добавления файла с помощью декларативного S3-клиента.
Для операции предлагается использовать аннотацию @S3.Put.
Требуется указать ключ и тело добавляемого файла:
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.Put
void operation1(String key, //(1)!
S3Body body); //(2)!
@S3.Put("some-key") //(3)!
S3ObjectUpload operation2(S3Body body);
}
- Ключ файла, по которому он будет добавлен в хранилище
- само тело файла, которое будет добавлено в хранилище
- ключ также можно указать в аннотации, если он статический
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.Put
fun operation(key: String, body: S3Body)
@S3.Put("some-key")
fun operation(body: S3Body): S3ObjectUpload
}
- Ключ файла, по которому он будет добавлен в хранилище
- само тело файла, которое будет добавлено в хранилище
- ключ также можно указать в аннотации, если он статический
Тело файла¶
Тело файла (S3Body) можно создать из byte[], ByteBuffer, InputStream или Flow.Publisher<ByteBuffer>
с помощью соответствующих статических фабричных методов. Каждый фабричный метод имеет перегрузки, дополнительно принимающие
значения type (Content-Type) и encoding (Content-Encoding):
| Фабричный метод | Источник | Размер | Описание |
|---|---|---|---|
S3Body.ofBytes(byte[]) |
byte[] |
Известен | Тело из массива байтов в памяти |
S3Body.ofBuffer(ByteBuffer) |
ByteBuffer |
Известен | Тело из буфера в памяти (в качестве размера используется remaining()) |
S3Body.ofInputStream(InputStream, long) |
InputStream |
Известен | Потоковое тело, точная длина которого передаётся явно через аргумент size |
S3Body.ofInputStreamReadAll(InputStream) |
InputStream |
Известен | Считывает весь поток в память немедленно, затем ведёт себя как массив байтов |
S3Body.ofInputStreamUnbound(InputStream) |
InputStream |
Неизвестен | Потоковое тело неизвестной длины (size() возвращает -1) |
S3Body.ofPublisher(Flow.Publisher) |
Flow.Publisher<ByteBuffer> |
Неизвестен | Реактивное потоковое тело неизвестной длины (size() возвращает -1) |
S3Body.ofPublisher(Flow.Publisher, long) |
Flow.Publisher<ByteBuffer> |
Известен | Реактивное потоковое тело, длина которого передаётся явно через аргумент size |
Само тело предоставляет следующие методы доступа:
| Метод | Описание |
|---|---|
byte[] asBytes() |
Считывает всё тело в массив байтов (исчерпывает нижележащий поток) |
InputStream asInputStream() |
Возвращает тело как блокирующий InputStream |
Flow.Publisher<ByteBuffer> asPublisher() |
Возвращает тело как реактивный Flow.Publisher |
long size() |
Длина содержимого в байтах или -1, если неизвестна (неограниченный поток / publisher) |
String type() |
Content-Type тела |
String encoding() |
Content-Encoding тела |
Если файл очень большой или его длина неизвестна и требуется потоковая передача, рекомендуется создавать тело с помощью
S3Body.ofPublisher(...) или S3Body.ofInputStreamUnbound(...).
Если тип файла не указан, будет использован application/octet-stream.
Для @S3.Put тело также можно передать напрямую как byte[] или ByteBuffer; в этом случае клиент сам создаёт S3Body.
Аннотация @S3.Put позволяет указать type и encoding, которые будут записаны как Content-Type и Content-Encoding.
HTTP-сервер может передавать тело запроса в S3 потоком, не читая весь файл в память заранее.
Для этого примите тело запроса как Flow.Publisher<ByteBuffer> и передайте его в S3Body.ofPublisher(...).
Если размер тела известен, например из заголовка Content-Length, лучше передать этот размер в S3Body;
если размер неизвестен, используйте перегрузку без размера, и размер будет считаться неизвестным.
@Component
@HttpController
public final class UploadController {
private final S3KoraClient s3;
public UploadController(S3KoraClient s3) {
this.s3 = s3;
}
@HttpRoute(method = HttpMethod.PUT, path = "/files/{key}")
public HttpServerResponse upload(@Path String key,
@Header("Content-Type") @Nullable String contentType,
@Header("Content-Length") @Nullable Long contentLength,
Flow.Publisher<ByteBuffer> body) {
var type = contentType == null ? "application/octet-stream" : contentType;
var s3Body = contentLength == null
? S3Body.ofPublisher(body, type)
: S3Body.ofPublisher(body, contentLength, type);
this.s3.put("documents", key, s3Body);
return HttpServerResponse.of(201);
}
}
@Component
@HttpController
class UploadController(
private val s3: S3KoraClient
) {
@HttpRoute(method = HttpMethod.PUT, path = "/files/{key}")
fun upload(
@Path key: String,
@Header("Content-Type") contentType: String?,
@Header("Content-Length") contentLength: Long?,
body: Flow.Publisher<ByteBuffer>
): HttpServerResponse {
val type = contentType ?: "application/octet-stream"
val s3Body = if (contentLength == null) {
S3Body.ofPublisher(body, type)
} else {
S3Body.ofPublisher(body, contentLength, type)
}
s3.put("documents", key, s3Body)
return HttpServerResponse.of(201)
}
}
В этом варианте Kora получает Flow.Publisher<ByteBuffer> из тела HTTP-запроса через стандартный
HttpServerRequestMapper, а S3-клиент читает тот же поток во время загрузки. Обработчику не нужно вызывать
asBytes(), asInputStream().readAllBytes() или S3Body.ofInputStreamReadAll(...), если цель — не держать весь файл в памяти.
Тип и кодировка содержимого¶
Вместо того чтобы самостоятельно конструировать S3Body, можно передать тело напрямую как byte[] или ByteBuffer и позволить
клиенту обернуть его в S3Body. В этом случае для построения тела используются атрибуты type (Content-Type)
и encoding (Content-Encoding) аннотации @S3.Put:
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.Put(value = "some-key", type = "image/jpeg", encoding = "gzip") //(1)!
void operation1(byte[] body); //(2)!
@S3.Put("some-key")
void operation2(ByteBuffer body); //(3)!
}
typeсопоставляется сContent-Type, аencoding— сContent-Encoding- Когда тело имеет тип
byte[]илиByteBuffer, клиент сам строитS3Body, используяtype/encodingиз аннотации - Если не заданы ни
type, ниencoding, в качествеContent-Typeиспользуетсяapplication/octet-stream
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.Put(value = "some-key", type = "image/jpeg", encoding = "gzip") //(1)!
fun operation1(body: ByteArray) //(2)!
@S3.Put("some-key")
fun operation2(body: ByteBuffer) //(3)!
}
typeсопоставляется сContent-Type, аencoding— сContent-Encoding- Когда тело имеет тип
ByteArrayилиByteBuffer, клиент сам строитS3Body, используяtype/encodingиз аннотации - Если не заданы ни
type, ниencoding, в качествеContent-Typeиспользуетсяapplication/octet-stream
Тип тела
Тело операции @S3.Put должно быть S3Body, byte[] или ByteBuffer, иначе возникает ошибка компиляции.
Атрибуты type и encoding применяются только к «сырым» телам byte[]/ByteBuffer; когда передаётся готовый S3Body,
используются его собственные значения type()/encoding(), а атрибуты аннотации игнорируются.
Шаблон ключа¶
Ключ также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все аргументы метода должны быть частью составного ключа.
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.Put("prefix-{key1}-{key2}-suffix") //(1)!
void operation(String key1, int key2, S3Body body); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа либо иметь тип
S3Body
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.Put("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int, body: S3Body) //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа либо иметь тип
S3Body
Удаление файла¶
В разделе описана операция удаления файла с помощью декларативного S3-клиента.
Для операции предлагается использовать аннотацию @S3.Delete.
Шаблон ключа¶
Ключ также можно задать в виде шаблона и подставлять в него аргументы метода как часть шаблона; все аргументы метода должны быть частью составного ключа.
@S3.Client("s3client.someClient")
public interface SomeClient {
@S3.Delete("prefix-{key1}-{key2}-suffix") //(1)!
void operation(String key1, int key2); //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа
@S3.Client("s3client.someClient")
interface SomeClient {
@S3.Delete("prefix-{key1}-{key2}-suffix") //(1)!
fun operation(key1: String, key2: Int) //(2)!
}
- Шаблон, используемый для построения ключа: каждый аргумент шаблона подставляется через
toString(), а аргументы шаблона указываются как имена аргументов метода в{фигурных скобках} - Все аргументы метода должны быть частью шаблона ключа
Несколько ключей¶
Также можно удалять несколько файлов по ключам.
Сигнатуры¶
Доступные из коробки сигнатуры методов декларативного S3-клиента:
Под T подразумевается тип возвращаемого значения.
T myMethod()CompletionStage<T> myMethod()CompletionStageCompletableFuture<T> myMethod()CompletableFutureMono<T> myMethod()Project Reactor (надо подключить зависимость)
Под T подразумевается тип возвращаемого значения, либо T?, либо Unit.
myMethod(): Tsuspend myMethod(): TKotlin Coroutine (надо подключить зависимость какimplementation)
Модели¶
И декларативные, и императивные клиенты возвращают один и тот же набор типов-моделей (если не используется
нативный формат ответа модуля AWS). Все модели — интерфейсы только для чтения.
S3Object¶
Полный объект вместе с его данными, возвращаемый операциями получения и доступный внутри S3ObjectList:
| Метод | Описание |
|---|---|
String key() |
Ключ объекта |
Instant modified() |
Время последнего изменения |
long size() |
Размер объекта в байтах |
S3Body body() |
Тело объекта с данными |
S3ObjectMeta¶
Облегчённые метаданные без данных объекта, возвращаемые операциями получения метаданных и доступные внутри S3ObjectMetaList. Получение метаданных быстрее, поскольку тело объекта не передаётся:
| Метод | Описание |
|---|---|
String key() |
Ключ объекта |
Instant modified() |
Время последнего изменения |
long size() |
Размер объекта в байтах |
S3ObjectList¶
Список полных объектов, возвращаемый операциями получения списка. Расширяет S3ObjectMetaList, поэтому также предоставляет префикс и метаданные:
| Метод | Описание |
|---|---|
String prefix() |
Префикс, использованный для получения списка |
List<S3Object> objects() |
Объекты, соответствующие префиксу (с данными) |
List<S3ObjectMeta> metas() |
Метаданные объектов, соответствующих префиксу |
S3ObjectMetaList¶
Список метаданных, возвращаемый операциями получения списка метаданных:
| Метод | Описание |
|---|---|
String prefix() |
Префикс, использованный для получения списка |
List<S3ObjectMeta> metas() |
Метаданные объектов, соответствующих префиксу |
S3ObjectUpload¶
Результат операции добавления файла:
| Метод | Описание |
|---|---|
String versionId() |
Идентификатор версии загруженного объекта (если для бакета включено версионирование) |
Императивный клиент¶
Для работы с S3 можно внедрить императивный клиент Kora; предоставляются как синхронный, так и асинхронный клиенты:
S3KoraClient- клиент для синхронной работыS3KoraAsyncClient- клиент для асинхронной работы
Оба клиента работают с явными параметрами bucket и key и поддерживают получение объектов или метаданных, получение списка объектов по префиксу,
загрузку S3Body и удаление одного или нескольких объектов. В отличие от декларативного клиента, они не привязаны к единственному bucket из
конфигурации — bucket передаётся в каждый метод явно.
@Component
public final class SomeService {
private final S3KoraClient s3;
public SomeService(S3KoraClient s3) {
this.s3 = s3;
}
public byte[] download(String bucket, String key) {
S3Object object = s3.get(bucket, key); //(1)!
return object.body().asBytes();
}
}
- Выбрасывает
S3NotFoundException, если объект отсутствует
Синхронный клиент¶
Интерфейс S3KoraClient предоставляет следующие операции:
| Метод | Описание |
|---|---|
S3Object get(bucket, key) |
Получить один объект с данными |
S3ObjectMeta getMeta(bucket, key) |
Получить метаданные одного объекта |
List<S3Object> get(bucket, Collection<String> keys) |
Получить несколько объектов с данными |
List<S3ObjectMeta> getMeta(bucket, Collection<String> keys) |
Получить метаданные нескольких объектов |
S3ObjectList list(bucket[, prefix[, delimiter, limit]]) |
Получить список объектов по префиксу (с данными) |
S3ObjectMetaList listMeta(bucket[, prefix[, delimiter, limit]]) |
Получить список метаданных объектов по префиксу |
List<S3ObjectList> list(bucket, Collection<String> prefixes[, delimiter, limit]) |
Получить список объектов сразу для нескольких префиксов |
List<S3ObjectMetaList> listMeta(bucket, Collection<String> prefixes[, delimiter, limit]) |
Получить список метаданных объектов сразу для нескольких префиксов |
S3ObjectUpload put(bucket, key, S3Body body) |
Добавить объект и вернуть результат загрузки |
void delete(bucket, key) |
Удалить один объект |
void delete(bucket, Collection<String> keys) |
Удалить несколько объектов (при неудаче выбрасывает S3DeleteException) |
Перегрузки list/listMeta без delimiter/limit по умолчанию используют null для delimiter и 1000 для limit.
Аргумент limit должен находиться в диапазоне 1..1000.
// получить один объект и его метаданные
S3Object object = s3.get("documents", "report.pdf");
S3ObjectMeta meta = s3.getMeta("documents", "report.pdf");
// получить сразу несколько объектов
List<S3Object> objects = s3.get("documents", List.of("a.pdf", "b.pdf"));
// получить список по префиксу с разделителем и ограничением
S3ObjectList list = s3.list("documents", "2024/", "/", 100);
for (S3Object o : list.objects()) {
// ...
}
// получить список сразу для нескольких префиксов
List<S3ObjectMetaList> perPrefix = s3.listMeta("documents", List.of("2023/", "2024/"));
// добавить объект
S3ObjectUpload upload = s3.put("documents", "report.pdf", S3Body.ofBytes(bytes));
String versionId = upload.versionId();
// удалить один объект и пакет объектов
s3.delete("documents", "report.pdf");
s3.delete("documents", List.of("a.pdf", "b.pdf"));
// получить один объект и его метаданные
val obj = s3.get("documents", "report.pdf")
val meta = s3.getMeta("documents", "report.pdf")
// получить сразу несколько объектов
val objects = s3.get("documents", listOf("a.pdf", "b.pdf"))
// получить список по префиксу с разделителем и ограничением
val list = s3.list("documents", "2024/", "/", 100)
for (o in list.objects()) {
// ...
}
// получить список сразу для нескольких префиксов
val perPrefix = s3.listMeta("documents", listOf("2023/", "2024/"))
// добавить объект
val upload = s3.put("documents", "report.pdf", S3Body.ofBytes(bytes))
val versionId = upload.versionId()
// удалить один объект и пакет объектов
s3.delete("documents", "report.pdf")
s3.delete("documents", listOf("a.pdf", "b.pdf"))
Асинхронный клиент¶
Интерфейс S3KoraAsyncClient повторяет S3KoraClient метод в метод, но каждая операция возвращает
CompletionStage
(CompletionStage<Void> для операций удаления):
Нативные клиенты¶
Помимо декларативных и императивных клиентов Kora, для внедрения также доступны нижележащие нативные клиенты SDK.
Они полезны для расширенных операций, не покрываемых декларативным/императивным API (например, управление бакетами, копирование
объектов, предподписанные (presigned) URL и так далее).
Модуль AWS предоставляет:
S3Client— синхронный клиентAWSS3AsyncClient— асинхронный клиентAWSS3AsyncClientс@Tag(MultipartUpload.class)— асинхронный клиентAWS, предварительно настроенный для многочастной загрузки в соответствии сupload.partSizeиupload.bufferSize
Модуль Minio предоставляет:
MinioClient— синхронный клиентMinioMinioAsyncClient— асинхронный клиентMinio
@Component
public final class BucketService {
private final S3Client s3Client; //(1)!
private final S3AsyncClient multipartClient;
public BucketService(S3Client s3Client,
@Tag(MultipartUpload.class) S3AsyncClient multipartClient) { //(2)!
this.s3Client = s3Client;
this.multipartClient = multipartClient;
}
public void ensureBucket(String bucket) {
s3Client.createBucket(b -> b.bucket(bucket));
}
}
- Нативный
S3ClientизAWS, внедряемый напрямую - Асинхронный клиент с тегом
@Tag(MultipartUpload.class)для многочастной загрузки
@Component
class BucketService(
private val s3Client: S3Client, //(1)!
@Tag(MultipartUpload::class) private val multipartClient: S3AsyncClient //(2)!
) {
fun ensureBucket(bucket: String) {
s3Client.createBucket { it.bucket(bucket) }
}
}
- Нативный
S3ClientизAWS, внедряемый напрямую - Асинхронный клиент с тегом
@Tag(MultipartUpload::class)для многочастной загрузки
Исключения¶
Если операция клиента завершается неудачей, выбрасывается одно из исключений S3. Все они наследуются от базового S3Exception,
который, в свою очередь, расширяет RuntimeException, поэтому их обработка необязательна и не проверяется компилятором.
Иерархия исключений:
Базовое исключение S3Exception предоставляет код ошибки и сообщение, сообщённые хранилищем:
| Метод | Описание |
|---|---|
String getErrorCode() |
Код ошибки хранилища (например, NoSuchKey) |
String getErrorMessage() |
Сообщение об ошибке хранилища |
Пример обработки:
@Component
public final class SomeService {
private final S3KoraClient s3;
public SomeService(S3KoraClient s3) {
this.s3 = s3;
}
public void call(String bucket) {
try {
s3.delete(bucket, List.of("a.pdf", "b.pdf"));
} catch (S3NotFoundException e) {
// Объект или бакет отсутствует: getErrorCode() возвращает NoSuchKey или NoSuchBucket
} catch (S3DeleteException e) {
// Один или несколько объектов не были удалены
for (S3DeleteException.Error error : e.getErrors()) {
// error.key(), error.bucket(), error.code(), error.message()
}
} catch (S3Exception e) {
// Любая другая ошибка хранилища: getErrorCode(), getErrorMessage()
}
}
}
@Component
class SomeService(
private val s3: S3KoraClient
) {
fun call(bucket: String) {
try {
s3.delete(bucket, listOf("a.pdf", "b.pdf"))
} catch (e: S3NotFoundException) {
// Объект или бакет отсутствует: errorCode равен NoSuchKey или NoSuchBucket
} catch (e: S3DeleteException) {
// Один или несколько объектов не были удалены
for (error in e.errors) {
// error.key(), error.bucket(), error.code(), error.message()
}
} catch (e: S3Exception) {
// Любая другая ошибка хранилища: errorCode, errorMessage
}
}
}
S3NotFoundException¶
Выбрасывается, когда запрошенный объект или бакет не существует.
Причины:
- Ключ объекта не существует (
getErrorCode()возвращаетNoSuchKey) - Бакет не существует (
getErrorCode()возвращаетNoSuchBucket)
Рекомендации:
- Сделайте результат
@S3.Getнеобязательным (Optional/nullable), если отсутствие объекта — нормальный исход - Проверьте
bucketиз конфигурации и запрошенныйkey
S3DeleteException¶
Выбрасывается пакетными операциями delete(bucket, keys), когда один или несколько объектов не удалось удалить.
Предоставляет список отдельных сбоев:
| Метод | Описание |
|---|---|
List<Error> getErrors() |
Сбои по каждому объекту, каждый с key(), bucket(), code(), message() |
Рекомендации:
- Изучите
getErrors(), чтобы определить, какие объекты не удалось обработать и почему - Повторите неудавшиеся ключи отдельно, если сбой временный
S3Exception¶
Базовое исключение, выбрасываемое при любой другой ошибке хранилища или клиента, не связанной с отсутствием объекта или сбоем пакетного удаления.
Рекомендации:
- Логируйте
getErrorCode()иgetErrorMessage()для диагностики - Включите логирование клиента на уровне
DEBUG, чтобы изучить нижележащий запрос/ответ
Тестирование¶
Декларативные и императивные S3-клиенты можно тестировать с помощью @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() {
var value = "value".getBytes(StandardCharsets.UTF_8);
client.putObject("k1", S3Body.ofBytes(value));
var found = client.getObject("k1");
assertArrayEquals(value, found.body().asBytes());
}
}
@TestcontainersMinio(
mode = ContainerMode.PER_RUN,
bucket = Bucket(value = [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()
client.putObject("k1", S3Body.ofBytes(value))
val found = client.getObject("k1")
assertArrayEquals(value, found.body().asBytes())
}
companion object {
const val BUCKET = "simple"
}
}