KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

マイクロフレームワーク Spark


shimoda01

こんにちは。下田です。はい、また焼肉です。焼肉に良く行っている割に、「月見カルビ」なるものを最近知りました。これ、すごく美味しいんですね。是非、見つけたら試してみてください。あぁ、お腹すいてきた・・・。

はい。では、今回は、 Spark について説明したいと思います。 Spark と聞いて、思いつく Apache Spark クラスターコンピューティングの方ではありません。今回説明する Spark は、Java のマイクロフレームワークです。 Java8 のラムダを使い、冗長になりやすい Java 構文を出来るだけ簡潔に書けるように目指したフレームワークです。 Java で Web の開発といえば、巨大な XML や、大量の Annotation でウンザリすることがあると思いますが、そういった部分をゴッソリと捨てた感じのフレームワークになっています。昨今のマイクロサービスブームにも乗った形であります。この記事を書いている時点では、 github の star は、 3500 程度となっており、ある程度の人気を博しています。

まずは、定番の Hello World から始めていきましょう。 gradle の依存関係に sparkjava を追加します。

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.8
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.sparkjava:spark-core:2.3'
}

次に Main.java を用意していきます。

package spark.sample;

import static spark.Spark.get;

public class Main {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello World");
    }
}

はい、これでおしまいです。たった7行で Webapp が作れました。では、 IDE から、この Main クラスを起動して curl を使い動作確認をしてみます。デフォルトでは、 jetty が 4567 ポートで起動します。

$ curl -X GET http://localhost:4567/ hello

Hello World

では、詳細にソースを見ていきます。

import static spark.Spark.get;

まずは、この static import ですが、 get は HTTP GET メソッドを意味しています。今回は、 GET メソッドのみ実装したので、 GET メソッドのみ取り込んでいますが、これ以外にも、 POST, PUT, DELETE などがありますので、実装する対象のメソッドを取り込んで使うことになります。

get("/hello", (req, res) -> "Hello World");

さきほど取り込んだ get にエンドポイントを追加しています。一つ目の引数がパスで、二つ目の引数が、呼びだされた時の処理です。ここでは、 lambda を使っていますが、 Route というインターフェースを実装したクラスであれば何でも問題ありません。昔ならば以下のように無名クラスなどを使って定義することしか出来ませんでした。上と下の書き方を比べると随分と簡略出来るようになりましたね。

Route hello = new Route() {
    @Override
    public Object handle(Request request, Response response) throws Exception {
        return "Hello World";
    }
};

次は、 REST API ぽいものを返したいと思います。そこで、 JSON パーサーの jackson を使います。

dependencies {
    compile 'com.sparkjava:spark-core:2.3'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1-1'
}

今度は、 JSON 形式で、 Hello World の配列を返します。

package spark.sample;

import static spark.Spark.get;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    static final ObjectMapper OM = new ObjectMapper();
    public static void main(String[] args) {
        get("/hello", (req, res) -> {
            res.type("application/json");
            return OM.writeValueAsString(Arrays.asList(new String[] { "Hello", "World" }));
        });
    }
}

ここでやっている処理は単純です。レスポンスの Content type を JSON に、そして、 Java のオブジェクトを jackson を使い文字列に直しているだけです。実際に帰ってくる結果を見てみます。

$ curl -X GET -i http://localhost:4567/hello
HTTP/1.1 200 OK
Date: Sat, 13 Feb 2016 19:58:46 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Server: Jetty(9.3.2.v20150730)

["Hello","World"]

curl の実行結果を見る通り、 Content type が Json になって、配列のようなものが返却されていることが分かります。
このように、非常に素直に記述するだけで簡単に API を作成することが出来ます。

次は、素数かどうかを返す API を作ってみます。クエリーパラメータを指定して、引数を貰うことにします。

package spark.sample;

import static spark.Spark.get;

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    static final ObjectMapper OM = new ObjectMapper();

    public static void main(String[] args) {
        get("/prime", (req, res) -> {
            long n = Long.parseLong(req.queryParams("n"));
            boolean isPrime = isPrime(n);
            Map<String, Object> map = new HashMap<>();
            map.put("n", n);
            map.put("prime", isPrime);
            res.type("application/json");
            return OM.writeValueAsString(map);
        });
    }

    static boolean isPrime(long n) {
        for (long i = 2; i < n; i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }
}

