yojikのlog

yojikのブログです

Simple-Build-Toolが素敵すぎる件

(追記:2010/03/29 jerseyのバージョンが古かったので上げています)

(追記:2011/11/25 sbtは執筆時より大幅にバージョンアップされています。0.10系列からは全く別物なので、他の参考文献に当たってください。後日新規記事をアップします。)

ScalaのビルドツールSimple-Build-Toolが非常に便利で、しかもJavaプログラマにも使えそうな気がしてきたのでメモっておきます。特にちょっとしたプログラムを試すときにIDEが面倒なタイプの人、エディタでいきなりmainメソッドつきの.javaファイル作ってコンパイルして実行するタイプの人にはお勧めです。

前提

たとえばJavaでちょっとしたREST-APIを持つプログラムを作りたくなったというユースケースを想定します。Simple-Build-Toolを使いながらもJavaでという所がミソです。
参考文献

インストール

0.ディレクトリの準備

適当なディレクトリを作成します。

1.起動バッチ/シェルの準備

用意したディレクトリにバッチファイルとかシェルスクリプトを作成しておきます。

<プロジェクトディレクトリ>/sbt.bat

@setlocal
@java -jar sbt-launch-0.7.2.jar %*

@rem proxy環境の人はこんな感じの引数つけて
@rem -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080
2.ブートストラップ起動

作成したsbtコマンドを起動します。Scalaコンパイラやライブラリ関係は、このときにローカルのディレクトリにダウンロードされます。sbtでは各プロジェクト毎にリポジトリを持つ富豪的な発想になっています。もちろんプロジェクトごとに何回もダウンロードしないように共有リポジトリみたいなところにキャッシュもされてます。内部ではIvyを使ってる感じですね。

>sbt
Project does not exist, create new project? (y/N/s)

ここでyを入力すると、いくつか必要な情報をきかれます。

Project does not exist, create new project? (y/N/s) y
Name: yojik
Organization: yojik
Version [1.0]: 0.1
Scala version [2.7.7]: 2.7.7
sbt version [0.7.2]: 0.7.2

各種ライブラリのダウンロードが始まります。Scala本体やSBTの本体もこのタイミングでダウンロードします。

Getting Scala 2.7.7 ...
downloading http://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.7.7/sc
ala-compiler-2.7.7.jar ...
        [SUCCESSFUL ] org.scala-lang#scala-compiler;2.7.7!scala-compiler.jar (19
368ms)
downloading http://repo1.maven.org/maven2/org/scala-lang/scala-library/2.7.7/sca
la-library-2.7.7.jar ...
        [SUCCESSFUL ] org.scala-lang#scala-library;2.7.7!scala-library.jar (9864
ms)

