Skip to content

配置 S3 / MinIO

avalon-s3-spring-boot-starter 提供 Avalon 服务端统一的对象存储接入能力,支持:

  • Amazon S3
  • MinIO
  • RustFS
  • 阿里云 OSS(S3 兼容)
  • 七牛云 Kodo(S3 兼容)
  • 通用 S3 兼容实现
  • 多命名 client
  • 预签名 URL
  • Multipart 上传
  • S3TransferManager
  • 原生 S3ClientS3ControlClient

依赖

Maven:

xml
<dependency>
  <groupId>io.github.lishangbu</groupId>
  <artifactId>avalon-s3-spring-boot-starter</artifactId>
  <version>${avalon.version}</version>
</dependency>

Gradle Kotlin DSL:

kotlin
implementation("io.github.lishangbu:avalon-s3-spring-boot-starter:${avalonVersion}")

基础配置

配置前缀是 avalon.s3

yaml
avalon:
  s3:
    enabled: true
    default-client-name: default
    clients:
      default:
        provider: AWS
        region: ap-southeast-1

最重要的几个点:

  • enabled 默认是 false
  • default-client-name 默认是 default
  • clients 至少需要一个启用中的 client
  • 默认 bean 都来自 default-client-name 指向的那个 client

支持的 provider

当前支持六种 provider:

  • AWS
  • MINIO
  • RUSTFS
  • ALIYUN_OSS
  • QINIU_KODO
  • GENERIC_S3

默认行为:

  • AWS 默认使用 virtual-hosted 风格,不自动强制 path-style-access
  • MINIORUSTFSGENERIC_S3 默认启用 path-style-access
  • ALIYUN_OSSQINIU_KODO 默认使用 virtual-hosted 风格
  • AWS 在未显式配置 region 时,会交给 AWS SDK 自己解析
  • MINIORUSTFSGENERIC_S3 在未显式配置 region 时,会回退到 us-east-1
  • ALIYUN_OSSQINIU_KODO 要求显式配置 region
  • AWS 外,其他 provider 都要求显式配置 endpoint

常用属性

yaml
avalon:
  s3:
    enabled: true
    default-client-name: media
    clients:
      media:
        enabled: true
        provider: AWS
        region: ap-southeast-1
        endpoint:
        use-arn-region-enabled: true
        dualstack-enabled: false
        accelerate-mode-enabled: false
        path-style-access:
        chunked-encoding-enabled:
        checksum-validation-enabled:
        bucket-aliases:
          public: my-public-bucket
          private: my-private-bucket
        credentials:
          access-key-id:
          secret-access-key:
          session-token:
        overrides:
          api-call-timeout: 30s
          api-call-attempt-timeout: 10s
        http:
          connection-timeout: 3s
          socket-timeout: 30s
          read-timeout: 30s
          write-timeout: 30s
          max-connections: 200
          max-concurrency: 200
        transfer:
          multipart-enabled: true

说明:

  • bucket-aliases 用来把业务里使用的别名映射成真实 bucket 名称
  • credentials 留空时,默认走 AWS SDK 的 DefaultCredentialsProvider
  • access-key-idsecret-access-key 同时存在时,使用静态凭证
  • session-token 也存在时,使用临时会话凭证

AWS 示例

如果部署在 EC2、ECS、EKS、Lambda,推荐直接使用默认凭证链,不显式写 AK / SK:

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: AWS
        region: ap-southeast-1
        bucket-aliases:
          media: my-media-bucket
          archive: my-archive-bucket

这时会由 AWS SDK 自动按顺序尝试环境变量、profile、web identity、instance role 等凭证来源。

如果必须显式写静态凭证:

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: AWS
        region: ap-southeast-1
        credentials:
          access-key-id: ${AWS_ACCESS_KEY_ID}
          secret-access-key: ${AWS_SECRET_ACCESS_KEY}

MinIO 示例

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: MINIO
        endpoint: http://127.0.0.1:9000
        region: us-east-1
        path-style-access: true
        credentials:
          access-key-id: minioadmin
          secret-access-key: minioadmin
        bucket-aliases:
          media: avalon-media

本地开发时通常只需要这几个属性:

  • provider=MINIO
  • endpoint
  • path-style-access=true
  • 静态凭证

RustFS 示例

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: RUSTFS
        endpoint: http://127.0.0.1:9000
        region: us-east-1
        credentials:
          access-key-id: rustfsadmin
          secret-access-key: rustfsadmin
        bucket-aliases:
          media: avalon-media

说明:

  • RUSTFS 默认按 path-style 访问
  • 如果你使用自定义网关域名,也可以显式覆写 path-style-access

阿里云 OSS 示例

阿里云 OSS 的 S3 兼容接入,通常需要同时配置 Region ID 和 S3 兼容 endpoint:

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: ALIYUN_OSS
        endpoint: https://oss-cn-hangzhou.aliyuncs.com
        region: cn-hangzhou
        credentials:
          access-key-id: ${ALIYUN_ACCESS_KEY_ID}
          secret-access-key: ${ALIYUN_ACCESS_KEY_SECRET}
        bucket-aliases:
          media: my-oss-bucket