では、 素数かどうかを求める API を curl で試していきます。今回実装した素数判定のロジックは、全く最適化されていないので、すごく遅くなることが想定されるので、 curl の実行時間も同時に表示して見ていきます。

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=4
{"prime":false,"n":4}
0.009

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=16769023
{"prime":true,"n":16769023}
0.244

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=1073676287
{"prime":true,"n":1073676287}
12.247

ちゃんと返ってきました。1600万台の素数でも一秒以内に返ってきますが、流石に10億台の素数は12秒かかりました。では、横道にそれますが、計算結果は不変なので、キャッシュしてみたいと思います。と言っても、 Java 内でのシンプルなキャッシュです。そこで、 Guava にある便利なキャッシュを使ってみます。
まずは、Gradle に依存関係を入れます。

dependencies {
    compile 'com.sparkjava:spark-core:2.3'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1-1'
    compile 'com.google.guava:guava:19.0'
}

Main クラスの方はほとんど変わりません。さすが Google 謹製のライブラリです。クラスの設計が完璧です。

package spark.sample;

import static spark.Spark.get;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class Main {
    static final ObjectMapper OM = new ObjectMapper();
    static final Cache<String, Object> CACHE = CacheBuilder.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .maximumSize(50)
            .build();

    public static void main(String[] args) {
        get("/prime", (req, res) -> {
            long n = Long.parseLong(req.queryParams("n"));
            return CACHE.get(n + "", () -> {
                boolean isPrime = isPrime(n);
                Map<String, Object> map = new HashMap<>();
                map.put("n", n);
                map.put("prime", isPrime);
                res.type("application/json");
                return OM.writeValueAsString(map);
            });
        });
    }

    static boolean isPrime(long n) {
        for (long i = 2; i < n; i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }
}

非常に straightforward な書き方が出来ます。今回は 30 分だけ 50 アイテムをキャッシュする設定です。キャッシュの get メソッドも spark と似たような記述です。一つ目の引数にキャッシュキー、二つ目の引数にキャッシュが無かった時の処理を記述します。

では、実際にキャッシュが効いているか、さきほどの素数を数回試してみます。

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=1073676287
{"prime":true,"n":1073676287}
10.785

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=1073676287
{"prime":true,"n":1073676287}
0.009

二回目の呼び出しは、劇的に早くなっています。と言うことは、ちゃんとキャッシュが効いているということですね。

では最後に、 SpringBoot 風に spark でも単体 jar 起動出来るように gradle を変更します。と言っても数行記述するだけで単体 jar 起動できるようになります。

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.8
version = '1.0'

ext.mainClassName = "spark.sample.Main"

jar {
    manifest {
      attributes "Main-Class": "$mainClassName"
    } 

    from {
      configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.sparkjava:spark-core:2.3'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1-1'
    compile 'com.google.guava:guava:19.0'
}

manifest に main class の記述をすることと、 jar 作成時に全ての依存関係を取り込むだけです。
では、本当に実行できるか見てみます。

$ gradle clean build && java -jar ./build/libs/spark-sample-1.0.jar
:clean
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
:check
:build

BUILD SUCCESSFUL

Total time: 17.989 secs
[Thread-1] INFO org.eclipse.jetty.util.log - Logging initialized @995ms
[Thread-1] INFO spark.webserver.JettySparkServer - == Spark has ignited ...
[Thread-1] INFO spark.webserver.JettySparkServer - >> Listening on 0.0.0.0:4567
[Thread-1] INFO org.eclipse.jetty.server.Server - jetty-9.3.z-SNAPSHOT
[Thread-1] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@57e9b2d{HTTP/1.1,[http/1.1]}{0.0.0.0:4567}
[Thread-1] INFO org.eclipse.jetty.server.Server - Started @1230ms

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=1073676287
{"prime":true,"n":1073676287}
11.047

$ curl -w '\n%{time_total}\n' -X GET http://localhost:4567/prime?n=1073676287
{"prime":true,"n":1073676287}
0.007

上記の通り、単体 jar で、実行しただけですが、正しく Jetty が 4567 ポートを listen しています。
以上で、 spark の説明を終わりますが、いかがだったでしょうか。今回コードを書いた行数は、今まで Java では考えられないほど少量で実際に動く API を開発することが出来ました。本家のサイトでは、今回試した事以外にももっと多様なサンプルが載っていますので、是非興味があれば見てください。

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!
Introduction to Antlr! Pt.2
JavaPoetで簡単コード生成!
Introduction to Antlr! Pt.3
モニタリングルーター Sensu