1. 概述

Amazon S3 凭借其可扩展性、耐用性和丰富的功能集,已成为最广泛使用的云存储后端。许多其他存储后端都力求兼容 S3 API(与 Amazon S3 交互的编程接口),这充分证明了其行业地位。

然而,当迁移到不完全兼容的替代存储后端时,依赖 S3 API 的应用可能面临挑战。这会导致开发成本激增和供应商锁定问题。

这时 S3Proxy 就派上用场了。S3Proxy 是一个开源库,通过在 S3 API 和各种存储后端之间提供兼容层来解决上述挑战。它让我们能使用熟悉的 S3 API 无缝对接不同存储后端,无需大量修改代码。

本教程将探讨如何在 Spring Boot 应用中集成 S3Proxy,并配置其与 Azure Blob Storage 和 Google Cloud Storage 协同工作。我们还会介绍如何将文件系统设置为本地开发和测试的存储后端。

2. S3Proxy 工作原理

在深入实现前,我们先了解 S3Proxy 的工作机制。

S3Proxy 位于应用和存储后端之间,充当代理服务器。当应用使用 S3 API 发送请求时,它会拦截请求并将其转换为配置的存储后端对应的 API 调用。同样,存储后端的响应会被转换回 S3 格式再返回给应用。

S3Proxy 工作原理示意图:展示 S3Proxy 如何将 S3 API 调用转换为其他存储后端调用

S3Proxy 通过嵌入式 Jetty 服务器运行,并使用Apache jclouds(多云工具包)处理转换过程来对接各种存储后端。

3. 项目搭建

使用 S3Proxy 访问多种存储后端前,需要添加必要的 SDK 依赖并正确配置应用。

3.1. 依赖管理

首先在项目的 pom.xml 中添加必要依赖:

<dependency>
    <groupId>org.gaul</groupId>
    <artifactId>s3proxy</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>s3</artifactId>
    <version>2.28.23</version>
</dependency>

S3Proxy 依赖 提供代理服务器及后续将配置的 Apache jclouds 组件。

Amazon S3 依赖 提供 S3Client 类,这是 S3 API 的 Java 封装器。

3.2. 定义云无关存储属性

现在定义一组可跨不同存储后端使用的云无关存储属性

将这些属性存储在项目的 application.yaml 文件中,并使用@ConfigurationProperties 将值映射到 POJO,后续定义 jclouds 组件和 S3Client bean 时会引用该类:

@ConfigurationProperties(prefix = "com.baeldung.storage")
class StorageProperties {

    private String identity;

    private String credential;

    private String region;

    private String bucketName;

    private String proxyEndpoint;

    // 标准的 setter 和 getter

}

上述属性代表大多数存储后端所需的通用配置参数,如安全凭证、区域和存储桶名称。此外还声明了 proxyEndpoint 属性,用于指定嵌入式 S3Proxy 服务器的运行 URL。

以下 application.yaml 片段展示了自动映射到 StorageProperties 类的属性配置:

com:
  baeldung:
    storage:
      identity: ${STORAGE_BACKEND_IDENTITY}
      credential: ${STORAGE_BACKEND_CREDENTIAL}
      region: ${STORAGE_BACKEND_REGION}
      bucket-name: ${STORAGE_BACKEND_BUCKET_NAME}
      proxy-endpoint: ${S3PROXY_ENDPOINT}

使用 ${} 属性占位符从环境变量 加载属性值。

这种设置允许我们将后端存储属性外部化,并在应用中轻松访问它们。

3.3. 应用启动时初始化 S3Proxy

为确保嵌入式 S3Proxy 服务器在应用启动时运行,创建实现ApplicationRunner 接口的 S3ProxyInitializer

@Component
class S3ProxyInitializer implements ApplicationRunner {

    private final S3Proxy s3Proxy;

    // 标准构造器

    @Override
    public void run(ApplicationArguments args) {
        s3Proxy.start();
    }

}

通过构造器注入 S3Proxy 实例,并在 run() 方法中使用它启动嵌入式代理服务器。

注意:我们尚未创建 S3Proxy 类的 bean,这将在下一节完成。

4. 访问 Azure Blob Storage

现在通过 S3Proxy 访问 Azure Blob Storage,创建 StorageConfiguration 类并注入之前创建的云无关 StorageProperties。所有必要的 bean 都将在这个新类中定义。

