【SpringBoot】AmazonS3にファイルをアップロード&ダウンロードする方法

# 概要
Spring Bootを3系にアップデートすると`javax`→`jakarta`に置き換える必要があるため、`javax`が依存関係に含まれる`spring-cloud-starter-aws`[^1]が使えなくなる(と思ったのですが、手元の環境では普通に動きました…)。
念のため依存関係が`jakarta`になっている`io.awspring.cloud.spring-cloud-aws-starter`に変更したのですが、情報が少なかったのでサンプルを公開します。

[^1]: `io.awspring.cloud`と`org.springframework.cloud`がありますがどちらでも一緒です

# 環境
- Java17
- SpringBoot
  - spring-boot-starter-parent：3.2.6
  - spring-cloud-aws-dependencies：3.1.1
    - spring-cloud-aws-starter
    - spring-cloud-aws-starter-s3

# サンプル
ストレージクラスにIntelligent-Tieringを指定してAmazon S3にファイル(オブジェクト)をアップロードするサンプルです。
ローカル環境からのアップロードはIntelligent-Tiering、EC2(サーバー)環境からのアップロードはStandard(何も指定しない場合と同じ)で指定する想定です。

※直接関係ない部分は省略しています

## pom.xml
AWS SDK For Javaを直接使っても良いと思いますが、今回は`spring-cloud-aws-starter`を使います。

```xml:pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws-dependencies</artifactId>
            <version>3.1.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>io.awspring.cloud</groupId>
        <artifactId>spring-cloud-aws-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.awspring.cloud</groupId>
        <artifactId>spring-cloud-aws-starter-s3</artifactId>
    </dependency>
</dependencies>
```

`dependencyManagement`で`spring-cloud-aws-dependencies`を指定すると一括でversion指定ができます。

## AwsStorageConfig.java

`S3Client`をSeriveクラスでAutowiredして使いたいのでConfigを作ってBeanに登録します。EC2上ではIAM Roleで認証情報を取得できます[^2]が、ローカル環境では情報が取れないので明示的に指定します。

[^2]: 直接関係ないので省略しますが、別途IAM Roleの設定が必要です

```Java:AwsStorageConfig.java
package com.tamorieeeen.sample.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
/**
 *
 * @author tamorieeeen
 *
 */
@Configuration
public class AwsStorageConfig {

    @Value("${spring.cloud.aws.credentials.access-key:unknown}")
    private String accessKey;
    @Value("${spring.cloud.aws.credentials.secret-key:unknown}")
    private String secretKey;
    @Value("${spring.cloud.aws.region.static:unknown}")
    private String region;

    /**
     * ローカル用
     */
    @Bean("s3Client")
    @Profile("local")
    public S3Client s3ClientLocal() {

        StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
                AwsBasicCredentials.create(accessKey, secretKey));

        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(credentialsProvider)
                .build();
    }

    /**
     * EC2(サーバー)用
     */
    @Bean("s3Client")
    @Profile("server")
    public S3Client s3Client() {

        return S3Client.create();
    }
}
```

## application.yml
ローカル用の`application-local.yml`とEC2(サーバー)用の`application-sever.yml`の設定例です。

### ローカル用

```yml:application-local.yml
spring:
  cloud:
    aws:
      credentials:
        instance-profile: false
        access-key: SUMPLEACCESSKEY1234
        secret-key: SumpleSecretKey123456789
      stack.auto: false
      region:
        instance-profile: false
        static: ap-northeast-1
      s3:
        bucket: your-bucket-name
        storage-class: INTELLIGENT_TIERING
```

### EC2(サーバー)用

```yml:application-sever.yml
spring:
  cloud:
    aws:
      credentials:
        instance-profile: true
        useDefaultAwsCredentialsChain: true
      stack.auto: false
      region:
        instance-profile: true
      s3:
        bucket: your-bucket-name
        storage-class: STANDARD
```
## AwsStorageService.java

アップロードファイルは`MultipartFile`で受け取り、ダウンロードは最終的に`ResponseEntity<byte[]>`に詰めて返す想定です。

```Java:AwsStorageService.java
package com.tamorieeeen.sample.service;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;



/**
 *
 * @author tamorieeeen
 *
 */
@Service
public class AwsStorageService {

    @Value("${spring.cloud.aws.s3.bucket}")
    private String bucket;
    @Value("${spring.cloud.aws.s3.storage-class}")
    private String storageClass;
    
    @Autowired
    private S3Client s3Client;

    /**
     * ファイルをS3にアップロード
     * @throws IOException
     * @throws SdkClientException
     */
    public void uploadFile(String s3Path, MultipartFile file) throws SdkClientException, IOException {

        PutObjectRequest putObjRequest = PutObjectRequest.builder()
                .bucket(bucket)
                .key(s3Path)
                .storageClass(storageClass)
                .contentType(file.getContentType())
                .contentLength(file.getSize())
                .build();
        byte[] bytes = file.getBytes();
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);) {
            s3Client.putObject(putObjRequest, RequestBody.fromInputStream(inputStream, bytes.length));
        }
    }

    /**
     * S3からファイルをダウンロード
     * @throws IOException
     */
    public byte[] download(String s3Path) throws IOException {

        GetObjectRequest getObjRequest = GetObjectRequest.builder()
                .bucket(bucket)
                .key(s3Path)
                .build();
        try (ResponseInputStream<GetObjectResponse> resInputStream = s3Client.getObject(getObjRequest);) {
            return resInputStream.readAllBytes();
            /*
             * ちなみに
             * ContentType は resInputStream.response().contentType()
             * ContentLength は resInputStream.response().contentLength()
             * で取れます
             */
        }
    }
}
```

## 公式のサンプル

情報少ないな～と思いつつ解決した後にAWS公式のサンプルが公開されていることに気付きました。
AWS SDK For Javaのサンプルコードだと思いますが大枠は多分一緒なので、  ~~最初からこれ見てれば楽勝だったのに…！~~  かなり参考になります。


https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/example_code/s3/src/main/java/com/example/s3

# 参考
- [[アップデート] Amazon S3 Intelligent-Tiering がより使いやすくなりました（最小ストレージ期間 / 小さなオブジェクトの監視・オートメーションコストの撤廃）](https://dev.classmethod.jp/articles/amazon-s3-intelligent-tiering-further-automating-cost-savings-for-short-lived-and-small-objects/)
- [Amazon S3 ストレージクラス](https://aws.amazon.com/jp/s3/storage-classes/)
- [Amazon EC2 での IAM ロールを使用した AWS リソースへのアクセスの許可](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/java-dg-roles.html)
- [Enum StorageClass](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/StorageClass.html)