Getting Started with Java on Heroku/Cedar をためしてみる

先日HerokuでJavaが対応されたのと、さっそくためしてみてるブログに触発されたので実際にやってみる。

とりあえず、Getting Started with Java on Heroku | Heroku Dev Centerをやってみる。
環境はMBA(SnowLeopard)。

Prerequisites

mavenとOpenJDK6とHerokuアカウントが必要な模様。

OpenJDK6

はじめから入っているのはJDK6(Apple)のはず。

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03-384-10M3425)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02-384, mixed mode)

なので、OpenJDKをMacPortsでインストール。

$ sudo port install openjdk6

1時間くらいかかったような・・・
で、バージョンを確認してみた。

$ /opt/local/share/java/openjdk6/bin/java -version
openjdk version "1.6.0"
OpenJDK Runtime Environment (build 1.6.0-b20)
OpenJDK 64-Bit Server VM (build 17.0-b16, mixed mode)

OK。PATHとJAVA_HOMEを設定しておく。

Maven

インストールした覚えがないので、はじめからはいってたっぽい。

$ mvn -v
Apache Maven 3.0.3 (r1075438; 2011-03-01 02:31:09+0900)
Maven home: /usr/share/maven
Java version: 1.6.0, vendor: Sun Microsystems Inc.
Java home: /opt/local/share/java/openjdk6/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "darwin", version: "10.8.0", arch: "amd64", family: "unix"

ちなみにOpenJDKいれるまえは以下のような出力だった。

$ mvn -v
Apache Maven 3.0.3 (r1075438; 2011-03-01 02:31:09+0900)
Maven home: /usr/share/maven
Java version: 1.6.0_26, vendor: Apple Inc.
Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
Default locale: ja_JP, platform encoding: SJIS
OS name: "mac os x", version: "10.6.8", arch: "x86_64", family: "mac"

んー、OS nameやencodingが違うのはなぜ・・・?まぁ、とりあえずほっとくか。

Heroku アカウント

Heroku | Loginからアカウント作成する。

Local Workstation Setup

HerokuのコマンドラインクライアントとGitをインストールしなさい、もしこれまでHeroku使ったりしてたらスキップしてとのこと。
以前HerokuでRubyをためしたときにインストールしたのがコマンドラインクライアントと同じなのかな。

$ gem list heroku

*** LOCAL GEMS ***

heroku (2.6.1)

とりあえずherokuコマンドつかえるので、今回はスキップする。
ちなみに、インストーラを起動して、何がインストールされるのかなと確認したら、

  • Foreman
  • Git
  • Heroku Client

がインストールされる模様。Foremanは・・・インストールされてないっぽい。コマンドにもgemにもない。とりえあえずそのままいってみる。
Gitは以前インストールしたのでこれを使う。ただし、バージョンは 1.7.6.1になってる。

Write Your App

サンプルコードを準備する

用意されているサンプルコードを src/main/java/HelloWorld.javaに作成する。

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.*;

public class HelloWorld extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.getWriter().print("Hello from Java!\n");
    }

    public static void main(String[] args) throws Exception{
        Server server = new Server(Integer.valueOf(System.getenv("PORT")));
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        server.setHandler(context);
        context.addServlet(new ServletHolder(new HelloWorld()),"/*");
        server.start();
        server.join();   
    }
}

Jettyの組み込みWebサーバを使っているっぽい。こんなふうに書くんだなぁ。

mavenのpom.xmlを準備する

依存ライブラリを解決するために用意されているpom.xmlを書く。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>helloworld</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>7.4.5.v20110725</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>appassembler-maven-plugin</artifactId>
                <version>1.1.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals><goal>assemble</goal></goals>
                        <configuration>
                            <assembleDirectory>target</assembleDirectory>
                            <generateRepository>false</generateRepository>
                            <programs>
                                <program>
                                    <mainClass>HelloWorld</mainClass>
                                    <name>webapp</name>
                                </program>
                            </programs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

依存ライブラリはjettyとservlet-apiのみ。
あと、appassembler-maven-pluginというのを使うらしい。

.gitignoreにtargetを追加

mavenでビルドすると成果物がtargetディレクトリに作成されるのでここをgitの管理外にする。

ここまでやってディレクトリ構造はこんな感じ。

$ tree -a
.
├── .gitignore
├── pom.xml
└── src
    └── main
        └── java
            └── HelloWorld.java