首先创建 BlobStore bean,它代表我们将交互的底层存储后端

@Bean
public BlobStore azureBlobStore() {
    return ContextBuilder
      .newBuilder("azureblob")
      .credentials(storageProperties.getIdentity(), storageProperties.getCredential())
      .build(BlobStoreContext.class)
      .getBlobStore();
}

使用 Apache jclouds 的 ContextBuilder 创建配置为 azureblob 提供商的 BlobStoreContext 实例,然后从该上下文获取 BlobStore 实例。

同时从注入的 StorageProperties 实例传递安全凭证。*对于 Azure Blob Storage,存储账户名称作为 identity,对应的访问密钥作为 credential*。

配置好 BlobStore 后,定义 S3Proxy bean:

@Bean
public S3Proxy s3Proxy(BlobStore blobStore) {
    return S3Proxy
      .builder()
      .blobStore(blobStore)
      .endpoint(URI.create(storageProperties.getProxyEndpoint()))
      .build();
}

使用 Blobstore 实例和 application.yaml 中配置的 proxyEndpoint 创建 S3Proxy bean。该 bean 负责将 S3 API 调用转换为底层存储后端调用。

最后创建 S3Client bean:

@Bean
public S3Client s3Client() {
    S3Configuration s3Configuration = S3Configuration
      .builder()
      .checksumValidationEnabled(false)
      .build();
    AwsCredentials credentials = AwsBasicCredentials.create(
        storageProperties.getIdentity(),
        storageProperties.getCredential()
    );
    return S3Client
      .builder()
      .region(Region.of(storageProperties.getRegion()))
      .endpointOverride(URI.create(storageProperties.getProxyEndpoint()))
      .credentialsProvider(StaticCredentialsProvider.create(credentials))
      .serviceConfiguration(s3Configuration)
      .build();
}

⚠️ 注意:我们在 S3Configuration 中禁用了校验和验证。这是必需的,因为 Azure 返回非 MD5 格式的 ETag,使用默认配置会导致错误

为简单起见,本教程中其他后端存储也使用相同的 S3Client bean。如果不使用 Azure Blob Storage,可以移除此配置。

配置好这些 bean 后,应用现在可以通过熟悉的 S3 API 与 Azure Blob Storage 交互。

5. 访问 GCP Cloud Storage

访问 Google Cloud Storage 时,只需修改 BlobStore bean。

首先为 Google Cloud Storage 创建新的 BlobStore bean。使用Spring profiles 根据激活的 profile 条件性创建 Azure 或 GCP BlobStore bean:

@Bean
@Profile("azure")
public BlobStore azureBlobStore() {
    // ... 同上
}

@Bean
@Profile("gcp")
public BlobStore gcpBlobStore() {
    return ContextBuilder
      .newBuilder("google-cloud-storage")
      .credentials(storageProperties.getIdentity(), storageProperties.getCredential())
      .build(BlobStoreContext.class)
      .getBlobStore();
}

当激活 gcp profile 时,使用 google-cloud-storage 提供商创建 BlobStore 实例。

对于 Google Cloud Storage,identity* 是服务账户的邮箱地址(如 dev-account@project-id.iam.gserviceaccount.com),*credential 是对应的 RSA 私钥**。

通过此配置更改,应用现在可以通过 S3 API 与 Google Cloud Storage 交互。

6. 使用文件系统进行本地开发和测试

6.1. 配置本地环境

首先在 StorageProperties 类中添加新属性,指定本地文件系统存储的基础目录:

private String localFileBaseDirectory;

// 标准的 setter 和 getter

创建新的 LocalStorageConfiguration 类。使用 @Profilelocaltest profile 激活该类。在此类中更新 bean 以适配本地文件系统:

@Configuration
@Profile("local | test")
@EnableConfigurationProperties(StorageProperties.class)
public class LocalStorageConfiguration {
    
    private final StorageProperties storageProperties;

    // 标准构造器
    
    @Bean
    public BlobStore blobStore() {
        Properties properties = new Properties();
        String fileSystemDir = storageProperties.getLocalFileBaseDirectory();
        properties.setProperty("jclouds.filesystem.basedir", fileSystemDir);
        return ContextBuilder
          .newBuilder("filesystem")
          .overrides(properties)
          .build(BlobStoreContext.class)
          .getBlobStore();
    }

