Azure Blob StorageにSpring Bootでアクセスしてみる

諸事情でAzureのBlob Storageへアップロードおよびダウンロードを行う方法を調べる機会があったので、少しメモっておきます。

# 検証バージョン

* Spring Boot 2.5.2
* Azure Spring Boot 3.6.0
* Azure Storage Blob(Azure JDK for Java) 12.12.0

# 試したこと

以下の３つの方法でアップロード/ダウンロードが行えることを確認しました。

* Springの `Resource` インタフェース経由でアクセスする方法
* DIコンテナから `BlobServiceClientBuilder` を取得してAzure JDKのAPI経由でアクセスする方法
* DIコンテナは使わずに Azure JDK のAPI経由でアクセスする方法

# Springの `Resource` インタフェースの利用

Azure Spring Bootを利用すると、Spring　の `Resource` インタフェース経由でアップロード及びダウンロードを行うことができます。

```yaml:src/main/resources/application.yml
azure:
  storage:
    account-name: kazuki43zoostorage
    # アカウントキーは環境変数から取得
    #account-key: ${AZURE_STORAGE_ACCOUNT_KEY}
    blob-endpoint: https://${azure.storage.account-name}.blob.core.windows.net
```

```bash:~/.bashrc
export AZURE_STORAGE_ACCOUNT_KEY=xxxxxxxxxxx
```

```java
@Value("azure-blob://test-container/data/uploadAndDownloadWithResource.txt")
Resource sampleText;

@Test
void uploadAndDownloadWithResource() throws IOException {
  // Upload
  String content = "uploadAndDownloadWithResource";
  try (OutputStream out = ((WritableResource) sampleText).getOutputStream()) {
    out.write(content.getBytes(StandardCharsets.UTF_8));
  }

  // Download
  try (InputStream in = sampleText.getInputStream()) {
    Assertions.assertThat(StreamUtils.copyToString(in, StandardCharsets.UTF_8)).isEqualTo(content);
  }
}
```

# DIコンテナに登録される `BlobServiceClientBuilder` を利用

Azure Spring Bootを利用すると、Azure Blobに接続するためのコンポーネント(`BlobServiceClientBuilder`)をDIコンテナから取得し、Azure JDKのAPIを呼び出すことでアップロード及びダウンロードを行うことができます。

:::note info
SpringのResourceインタフェースを利用すると非常に簡単にアクセスすることができる一方で、Auzre JDKでサポートされている機能をフルに利用できないというデメリットもあります。そのため、Auzre JDKが提供している機能をフルに活用したい場合は、BlobServiceClientBuilderをDIコンテナから取得する方法を利用すると良いと思います。
:::

```java
@Autowired
BlobServiceClientBuilder blobServiceClientBuilder;

@Test
void uploadAndDownloadWithClientOnDIContainer() {
  // Build client
  BlobClient blobClient = blobServiceClientBuilder.buildClient()
      .getBlobContainerClient("test-container")
      .getBlobClient("data/uploadAndDownloadWithClientOnDIContainer.txt");

  // Upload
  String content = "uploadAndDownloadWithClientOnDIContainer";
  {
    BinaryData uploadData = BinaryData.fromString(content);
    blobClient.uploadWithResponse(
        new BlobParallelUploadOptions(uploadData).setComputeMd5(true), Duration.ofSeconds(10), Context.NONE);
  }

  // Download
  {
    BlobDownloadContentResponse downloadResponse = blobClient.downloadContentWithResponse(
        null, null, Duration.ofSeconds(10), Context.NONE);

    String contentMd5 = encoder.encodeToString(DigestUtils.md5Digest(downloadResponse.getValue().toBytes()));
    String headerMd5 = encoder.encodeToString(downloadResponse.getDeserializedHeaders().getContentMd5());
    Assertions.assertThat(contentMd5).isEqualTo(headerMd5);
    Assertions.assertThat(downloadResponse.getValue().toString()).isEqualTo(content);
  }
}
```

:::note warn 
MD5ハッシュ値を利用したアップロードとダウンロードの検証については、Azure SDK側でどこまで行ってくれるのかちゃんとわかってないです・・・・。
BlobParallelUploadOptionsのsetComputeMd5メソッドのJavaDocには「Whether or not the library should　calculate the md5 and send it for the service to verify.」と記載があるので、setComputeMd5(true)を指定することで、アップロード時にサーバ側でアップロードしたファイルの検証してくれるのかな〜と思っていますが・・・実際のところよくわかってないです。
ダウンロード時の検証については、おそらくAzure SDKの中で自動で検証してくれる機能はないのかな〜と思っており、Azure SDKのAPI経由でサーバで管理しているコンテンツのMD5ハッシュ値を取得して、実際にダウンロードしたファイルとの検証を自前で行う必要があるのかな〜と思っています（こちらもよくわかってない・・・）。
:::

# Azure JDKのAPIだけを利用

Azure JDKのAPIのみを利用してアップロード及びダウンロードを行うことも当然できます。

```java
@Test
void uploadAndDownloadWithClient() {
  String accountName = "kazuki43zoostorage";
  String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY"); // アカウントキーは環境変数から取得
  // Build client
  BlobClient blobClient = new BlobServiceClientBuilder()
      .endpoint("https://" + accountName + ".blob.core.windows.net")
      .credential(new StorageSharedKeyCredential(accountName, accountKey))
      .buildClient()
      .getBlobContainerClient("test-container")
      .getBlobClient("data/uploadAndDownloadWithClient.txt");

  // Upload
  String content = "uploadAndDownloadWithClient";
  {
    BinaryData uploadData = BinaryData.fromString(content);
    blobClient.uploadWithResponse(
        new BlobParallelUploadOptions(uploadData).setComputeMd5(true), Duration.ofSeconds(10), Context.NONE);
  }

  // Download
  {
    BlobDownloadContentResponse downloadResponse = blobClient.downloadContentWithResponse(
        null, null, Duration.ofSeconds(10), Context.NONE);

    String contentMd5 = encoder.encodeToString(DigestUtils.md5Digest(downloadResponse.getValue().toBytes()));
    String headerMd5 = encoder.encodeToString(downloadResponse.getDeserializedHeaders().getContentMd5());
    Assertions.assertThat(contentMd5).isEqualTo(headerMd5);
    Assertions.assertThat(downloadResponse.getValue().toString()).isEqualTo(content);
  }
}
```

# 実行結果

こんな感じでアップロードされました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/a3fc759d-5d27-5734-e65d-ffd21cf2f246.png)

# 検証アプリ

* https://github.com/kazuki43zoo/demo-azure

# まとめ

MD5検証がライブラリ側（サーバ側）でどこまで対応しているのかがまだ不明な点はありますが、コンテンツのアップロードおよびダウンロードができることは確認できたので、いったん満足しています。**MD5検証どこまでサポートされているか知っている方がいたら是非コメントください！！**