今回は前回に引き続き、Spring Cloud AWSでS3へアクセスするアプリケーションを実装していきます。

アプリケーションコンポーネントの実装

では早速、アプリケーションコンポーネントを実装していきましょう。Controllerでは、以下3種類の処理を実装します。

  • S3のバケット内にアップロードしている画像ファイル「sample.jpg」を取得し、MediaType.IMAGEJPEGVALUEとして、画像データをレスポンスとして返却する処理
  • S3のバケット内にアップロードしているテキストファイル「test.txt」を取得し、中身の文字列をレスポンスとして返却する処理
  • 画面からアップロードされたマルチパート形式のファイルをS3バケットに保存し、「uploadResult.html」へリダイレクトする処理

※ 今回は比較的小さいファイルサイズの画像を扱うことを想定して、Controllerから取得する例を実装しています。なお、リクエストマッピング実装の要領についてはTERASOLUNAのガイドライン「リクエストとハンドラメソッドのマッピング方法」も適宜参考にしてください。

package org.debugroom.mynavi.sample.aws.s3.app.web;

import java.awt.image.BufferedImage;
// omit

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {

    @Autowired
    S3DownloadHelper s3DownloadHelper;

    @Autowired
    S3UploadHelper s3UploadHelper;

    // omit

    @GetMapping(value = "/image",
         headers = "Accept=image/jpeg, image/jpg, image/png, image/gif",
         produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_GIF_VALUE})
    @ResponseBody
    public ResponseEntity getImage(){
        return ResponseEntity.ok().body(
             s3DownloadHelper.getImage("sample.jpg"));
    }

    @GetMapping("getTextFileBody")
    @ResponseBody
    public ResponseEntity getTextFileBody(){
        return ResponseEntity.ok().body(
             s3DownloadHelper.getTextFileBody("test.txt"));
    }

    @PostMapping("upload")
    public String upload(FileUploadForm fileUploadModel){
        s3UploadHelper.saveFile(fileUploadModel.getUploadFile());
        return "redirect:/uploadResult.html";
    }

    // omit

}

Controllerから呼び出すS3でダウンロード、アップロードを行う処理をHelperとして実装します。ダウンロード処理では、org.springframework.core.io.ResourceLoaderで、 S3のバケットプレフィックスを指定してオブジェクトキーを指定し、InputStreamとして読み込みを行います。なお、画像ファイルの場合はデータ型としてjava.awt.image.BufferedImageを使用し、テキストデータなどの場合は、org.apache.commons.io.IOUtilsなどのユーティリティライブラリを使ってストリームデータをString型へ変換します。

package org.debugroom.mynavi.sample.aws.s3.app.web.helper;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

@Component
public class S3DownloadHelper{

    private static final String S3_BUCKET_PREFIX = "s3://";
    private static final String DIRECTORY_DELIMITER = "/";

    @Value("${bucket.name}")
    private String bucketName;

    @Autowired
    ResourceLoader resourceLoader;

    public BufferedImage getImage(String imageFilePath){
        Resource resource = resourceLoader.getResource(
          new StringBuilder()
          .append(S3_BUCKET_PREFIX)
          .append(bucketName)
          .append(DIRECTORY_DELIMITER)
          .append(imageFilePath)
          .toString());
        BufferedImage image = null;
        try(InputStream inputStream = resource.getInputStream()){
            image = ImageIO.read(inputStream);
        }catch (IOException e){
            e.printStackTrace();
        }
        return image;
    }

    public String getTextFileBody(String textFilePath){
        Resource resource = resourceLoader.getResource(
          new StringBuilder()
          .append(S3_BUCKET_PREFIX)
          .append(bucketName)
          .append(DIRECTORY_DELIMITER)
          .append(textFilePath)
          .toString());
        String textBody = null;
        try(InputStream inputStream = resource.getInputStream()){
            textBody = IOUtils.toString(inputStream, "UTF-8");
        }catch (IOException e){
            e.printStackTrace();
        }
        return textBody;
    }
}

アップロード処理は同じくResourceLoaderを経由して、S3のバケットプレフィックスを保存したいオブジェクトキーと組み合わせ、WritableResourceとして取得し、OutputStreamにデータを保存します。

また、バケット上のディレクトリを含めた、オブジェクキーのデータが存在するかどうかはResourcePatternResolverを使って検索ができますが、ディレクトリの作成やデータの削除などの処理はSDKのライブラリとして提供されているcom.amazonaws.services.s3.AmazonS3を使って直接操作を行う必要があります。

package org.debugroom.mynavi.sample.aws.s3.app.web.helper;

// omit

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;

import org.apache.commons.io.IOUtils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.WritableResource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class S3UploadHelper{

    private static final String S3_BUCKET_PREFIX = "s3://";
    private static final String DIRECTORY_DELIMITER = "/";

    @Value("${bucket.name}")
    private String bucketName;

    @Autowired
    ResourceLoader resourceLoader;

    @Autowired
    ResourcePatternResolver resourcePatternResolver;

    @Autowired
    AmazonS3 amazonS3;

    public String saveFile(MultipartFile multipartFile){
        String objectKey = new StringBuilder()
          .append(S3_BUCKET_PREFIX)
          .append(bucketName)
          .append(DIRECTORY_DELIMITER)
          .append(multipartFile.getOriginalFilename())
          .toString();
        WritableResource writableResource = (WritableResource)resourceLoader.getResource(objectKey);
        try(InputStream inputStream = multipartFile.getInputStream();
                OutputStream outputStream = writableResource.getOutputStream()){
            IOUtils.copy(inputStream, outputStream);
        }catch (IOException e){
            e.printStackTrace();
        }
        return objectKey;
     }

    public boolean existsDirectory(String directoryPath){
        try{
            List resourceList = Arrays.asList(
              resourcePatternResolver.getResources(directoryPath + "/**"));
            if (resourceList.size() == 0){
                return false;
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        return true;
    }

    public void createDirectory(String directoryPath){
        ObjectMetadata objectMetadata = new ObjectMetadata();
        try(InputStream emptyContent = new ByteArrayInputStream(new byte[0]);){
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                bucketName, directoryPath, emptyContent, objectMetadata);
            amazonS3.putObject(putObjectRequest);
    }catch (IOException e){
        e.printStackTrace();;
    }
}

実装が完了したら、画面を作成して実際に画像がダウンロードされるかを確認し、アップロード処理を実行してみましょう。今回アップロードしていた「sample.jpg」は本連載のバナー画像です。「test.txt」をアップロードして、「Get TextFile Body」ボタンを押し、その内容を取得してみます。

「test.txt」をアップロードする

すると、ファイルがアップロードされていることが確認できます。

ファイルのアップロードを確認

続いて、アップロードしたファイルの中身を取得し、表示します。

「Get TextFile Body」ボタンを押下

このように、Spring Cloud AWSを用いることで、S3にアクセスしてダウンロード/アップロードするアプリケーションを簡単に実装することができます。AWS上に構築するクラウドネイティブなアプリケーションは、データ保存にS3を利用することで、可用性/信頼性の高い構成が可能です。

なお、署名つきURLや、一時認証情報を使って、クライアントからS3に直接ファイルをダウンロード/アップロードする方法については、今回GitHub上にサンプル実装していますが、AWS上のIAMアクセスロール設定やサーバ側のアプリケーション実装が複雑で基本の範疇を越えるため、詳細な解説は別の機会に譲りたいと思います。

次回は、AmazonSQSを使ったSpringアプリケーション(オンライン・バッチ)の実装方法を解説します。