最終更新日:2020/04/04 原本2018-03-17

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

Spark Frameworkの使い方:Getting Start編はこちら

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

今回使用するライブラリ

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

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

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

Spark FrameworkにおけるRESTアプリ

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

import static spark.Spark.get;

public class Application {

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

}

マッピングメソッドはHTTP Methodに対応して、getpostputdeleteが用意されています。

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

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

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
@data
public class Todo {
    private String id;
    private String title;
    private LocalDateTime created;
    private boolean finished;
}

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

  • TodoRepository.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
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
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
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リストアプリを画面アプリとして作り変えてみます。