Spark Web Framework の使い方 : RESTアプリを作成する


[Spark Frameworkの使い方：Getting Start編](https://qiita.com/yoshikawaa/items/e6c0dff41eeb26dad1ce)はこちら

今回はSpark Frameworkを使ったRESTアプリの作成について解説します。

## 今回使用するライブラリ

* spark-core 2.7.1
* spark-debug-tools 0.5
* gson 2.8.2

gsonを依存関係に追加します。

```xml
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>
```

## Spark FrameworkにおけるRESTアプリ

Getting Start編でやったように、Spark Frameworkではパスにマッピングしたハンドラでリクエストを処理し、戻り値としてレスポンスボディを返却します。

```java
import static spark.Spark.get;

public class Application {

    public static void main(String[] args) { 
        get("/", (req, res) -> "hello world!");
    }

}
```

マッピングメソッドはHTTP Methodに対応して、`get`・`post`・`put`・`delete`が用意されています。

```java
get("/", (req, res) -> { ... });
post("/", (req, res) -> { ... });
put("/", (req, res) -> { ... });
delete("/", (req, res) -> { ... });
```

また、各マッピングメソッドには、レスポンスの内容によって異なる`Route`インターフェイスが用意されています。


```java
get(String, Route);
get(String, Route, ResponseTransformer);
get(String, TemplateViewRoute, TemplateEngine);
```

RESTアプリではレスポンスとしてJSONを返却するため、`get(String, Route, ResponseTransformer);`を利用します。
`ResponseTransformer`はハンドラが返却した戻り値を変換するためのインターフェイスで、`Route`と同様に`@FunctionalInterface`で定義されています。

## RESTアプリを実装する

### 前提となる実装

今回はTODOリストを管理するRESTアプリを作成します。
TODOは以下のモデルで登録します。

* Todo.java

```java
@data
public class Todo {
    private String id;
    private String title;
    private LocalDateTime created;
    private boolean finished;
}
```

モデルを操作・永続化するリポジトリは以下のとおりです。
なお、リポジトリの実装は主眼ではないので省略します。

* TodoRepository.java

```java
public class TodoRepository {
    public List<Todo> findAll() { ... }
    public Optional<Todo> findOne(String id) { ... }
    public void create(String title) { ... }
    public void finish(String id) { ... }
    public void remove(String id) { ... }
}
```

### 実装

それでは、RESTアプリを実装していきます。
ここでは、ハンドラの処理が複雑化することを考慮して、ハンドラをApplication.javaから分離してコントローラクラスを作成します。

* TodoController.java

```java
public class TodoController {

    private static final TodoRepository repository = new TodoRepository();
    private static final Gson gson = new Gson();

    // (1)
    public static final Route findAll = (req, res) -> {
        return repository.findAll(); // (2)
    };

    public static final Route findOne = (req, res) -> {
        return repository.findOne(req.params("id")); // (3)
    };

    public static final Route create = (req, res) -> {
        Todo todo = gson.fromJson(req.body(), Todo.class); // (4)
        repository.create(todo.getTitle());
        return Collections.singletonMap("message", "created.");
    };

    public static final Route finish = (req, res) -> {
        repository.finish(req.params("id"));
        return Collections.singletonMap("message", "finished.");
    };

    public static final Route remove = (req, res) -> {
        repository.remove(req.params("id"));
        return Collections.singletonMap("message", "removed.");
    };
}
```

| no. | description |
|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | 各ハンドラは`Route`インターフェイスを実装します。`@FunctionalInterface`なので分かりづらいかもしれませんが、匿名クラスのオブジェクトを生成してクラスフィールドに格納しています。 |
| (2) | 戻り値として、JSONに変換したいオブジェクトを返却します。JSONへの変換は前述のとおり`ResponseTransformer`が実施します。 |
| (3) | `Request#params`メソッドでパス変数を取得できます。メソッド名から勘違いしそうですが、取得できるのはリクエストパラメータではなくパス変数（パスの一部）です。 |
| (4) | JSONで送信されたリクエストボディは`Gson#formJson`メソッドで`Todo`オブジェクトに復元します。要はSpark側でリクエストボディを復元する仕組みは用意されていないので、自分でやる必要があるということです。 |

> ハンドラを格納するのはクラスフィールドである必要はありません。DIすることを考慮すると、インスタンスフィールドにするほうが適切かもしれません。

次に、ハンドラの戻り値をレスポンスに変換する`ResponseTransformer`を作成します。
ここではgsonを利用してオブジェクトをJSONに変換します。

* JsonResponseTransformer

```java
public class JsonResponseTransformer implements ResponseTransformer {

    private static final Gson gson = new Gson();

    // Gson#toJsonメソッドでオブジェクトをJSONに変換する。
    @Override
    public String render(Object model) throws Exception {
        return gson.toJson(model);
    }

}
```

> `ResponseTransformer`も`@FunctionalInterface`なので、ラムダ式で実装することも可能です。

`ResponseTransformer`はモデルからレスポンスの変換を抽象化する仕組みですが、同じようにリクエストからモデルへの変換を抽象化する仕組みがSparkにはなく、ちょっと残念です。次回以降、リクエストを抽象化する仕組みについても触れていこうと思っています。

これで必要なピースは揃ったので、あとはリクエストマッピングの設定を行うだけです。

* Application.java

```java
public class Application {

    private static final ResponseTransformer transformer = new JsonResponseTransformer();

    public static void main(String[] args) {
        // configure application.
        enableDebugScreen();

        // configure routes.
        path("/todos", () -> { // (1)
            // (2)
            get("", TodoController.findAll, transformer);
            // (3)
            get("/:id", TodoController.findOne, transformer);
            post("", TodoController.create, transformer);
            put("/:id", TodoController.finish, transformer);
            delete("/:id", TodoController.remove, transformer);
        });

        // configure after filters.
        after("/*", (req, res) -> res.type("application/json")); // (4)
    }
}
```

| no. | description |
|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | `path`メソッドは、パスの階層化・グルーピングに使用します。例えば、2番目の`get`メソッドにはパス`/:id`を指定しているので、`/todos/:id`というパスへのリクエストがマッピングされることになります。 |
| (2) | 各メソッドとパスに対するリクエストを処理するハンドラと、ハンドラの戻り値を変換する`ResponseTransformer`に、それぞれ先ほど作成したものを指定しています。 |
| (3) | パスに`:id`と記述していますが、これがパス変数を意味します。パスのこの部分に入力された値を`Request#params`メソッドで取り出すことが可能です。 |
| (4) | 最後に`after`メソッドで文字通りハンドラの後に処理するフィルタを定義しています。Sparkでは、レスポンスのステータスコードやコンテントタイプなどを自動で調整してくれないので、自分で`application/json`を付与する必要があります。 |

### 実行

アプリを起動して、ブラウザのRESTクライアントプラグインなどでリクエストを送ってみると、レスポンスがJSONとして返却され、RESTアプリとして機能していることが分かると思います。

## まとめ

REST APIとして必要なマッピングが揃っており、シンプルに実装できることが分かりました。
ただ、やはりJSONへの変換などは自動でやってくれるわけではないので、何を受け取って何を返却するかなど理解の上で作る必要がありそうですね。

次回は、今回作ったTODOリストアプリを画面アプリとして作り変えてみます。