(以下略
3 対話型シェルの終了

起動時引数なしにsbtを起動すると対話型シェルに入ります。終了したい場合は、exitとかquitと入力します。

> exit
[info]
[info] Total session time: 6 s, completed 2010/03/28 15:48:01
[success] Build completed successfully.

対話型シェルで出来ることはhelpとかactionsを打ち込んで確認してください。

プロジェクト依存関係の定義

0.プロジェクト定義ファイル配置用ディレクトリの作成

まず以下のディレクトリを作成します。

<プロジェクトディレクトリ>/project/build/

1.プロジェクト定義ファイルの作成

作成したディレクトリに以下のようなファイルを作成します。

<プロジェクトディレクトリ>/project/build/SampleProject.scala

import sbt._
class SampleProject(info: ProjectInfo) extends DefaultWebProject(info)  {
  import BasicScalaProject._
}
2.依存ライブラリ情報の追加

依存ライブラリはちょっと微妙なDSLで記述します。基本的にmavenやIvyの知識がある人だったら直感で理解できると思います。

import sbt._ 
class SampleProject(info: ProjectInfo) extends DefaultWebProject(info) with IdeaPlugin {
  import BasicScalaProject._

   val repo = "Java.net Repository" at "http://download.java.net/maven/2/"
   val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.14" % "test->default"
   val asm           =  "asm"           %  "asm"          % "3.1"
   val jersey_server = "com.sun.jersey" % "jersey-server" % "1.1.5"
   val jersey_core   = "com.sun.jersey" % "jersey-core" % "1.1.5" 
   val jsr           = "javax.ws.rs"    % "jsr311-api-1.1.1.jar"
   val scalaTest = "org.scalatest" % "scalatest" % "1.0" % "test"

}

リポジトリの追加と、各種ライブラリの定義をおこなっています。ちなみに val repo とか val jetty6 といった変数は被らなければなんでもオッケイです。

3.javac用の起動時引数の作成

SimpleBuildToolはJavaScala混在プロジェクトで動作するようになっています。しかしjavacを内部で起動するときにはソースコードエンコーディングをきちんと処理してくれないようです。したがって起動時引数でソースコードエンコーディングを渡すように指定してあげる必要があります。

import sbt._ 
class SampleProject(info: ProjectInfo) extends DefaultWebProject(info) {
  import BasicScalaProject._

   val repo = "Java.net Repository" at "http://download.java.net/maven/2/"
   val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.14" % "test->default"
   val asm           =  "asm"           %  "asm"          % "3.1"
   val jersey_server = "com.sun.jersey" % "jersey-server" % "1.1.5"
   val jersey_core   = "com.sun.jersey" % "jersey-core" % "1.1.5" 
   val jsr           = "javax.ws.rs"    % "jsr311-api-1.1.1.jar"
   val scalaTest = "org.scalatest" % "scalatest" % "1.0" % "test"

   override def javaCompileOptions = super.javaCompileOptions ++ 
                               Seq("-source", "1.5","-encoding", "utf8").map(x => JavaCompileOption(x))

}

依存jarファイルのダウンロード

0.プロジェクト定義のリロード

対話型シェルを起動すると先ほど記述したプロジェクト定義ファイルのリロードが行われます。

[info] Recompiling project definition...
[info]    Source analysis: 1 new/modified, 0 indirectly invalidated, 0 removed.

もしくは対話シェル上でreloadと打ち込んでもよいです。試行錯誤しながらプロジェクト定義を記述する場合に便利ですね。

1.依存jarファイルのダウンロード

対話シェル上でupdateコマンドを打ち込むと依存jarファイルがダウンロードされます。ダウンロードされたファイルは、<プロジェクトディレクトリ>/lib_managed以下に配置されます。

> update
[info]
[info] == update ==
[info] downloading http://download.java.net/maven/2/javax/ws/rs/jsr311-api/1.1-e
a/jsr311-api-1.1-ea.jar ...

(中略

[info] == update ==
[success] Successful.
[info]
[info] Total time: 49 s, completed 2010/03/28 17:04:36

以上で基本的なセッティングが終わりました。環境が整ったらsbtの対話シェルはあげっぱなしにしておいた方がよいでしょう。プロジェクト定義を変えたらreloadしてupdateです。

Javaソースの作成

0.ためしにmain文のあるクラスを書いてみる

<プロジェクトディレクトリ>/src/main/java/Sample.javaを作成します。maven使う人にはおなじみの階層ですね。

public class Sample {
    public static void main(String[] args) {
        System.out.println("Hello!");
    }
}
1.コンパイルと実行

sbtの対話型シェルでcompileと打ち込めばコンパイルされます。

> compile
[info]
[info] == compile ==
[info]   Source analysis: 1 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Compilation successful.
[info]   Post-analysis: 1 classes.
[info] == compile ==
[success] Successful.
2.更新されたら自動的にコンパイルされるようにする

ちなみに~compileと打ち込むとファイルの監視モードになりファイルを更新するたびにコンパイルされるようになります。

> ~compile
Waiting for source changes... (press enter to interrupt)

IDE感覚でコーディングできますね。

3.実行

runと打ち込むとmainメソッドがあるクラスを自動検索し実行します。ちなみに複数ある場合は候補が表示されて選択できるようになります。

> run
[info]
[info] == copy-resources ==
[info] == copy-resources ==
[info]
[info] == compile ==
[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed
[info] Compiling main sources...
[info] Nothing to compile.
[info]   Post-analysis: 1 classes.
[info] == compile ==
[info]
[info] == run ==
[info] Running Sample
Hello
[info] == run ==
[success] Successful.
[info]
4.複数のクラスがmainメソッドを持っている場合

実行するクラスを聞かれるので番号で指定します。

(前略)

[info] == run ==

Multiple main classes detected, select one to run:

 [1] Sample2
 [2] Sample

Enter number:

サーブレット関連の各種設定とソースコード作成

0.web.xmlの定義

<プロジェクトディレクトリ>/src/main/webapp/WEB-INF/web.xmlを作成します

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <servlet>
    <servlet-name>restapi</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>jp.littlevoice</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>restapi</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>30</session-timeout>
  </session-config>
</web-app>
1.RestAPI用のクラスの作成

<プロジェクトディレクトリ>/src/main/java/jp/littlecvoice/MyAPI.javaを作成します。*1

package jp.littlevoice;
import javax.ws.rs.*;
@Path("/api")
public class MyAPI {
  @GET 
  public String doGet() {
    return "Hello";
  }
}

jettyの実行とテスト

0.jetty起動

コンソールでjettry-runを実行するとjettyが起動します。

> jetty-run
(いろいろ省略)
2010/03/28 18:50:06 com.sun.jersey.api.core.PackagesResourceConfig init
情報: Root resource classes found:
  class jp.littlevoice.MyAPI
2010/03/28 18:50:06 com.sun.jersey.api.core.PackagesResourceConfig init
情報: Provider classes found:
2010/03/28 18:50:07 com.sun.jersey.server.impl.application.WebApplicationImpl
itiate
情報: Initiating Jersey application, version 'Jersey: 1.1.2-ea 08/25/2009 04:3PM'
1.ブラウザでアクセス

localhost/api/にアクセスしてチェックしてみてください。

2.jettyのストップ

jetty-stopで停止します。

まとめ

こういう風に書き出すと長いですが意外に簡単です。対話型シェルからscalaのコンソールを呼び出して対話的に開発中のScalaクラスやJavaクラスのメソッドを実行することもできます。手元で自由に出来る感覚、箱庭感覚が良いですね。

*1:jp.littlevoiceは俺の個人用のドメインです