Java および Play 2 を使用したファイルアップロードのための Amazon S3 の使用
最終更新日 2022年11月14日(月)
Table of Contents
AWS S3 などのストレージサービスを使用してファイルアップロードを保存すると、単なるローカルファイルシステムへのファイルの保存と比較して 大幅なスケーラビリティ、信頼性、および速度の向上が得られます。S3 または同様のストレージサービスは、スケーリングに対応したアプリケーションを設計する場合に重要であり、Heroku の一時的なファイルシステムを完全に補完するものです。
この記事では、Play 2 を使用して Amazon S3 にファイルアップロードを保存する Java Web アプリケーションを作成する方法について説明します。 この記事を読む前に、必要な S3 の資格情報やキーを確立する方法を示すと共に、このようなアプローチのメリットを詳細に説明している「AWS S3 を使用して静的アセットとファイルアップロードを格納する」を確認してください。
この記事のサンプルアプリケーションのソースは、GitHub で入手できます。
Heroku で Play 2 を初めて使用する場合は、Play 2 のドキュメントの「Deploying to Heroku」(Heroku へのデプロイ) を参照する必要があります。
AWS ライブラリ
S3 では、このサービスと対話するための RESTful API が提供されます。 その API をラップする Java ライブラリが存在するため、Java コードからの対話が簡単になっています。 Play 2 プロジェクトで、project/Build.scala の appDependencies
セクションを更新することによって、アプリケーションへの aws-java-sdk
の依存関係を追加できます。
val appDependencies = Seq(
"com.amazonaws" % "aws-java-sdk" % "1.3.11"
)
Play 2 プロジェクトで依存関係を更新したら、Play 2 サーバーを再起動し、すべての IDE 設定ファイル (Eclipse および IntelliJ) を再生成する必要があります。
Play 2 用の S3 プラグイン
Play 2 には、サーバーの起動時に自動的に起動できるプラグインを作成する方法が用意されています。 Play 2 用の正式な S3 プラグインはまだ存在しませんが、次の内容を含む app/plugins/S3Plugin.java という名前のファイルを作成することによって独自のプラグインを作成できます。
package plugins;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import play.Application;
import play.Logger;
import play.Plugin;
public class S3Plugin extends Plugin {
public static final String AWS_S3_BUCKET = "aws.s3.bucket";
public static final String AWS_ACCESS_KEY = "aws.access.key";
public static final String AWS_SECRET_KEY = "aws.secret.key";
private final Application application;
public static AmazonS3 amazonS3;
public static String s3Bucket;
public S3Plugin(Application application) {
this.application = application;
}
@Override
public void onStart() {
String accessKey = application.configuration().getString(AWS_ACCESS_KEY);
String secretKey = application.configuration().getString(AWS_SECRET_KEY);
s3Bucket = application.configuration().getString(AWS_S3_BUCKET);
if ((accessKey != null) && (secretKey != null)) {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
amazonS3 = new AmazonS3Client(awsCredentials);
amazonS3.createBucket(s3Bucket);
Logger.info("Using S3 Bucket: " + s3Bucket);
}
}
@Override
public boolean enabled() {
return (application.configuration().keys().contains(AWS_ACCESS_KEY) &&
application.configuration().keys().contains(AWS_SECRET_KEY) &&
application.configuration().keys().contains(AWS_S3_BUCKET));
}
}
S3Plugin
は 3 つの設定パラメータを読み取り、S3 への接続を設定して、ファイルを保持するための S3 バケットを作成します。 このプラグインを有効にするには、次の内容を含む conf/play.plugins という名前の新しいファイルを作成します。
1500:plugins.S3Plugin
これは、S3Plugin
に 1500
の優先度で開始するよう指示します。つまり、すべてのデフォルトの Play プラグインの後に起動します。
S3Plugin の設定
S3Plugin
が機能するには、3 つの設定パラメータが必要です。 aws.access.key
は AWS アクセスキーを保持し、aws.secret.key
は AWS 秘密鍵を保持します。 また、aws.s3.bucket
パラメータを使用してグローバル固有バケット ID を指定することも必要です。 これらの設定パラメータを設定するには、それを conf/application.conf ファイルに追加します。
aws.access.key=${?AWS_ACCESS_KEY}
aws.secret.key=${?AWS_SECRET_KEY}
aws.s3.bucket=com.something.unique
機密の接続情報を設定ファイルに直接入力することはお勧めしません。代わりに aws.access.key
と aws.secret.key
は AWS_ACCESS_KEY
と AWS_SECRET_KEY
という名前の環境変数から取得します。 これらの値は、次のようにローカルにエクスポートすることによって設定できます。
$ export AWS_ACCESS_KEY=<Your AWS Access Key>
$ export AWS_SECRET_KEY=<Your AWS Secret Key>
aws.s3.bucket
の名前は、アプリケーションに関連した一意の名前に変更する必要があります。たとえば、デモアプリケーションでは値 com.heroku.devcenter-java-play-s3
を使用していますが、デモを自分で実行する場合は、これを他の名前に変更する必要があります。
S3File モデル
単純な S3File
モデルオブジェクトはファイルを S3 にアップロードし、ファイルメタデータをデータベースに保存します。 次の内容を含む app/models/S3File.java という名前のファイル内に新しい S3File
モデルを作成できます。
package models;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import play.Logger;
import play.db.ebean.Model;
import plugins.S3Plugin;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
@Entity
public class S3File extends Model {
@Id
public UUID id;
private String bucket;
public String name;
@Transient
public File file;
public URL getUrl() throws MalformedURLException {
return new URL("https://s3.amazonaws.com/" + bucket + "/" + getActualFileName());
}
private String getActualFileName() {
return id + "/" + name;
}
@Override
public void save() {
if (S3Plugin.amazonS3 == null) {
Logger.error("Could not save because amazonS3 was null");
throw new RuntimeException("Could not save");
}
else {
this.bucket = S3Plugin.s3Bucket;
super.save(); // assigns an id
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, getActualFileName(), file);
putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead); // public for all
S3Plugin.amazonS3.putObject(putObjectRequest); // upload file
}
}
@Override
public void delete() {
if (S3Plugin.amazonS3 == null) {
Logger.error("Could not delete because amazonS3 was null");
throw new RuntimeException("Could not delete");
}
else {
S3Plugin.amazonS3.deleteObject(bucket, getActualFileName());
super.delete();
}
}
}
S3File
クラスには、プライマリキーである id
、ファイルが保存される bucket
、ファイルの name
、実際にはデータベースに保存されない実際の file
(そのため、これは @Transient
) という 4 つのパラメータがあります。
S3File
クラスは save
メソッドを上書きします。ここでは、設定されたバケット名を S3Plugin
から取得してから S3File
をデータベースに保存し、そこで新しい id
が割り当てられます。 その後、このファイルは S3 Java ライブラリを使用して S3 にアップロードされます。
この例では、ファイルのアクセス許可か public に設定されている (リンクによりすべてのユーザーに表示される) ことに注意してください。
逆に、S3File
クラスは、S3File
がデータベースから削除される前に S3 上のファイルが削除されるように delete
メソッドも上書きします。
S3 上の実際のファイル名は getActualFileName
メソッドから派生しています。これは、id
と元のファイル名が /
で連結されたものです。 S3 にはディレクトリの概念はありませんが、これはシミュレートし、ファイル名の衝突を回避しています。
S3File
クラスにはまた、S3 の HTTP サービスを使用してファイルへの URL を返す getUrl
メソッドもあります。 これは、ユーザーが S3 からファイルを取得するための最も直接的な方法ですが、これが機能するのは単に、ファイルが public のアクセス権を持つように設定されているためです。
別の方法として、ファイルを private にし、S3 の API 呼び出しを使用してファイルをフェッチする別のメソッドを S3File
に用意することもできます。
データベースの設定
ここではデータベースを使用しているため、conf/application.conf で EBean とデータベース接続を設定する必要があります。
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
ebean.default="models.*"
これらの値はローカルでの開発には有効ですが、Heroku 上で実行する場合は、新しい Play アプリに対して自動的にプロビジョニングされる Heroku Postgres アドオンを使用できます。 プロジェクトに PostgreSQL JDBC ドライバーを追加するには、project/Build.scala ファイルに次の依存関係を追加します。
"org.postgresql" % "postgresql" % "9.4-1201-jdbc4"
Play に PostgreSQL データベースを使用するよう指示するには、次の内容を含む Procfile
という名前のファイルを作成します。
web: target/start -Dhttp.port=$PORT -DapplyEvolutions.default=true -Ddb.default.driver=org.postgresql.Driver -Ddb.default.url=$DATABASE_URL
これにより、Heroku 上でアプリケーションが実行されると、データベース設定が (PostgreSQL を使用するように) 上書きされます。
アプリケーションのコントローラー
これで、ファイルメタデータを保持し、S3 にファイルをアップロードするモデルが用意されたので、アップロード Web ページのレンダリングや実際のファイルアップロードを処理するコントローラーを作成しましょう。 次の内容を含む app/controllers/Application.java という名前のファイルを作成 (または更新) します。
package controllers;
import models.S3File;
import play.db.ebean.Model;
import play.mvc.Controller;
import play.mvc.Result;
import play.mvc.Http;
import views.html.index;
import java.util.List;
import java.util.UUID;
public class Application extends Controller {
public static Result index() {
List<S3File> uploads = new Model.Finder(UUID.class, S3File.class).all();
return ok(index.render(uploads));
}
public static Result upload() {
Http.MultipartFormData body = request().body().asMultipartFormData();
Http.MultipartFormData.FilePart uploadFilePart = body.getFile("upload");
if (uploadFilePart != null) {
S3File s3File = new S3File();
s3File.name = uploadFilePart.getFilename();
s3File.file = uploadFilePart.getFile();
s3File.save();
return redirect(routes.Application.index());
}
else {
return badRequest("File upload error");
}
}
}
Application
クラスの index
メソッドは、データベースに S3File
オブジェクトに関するクエリを実行した後、それをレンダリングのために index
ビューに渡します。 upload
メソッドはファイルアップロードを受信し、それで新しい S3File
を作成し、そのアップロードを保存してから元のインデックスページにリダイレクトします。
インデックスビュー
ここで、ユーザーがファイルをアップロードするために使用できるフォームを含み、アップロードの一覧表示も行う単純なインデックスページを作成しましょう。 次の内容を含む app/views/index.scala.html という名前のファイルを作成 (または更新) します。
@(s3Files: List[models.S3File])
< !DOCTYPE html>
<html>
<head>
<title>File Upload with Java, Play 2, and S3</title>
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
</head>
<body>
<h1>Upload a file:</h1>
@helper.form(action = routes.Application.upload, 'enctype -> "multipart/form-data") {
<input type="file" name="upload">
<input type="submit">
}
<h1>Uploads:</h1>
<ul>
@for(upload <- uploads) {
<li><a href="@upload.getUrl()">@upload.name</a></li>
}
</ul>
</body>
</html>
このビューには、ファイルアップロードのフォーム (helper.form
メソッドを使用して作成) とファイルの一覧が含まれています。
ルート
設定する必要のある最後のものがルートです。 conf/routes ファイルには、HTTP リクエストの動詞とパスのコントローラーメソッドへのマッピングが含まれています。 GET リクエストを Application.index
メソッドに、POST リクエストを Application.upload
メソッドにマッピングするには、conf/routes
ファイルに次の行を追加します。
GET / controllers.Application.index()
POST / controllers.Application.upload()
これで、このアプリケーションはローカルで完全に動作します。
Heroku 上での実行
サンプルプロジェクトの Git リポジトリからソースを複製しなかった場合は、ファイルを新しい Git リポジトリに追加してコミットする必要があります。
$ git init
$ git add .
$ git commit -m init
これで、Heroku で新しいアプリケーションをプロビジョニングできます。
$ heroku create
アプリケーションの環境設定として AWS 接続キーを設定します。
$ heroku config:set AWS_ACCESS_KEY=<Your AWS Access Key> AWS_SECRET_KEY=<Your AWS Secret Key>
Heroku でアプリケーションをデプロイするには、Git リポジトリを Heroku にプッシュします。
$ git push heroku master
次に、アプリケーションが動作していることを確認します。
$ heroku open
より深い学習
これは非常に単純な例にすぎないため、本番ユースケースで改善できる可能性のあるいくつかの領域が存在します。 この例では、ファイルのダウンロードは Amazon S3 から処理されました。 より優れた設定は、Amazon CloudFront を使用したアップロードのエッジキャッシュです。
この例では、ファイルはまず Play アプリに移動した後 S3 に移動するため、2 ホップのアップロードを実行しています。 S3 に直接 POST を送信することによって、最初のホップをスキップして S3 に直接アップロードできます。
最後に、アップロード (やすべての IO) はブロック操作であるため、より多くの並列リクエストを処理できるように Play サーバーのスレッドプールサイズを増やす (デフォルトは、わずか 4) ことが必要になる可能性があります。