Build Your App

ローカルでビルドする。といってもmavenなのでこれだけ。

$ mvn install

BUILD SUCCESSがでればOK。
ビルド後のディレクトリ構成はこんな感じ。

$ tree -a
.
├── .gitignore
├── pom.xml
├── src
│&#160;&#160; └── main
│&#160;&#160;     └── java
│&#160;&#160;         └── HelloWorld.java
└── target
    ├── bin
    │&#160;&#160; ├── webapp
    │&#160;&#160; └── webapp.bat
    ├── classes
    │&#160;&#160; └── HelloWorld.class
    ├── helloworld-1.0-SNAPSHOT.jar
    ├── maven-archiver
    │&#160;&#160; └── pom.properties
    └── surefire

target/binディレクトリがある。これがappassemblerで作成されるのか、へー。どうやらCLASSPATHに依存ライブラリも設定してくれるっぽい、ほー。うまく使うと便利そうだなぁ。

Declare Process Types With Foreman/Procfile

ビルドしたアプリを起動するのにProcfileというファイルが必要な模様。内容は1行のみ。

web: sh target/bin/webapp

どうやら、target/bin/webapp (以下ラッパースクリプト)を起動させるコマンドのようだ。

ラッパースクリプトを使うには、あらかじめ REPO 変数を定義しておく必要があるっぽい。ので定義しておく。

$ export REPO=$HOME/.m2/repository

ここまででディレクトリ構造はこんな感じ。Procfileが追加されただけ。

$ tree -a
.
├── .gitignore
├── Procfile
├── pom.xml
├── src
│&#160;&#160; └── main
│&#160;&#160;     └── java
│&#160;&#160;         └── HelloWorld.java
└── target
    ├── bin
    │&#160;&#160; ├── webapp
    │&#160;&#160; └── webapp.bat
    ├── classes
    │&#160;&#160; └── HelloWorld.class
    ├── helloworld-1.0-SNAPSHOT.jar
    ├── maven-archiver
    │&#160;&#160; └── pom.properties
    └── surefire

で、foremanで起動するのでインストールせよとのこと。

$ sudo gem install foreman

そして、foreman起動。

$ foreman start
02:49:23 web.1     | started with pid 77595
02:49:24 web.1     | 2011-09-14 02:49:24.508:INFO::jetty-7.4.5.v20110725
02:49:24 web.1     | 2011-09-14 02:49:24.640:INFO::started o.e.j.s.ServletContextHandler{/,null}
02:49:24 web.1     | 2011-09-14 02:49:24.689:INFO::Started SelectChannelConnector@0.0.0.0:5000 STARTING

で、http://localhost:5000/ にアクセス。Hello from Java!のメッセージを確認。OK。

foreman ってなにー?と思ったので調べてみたら以下がわかりやすかった。

どうやら、Procfileに記述した複数のプロセスを管理してくれるらしい。ログはコンソールにまとめて表示してくれるらしい。便利。

Store Your App in Git

herokuにデプロイするために、Gitのローカルリポジトリを作成しコミット。

$ git init
$ git add .
$ git commit -m "init"

Deploy to Heroku/Cedar

いよいよherokuにデプロイ。
まずは、Cedarスタックを作成。

$ heroku create --stack cedar

アプリのURLとherokuのgitのURLが表示される。
また、herokuのリモートリポジトリもローカルリポジトリに登録されるので、そのままherokuへpushしてデプロイ。

$ git push heroku master

pushが完了したらmavenのビルドが走った。
コマンドは以下の模様。

.maven/bin/mvn -B -Duser.home=/tmp/build_2y56vwsxf0tss -s .m2/settings.xml -DskipTests=true clean install

テストはスキップするみたい。当たり前か。

アプリのURLを叩くと、先ほどと同じメッセージが表示された!

まとめ

  • servlet は jetty などで組み込みで記述する
  • maven と git でビルド・デプロイ
  • java を起動するためのラッパースクリプトは appassembler-maven-plugin でビルド時に生成、これ他でも使えそう
  • foreman でプロセス管理、Procfileが必要
    • java アプリなんだけど gem がいる、ってことは ruby もいるってことか
  • ローカルでも問題なくうごかせそう
    • IDE 使ってたら普通に java を起動させればよさそう
    • ってことは ruby なくてもローカルならうごかせそうかな