说明:

  • endpoint 形如 https://oss-<region>.aliyuncs.com
  • region 应填写 Region ID,例如 cn-hangzhou
  • ALIYUN_OSS 默认使用 virtual-hosted 风格
  • 如有特殊网关要求,也可以显式设置 path-style-access: true

七牛云 Kodo 示例

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: QINIU_KODO
        endpoint: https://s3.cn-east-1.qiniucs.com
        region: cn-east-1
        credentials:
          access-key-id: ${QINIU_ACCESS_KEY}
          secret-access-key: ${QINIU_SECRET_KEY}
        bucket-aliases:
          media: my-kodo-bucket

说明:

  • endpoint 使用七牛提供的 S3 服务域名
  • region 需要与 endpoint 所在区域保持一致
  • QINIU_KODO 默认使用 virtual-hosted 风格
  • 如果你的 bucket 命名或网关策略更适合 path-style,也可以显式开启 path-style-access

多 client 配置

如果一个应用要同时连接多个对象存储,可以直接配置多个命名 client:

yaml
avalon:
  s3:
    enabled: true
    default-client-name: media
    clients:
      media:
        provider: AWS
        region: ap-southeast-1
        bucket-aliases:
          public: media-public-bucket
      archive:
        provider: AWS
        region: ap-southeast-1
        bucket-aliases:
          cold: archive-cold-bucket
      local:
        provider: MINIO
        endpoint: http://127.0.0.1:9000
        region: us-east-1
        path-style-access: true
        credentials:
          access-key-id: minioadmin
          secret-access-key: minioadmin
      rustfs:
        provider: RUSTFS
        endpoint: http://127.0.0.1:9001
        region: us-east-1
        credentials:
          access-key-id: rustfsadmin
          secret-access-key: rustfsadmin
      oss:
        provider: ALIYUN_OSS
        endpoint: https://oss-cn-hangzhou.aliyuncs.com
        region: cn-hangzhou
        credentials:
          access-key-id: ${ALIYUN_ACCESS_KEY_ID}
          secret-access-key: ${ALIYUN_ACCESS_KEY_SECRET}
      qiniu:
        provider: QINIU_KODO
        endpoint: https://s3.cn-east-1.qiniucs.com
        region: cn-east-1
        credentials:
          access-key-id: ${QINIU_ACCESS_KEY}
          secret-access-key: ${QINIU_SECRET_KEY}

说明:

  • Spring 默认只暴露 default-client-name 对应的一组 bean
  • 其他命名 client 通过 AvalonS3ClientRegistry 获取

可直接注入的 Bean

启用 starter 后,默认 client 会自动注册以下 Bean:

  • S3Facade
  • BucketOperations
  • ObjectOperations
  • MultipartOperations
  • PresignOperations
  • TransferOperations
  • S3Client
  • S3AsyncClient
  • S3Waiter
  • S3Presigner
  • S3ControlClient
  • S3ControlAsyncClient
  • S3TransferManager
  • AvalonS3ClientRegistry

S3Facade 是默认入口,聚合了常用 facade 和原生 client。

使用 S3Facade

kotlin
import io.github.lishangbu.avalon.s3.facade.S3Facade
import org.springframework.stereotype.Service
import java.time.Duration

@Service
class FileStorageService(
    private val s3Facade: S3Facade,
) {
    fun upload(
        key: String,
        content: ByteArray,
    ) {
        s3Facade.objects.put("media", key, content, "application/octet-stream")
    }

    fun downloadUrl(key: String): String =
        s3Facade.presign
            .get("media", key, Duration.ofMinutes(5))
            .url()
            .toString()
}

这里的 "media" 会先按 bucket-aliases 解析,再转换成真实 bucket 名称。

使用命名 client 注册表

kotlin
import io.github.lishangbu.avalon.s3.client.AvalonS3ClientRegistry
import org.springframework.stereotype.Service

@Service
class CrossStorageService(
    private val registry: AvalonS3ClientRegistry,
) {
    fun copyToArchive(key: String) {
        val media = registry.facade("media")
        val archive = registry.facade("archive")

        val content = media.objects.getBytes("public", key).asByteArray()
        archive.objects.put("cold", key, content)
    }
}

使用原生 S3Client

如果 facade 还没有覆盖某个能力,直接使用 AWS SDK 即可:

kotlin
import org.springframework.stereotype.Service
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetBucketVersioningRequest

@Service
class VersioningQueryService(
    private val s3Client: S3Client,
) {
    fun getStatus(bucket: String) =
        s3Client.getBucketVersioning(
            GetBucketVersioningRequest
                .builder()
                .bucket(bucket)
                .build(),
        )
}

这也是当前 starter 的设计重点:

  • facade 负责常用能力
  • 原生 SDK 负责完整能力

使用 S3ControlClient

Amazon S3 的一部分高级控制面能力并不在 S3Client 上,而是在 S3ControlClient 上,例如:

  • Access Points
  • Batch Operations
  • Storage Lens
  • Multi-Region Access Points
  • 其他 S3 Control API