    @Bean
    public S3Proxy s3Proxy(BlobStore blobStore) {
        return S3Proxy
          .builder()
          .awsAuthentication(AuthenticationType.NONE, null, null)
          .blobStore(blobStore)
          .endpoint(URI.create(storageProperties.getProxyEndpoint()))
          .build();
    }

}

使用 filesystem 提供商创建 BlobStore bean 并配置基础目录。

然后为文件系统 BlobStore 创建 S3Proxy bean。注意将认证类型设为 NONE,因为本地文件系统存储不需要任何认证。

最后创建不需要任何凭证的简化版 S3Client bean:

@Bean
public S3Client s3Client() {
    return S3Client
      .builder()
      .region(Region.US_EAST_1)
      .endpointOverride(URI.create(storageProperties.getProxyEndpoint()))
      .build();
}

上述代码硬编码了 US_EAST_1 区域,但在此配置中区域选择实际无关紧要。

通过此设置,应用现在配置为使用本地文件系统作为存储后端。这无需连接真实的云存储服务,降低了成本并加速了开发和测试周期

6.2. 测试与 S3Client 的交互

现在编写测试验证 S3Client 确实能与本地文件系统存储交互

首先在 application-local.yaml 中定义必要属性:

com:
  baeldung:
    storage:
      proxy-endpoint: http://127.0.0.1:8080
      bucket-name: baeldungbucket
      local-file-base-directory: tmp-store

设置测试类:

@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
@ActiveProfiles({ "local", "test" })
@EnableConfigurationProperties(StorageProperties.class)
class LocalFileSystemStorageIntegrationTest {

    @Autowired
    private S3Client s3Client;

    @Autowired
    private StorageProperties storageProperties;

    @BeforeAll
    void setup() {
        File directory = new File(storageProperties.getLocalFileBaseDirectory());
        directory.mkdir();

        String bucketName = storageProperties.getBucketName();
        try {
            s3Client.createBucket(request -> request.bucket(bucketName));
        } catch (BucketAlreadyOwnedByYouException exception) {
            // 忽略异常
        }
    }
    
    @AfterAll
    void teardown() {
        File directory = new File(storageProperties.getLocalFileBaseDirectory());
        FileUtils.forceDelete(directory);
    }

}

@BeforeAll 注解的 setup() 方法中,创建基础目录和存储桶(如果不存在)。在 @AfterAll 注解的 teardown() 方法中删除基础目录以清理测试环境。

最后编写测试验证能否使用 S3Client 上传文件:

@Test
void whenFileUploaded_thenFileSavedInFileSystem() {
    // 准备要上传的测试文件
    String key = RandomString.make(10) + ".txt";
    String fileContent = RandomString.make(50);
    MultipartFile fileToUpload = createTextFile(key, fileContent);
    
    // 将文件保存到文件系统
    s3Client.putObject(request -> 
        request
          .bucket(storageProperties.getBucketName())
          .key(key)
          .contentType(fileToUpload.getContentType()),
        RequestBody.fromBytes(fileToUpload.getBytes()));
    
    // 通过检查文件系统中是否存在来验证保存成功
    List<S3Object> savedObjects = s3Client.listObjects(request -> 
        request.bucket(storageProperties.getBucketName())
    ).contents();
    assertThat(savedObjects)
      .anyMatch(savedObject -> savedObject.key().equals(key));
}

private MultipartFile createTextFile(String fileName, String content) {
    byte[] fileContentBytes = content.getBytes();
    InputStream inputStream = new ByteArrayInputStream(fileContentBytes);
    return new MockMultipartFile(fileName, fileName, "text/plain", inputStream);
}

在测试方法中,首先准备随机名称和内容的 MultipartFile。然后使用 S3Client 将文件上传到测试存储桶。

最后通过列出存储桶中所有对象并断言存在随机 key 的文件来验证保存成功。

7. 总结

本文探讨了在 Spring Boot 应用中集成 S3Proxy 的方法。

我们介绍了必要的配置步骤,并设置了可跨不同存储后端使用的云无关存储属性。

然后展示了如何通过 Amazon S3 API 访问 Azure Blob Storage 和 GCP Cloud Storage。

最后建立了使用文件系统的本地开发和测试环境。

✅ 所有代码示例均可在 GitHub 获取。


原始标题:Introduction to S3proxy | Baeldung