示例:

kotlin
import org.springframework.stereotype.Service
import software.amazon.awssdk.services.s3control.S3ControlClient
import software.amazon.awssdk.services.s3control.model.ListAccessPointsRequest

@Service
class AccessPointService(
    private val s3ControlClient: S3ControlClient,
) {
    fun list(accountId: String) =
        s3ControlClient.listAccessPoints(
            ListAccessPointsRequest
                .builder()
                .accountId(accountId)
                .build(),
        )
}

注意:

  • S3ControlClient 主要面向 Amazon S3
  • 对 MinIO 或其他兼容实现,很多控制面操作并不支持

常用 facade 能力

1. 桶操作

kotlin
s3Facade.buckets.create("media")
s3Facade.buckets.exists("media")
s3Facade.buckets.list()
s3Facade.buckets.delete("media")

2. 对象操作

kotlin
s3Facade.objects.put("media", "hello.txt", "hello".toByteArray(), "text/plain")
s3Facade.objects.getBytes("media", "hello.txt")
s3Facade.objects.head("media", "hello.txt")
s3Facade.objects.list("media", "prefix/")
s3Facade.objects.delete("media", "hello.txt")

3. Multipart 上传

kotlin
val upload = s3Facade.multipart.create("media", "large.bin")

val part1 = s3Facade.multipart.uploadPart("media", "large.bin", upload.uploadId(), 1, firstChunk)
val part2 = s3Facade.multipart.uploadPart("media", "large.bin", upload.uploadId(), 2, secondChunk)

s3Facade.multipart.complete(
    bucketName = "media",
    key = "large.bin",
    uploadId = upload.uploadId(),
    completedParts = listOf(
        CompletedPart.builder().partNumber(1).eTag(part1.eTag()).build(),
        CompletedPart.builder().partNumber(2).eTag(part2.eTag()).build(),
    ),
)

注意:

  • 除最后一个 part 外,Amazon S3 要求每个 part 至少 5 MiB
  • 这条规则在 MinIO 兼容实现上通常也会生效

4. 预签名 URL

kotlin
val getUrl =
    s3Facade.presign
        .get("media", "hello.txt", Duration.ofMinutes(5))
        .url()

val putUrl =
    s3Facade.presign
        .put("media", "upload.txt", Duration.ofMinutes(10), "text/plain")
        .url()

支持:

  • GET
  • PUT
  • DELETE
  • HEAD
  • UploadPart

5. 传输管理

kotlin
import java.nio.file.Paths

val upload =
    s3Facade.transfer.uploadFile(
        bucketName = "media",
        key = "avatars/u1.png",
        source = Paths.get("/data/avatar.png"),
        contentType = "image/png",
    )

s3Facade.transfer.await(upload)

目录上传和下载也可以直接使用 TransferOperations

自定义 SDK Builder

如果你要接入代理、链路追踪、特殊 endpoint、额外 override 配置,可以注册以下扩展点之一:

  • S3ClientBuilderCustomizer
  • S3AsyncClientBuilderCustomizer
  • S3PresignerBuilderCustomizer
  • S3ControlClientBuilderCustomizer
  • S3ControlAsyncClientBuilderCustomizer
  • S3TransferManagerBuilderCustomizer

示例:

kotlin
import io.github.lishangbu.avalon.s3.customizer.S3ClientBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.net.URI

@Configuration
class S3CustomizationConfiguration {
    @Bean
    fun s3ClientBuilderCustomizer(): S3ClientBuilderCustomizer =
        S3ClientBuilderCustomizer { clientName, properties, builder ->
            if (clientName == "media") {
                builder.endpointOverride(URI.create("https://s3.ap-southeast-1.amazonaws.com"))
            }
        }
}

适用场景:

  • 统一注入自定义 User-Agent
  • 增加 SDK override 配置
  • 做某个命名 client 的定制化接入

与旧版本的差异

当前版本已经不再提供旧的 S3Template 和旧的 s3.* 配置前缀。

现在应该改成:

  • 使用 avalon.s3.* 配置
  • 使用 S3FacadeAvalonS3ClientRegistry
  • 对高级能力直接使用 S3Client / S3ControlClient

旧版本常见写法:

yaml
s3:
  endpoint: http://127.0.0.1:9000
  access-key: xxx
  secret-key: xxx

现在应改成:

yaml
avalon:
  s3:
    enabled: true
    clients:
      default:
        provider: MINIO
        endpoint: http://127.0.0.1:9000
        region: us-east-1
        path-style-access: true
        credentials:
          access-key-id: xxx
          secret-access-key: xxx

当前边界

当前 starter 已经把原生 AWS S3 与 S3 Control client 暴露出来,但 facade 仍然只覆盖最常用的能力域:

  • bucket
  • object
  • multipart
  • presign
  • transfer

如果你需要 Access Points、Batch Operations、Storage Lens、Object Lock、Replication、Inventory 等能力,推荐直接使用原生 AWS SDK request / response 对象调用。

Released under the AGPL v3 License.