Spark Frameworkで簡単マイクロサービス！

## 1. Spark Frameworkとは

非常にシンプルなWebアプリケーションのフレームワークで、[公式サイト](http://sparkjava.com/)では以下のように説明されています。

> Spark - A micro framework for creating web applications in Kotlin and Java 8 with minimal effort

[githubで:star:7199の評価がある](https://github.com/perwendel/spark)(2018/3/4時点)ので、かなり使われているフレームワークのようです。

特徴としては、Webアプリケーションをラムダ式とstaticメソッドを使ってとても手軽に実装することができます。
以下は公式ドキュメントに掲載されているサンプルです。

```java:HelloWorld.java
import static spark.Spark.*;

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

mainメソッドのある普通のjavaアプリケーションとして実行します。
アプリケーションが起動した後、Webブラウザで http://localhost:4567/hello にアクセスすると`Hello World`が表示されます。非常にシンプルですね！
今回はこのSpark Frameworkを使ってRESTサービスのTodoアプリを作ってみたいと思います。

## 2. Todoアプリの仕様


```java:Todo.java
package com.example.spark.demo;

import java.io.Serializable;
import java.util.Date;

public class Todo implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String todoId;
    private String todoTitle;
    private Date createdAt;
    private boolean finished;
    
    // constructor, setter, getter omitted
}
```

| 項番 | パス | HTTPメソッド | 説明 |
|:----:|:-----|:------------:|:-----|
| 1 | /api/todo | POST | 送信されたデータでTODOを作成する |
| 2 | /api/todo/:todoId | GET | todoIdで指定されたTODOを取得する |
| 3 | /api/todo/:todoId | PUT | todoIdで指定されたTODOを更新する |
| 4 | /api/todo/:todoId | DELETE | todoIdで指定されたTODOを削除する |

* サーバのポート番号はデフォルトの`4567`から`8090`に変更
* データはJSONフォーマットを利用


## 2. プロジェクトの作成

まずはmvnでブランクプロジェクトを作成します。

```bat:windowsでのコマンド例
mvn archetype:generate ^
-DinteractiveMode=false ^
-DarchetypeArtifactId=maven-archetype-quickstart ^
-DgroupId=com.example.spark.demo ^
-DartifactId=spark-demo
```

ブランクプロジェクトができたら、今回利用するライブラリを`pom.xml`に追加します。
今回はJSONフォーマットの変換にGSONを利用します。他のライブラリでも構いません。Spark Frameworkの公式ドキュメントではGSONを利用していたので今回はGSONを利用することにしました

```xml:pom.xml
    <!-- add spark framework -->
    <dependency>
      <groupId>com.sparkjava</groupId>
      <artifactId>spark-core</artifactId>
      <version>2.7.1</version>
    </dependency>
    <!-- add gson -->
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.2</version>
    </dependency>
```

`pom.xml`にライブラリの追記が完了したらライブラリを取得するため、以下のコマンドでお試しビルドしましょう。`BUILD SUCCESS`が表示されればOKです。

```console:お試しビルドのコマンド
mvn package -Dmaven.test.skip=true
```

## 3. ソースコード

### 3.1. アプリケーションのクラス

```java:App.java
package com.example.spark.demo;

import static spark.Spark.*;

/**
 * ★ポイント1
 * spark demo app
 */
public class App {
    // ★ポイント1
    public static void main(String[] args) {
        // ★ポイント2
        // initialize
        initialize();
        // ★ポイント3
        // define api
        TodoApi.api();
        // omitted
    }

    // ★ポイント2
    private static void initialize() {
        // server port
        port(8090);
        // static files
        staticFiles.location("/public");
        // connection pool
        // maxThreads, minThreads, timeOutMillis
        threadPool(8, 2, 30000);
    }
}
```

**★ポイント1**
Spark FrameworkのアプリケーションはMainメソッドで実行する普通のjavaアプリケーションとして実装します。

**★ポイント2**
アプリケーションの初期設定（コンフィグレーション）の処理を`initialize`メソッドに切り出しました。
今回はよく利用しそうな以下の３点の初期設定を行いました。

* サーバポートの変更

`port`メソッドでサーバのポートをデフォルトの`4567`から`8090`に変更しました。

* 静的ファイルのWeb公開

通常のRESTサービスだと不要ですが、簡易Webサーバとして使うこともあるかもしれないので、クラスパス配下にある静的ファイルをWeb公開する方法について説明します。
`staticFiles.location`メソッドで公開するディレクトリを指定します。

サンプルの設定ではWebブラウザで`http://localhost:8090/css/styles.css`にアクセスすると`/spark-demo/src/main/resources/public/css/styles.css`ファイルが取得できます。

* リクエストを受け付けるコネクションのプール設定

`threadPool`メソッドでコネクションのプール設定を行います。
引数は順番に最大スレッド数、最少スレッド数、タイムアウト（ミリ秒）です。

**★ポイント3**
Web APIは追加、変更等があった際のメンテナンスビリティを考慮し、別のクラスとして定義することにしました。

### 3.2. Web APIのクラス

```java:TodoApi.java
package com.example.spark.demo;

import static spark.Spark.*;

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

/**
 * ★ポイント4
 * Web API for TODO
 */
public class TodoApi {
    // ★ポイント4
    public static void api() {
        // ★ポイント5
        TodoService todoService = new TodoService();
        JsonTransformer jsonTransformer = new JsonTransformer();

        // ★ポイント6
        path("/api", () -> {
            path("/todo", () -> {
                post("", (request, response) -> {
                    String json = request.body();
                    Todo todo = jsonTransformer.fromJson(json, Todo.class);
                    return todoService.create(todo);
                }, jsonTransformer);
                get("/:todoId", (request, response) -> {
                    return todoService.find(request.params(":todoId"));
                }, jsonTransformer);
                put("/:todoId", (request, response) -> {
                    String json = request.body();
                    Todo todo = jsonTransformer.fromJson(json, Todo.class);
                    todo.setTodoId(request.params(":todoId"));
                    return todoService.update(todo);
                }, jsonTransformer);
                delete("/:todoId", (request, response) -> {
                    todoService.delete(request.params(":todoId"));
                    return success();
                }, jsonTransformer);
            });
            // ★ポイント7
            // set response-type to all request of under '/api'
            after("/*", (request, response) -> {
                response.type("application/json;charset=UTF-8");
            });
        });
    }

    private static Map<String, String> success() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("result", "success!");
        return map;
    }
}
```

**★ポイント4**
Web APIの処理を通常のクラスとして定義します。
Spark FrameworkのAPIはstaticメソッドが多いのでそれにならって、今回はstaticの`api`メソッドとして定義することにしました。

**★ポイント5**
業務ロジック（TodoService）とフォーマット変換（JsonTransformer）のクラスのインスタンスを生成します。
２つのクラスについては後程説明します。

**★ポイント6**
今回の記事のポイントになります。

* GET、POST、PUT、DELETE等のHTTPメソッドに対応したstaticメソッドが用意されているのでそれを利用します。
 * 第１引数　：　ハンドリング対象となるパス
 * 第２引数　：　リクエスト、レスポンスの処理を行うラムダ式。この処理の戻り値がレスポンスのデータとなる
 * 第３引数　：　（任意）レスポンスデータのフォーマット変換を行う（ResponseTransformerの）オブジェクト
* パスの入れ子（ネスト）を利用して階層構造にしたい場合、`path`メソッドを利用します。
* パスの値を取得するには`/:todoId`のようにパスを定義し、`request.params`メソッドで取得します。
* リクエストのBODYデータは`request.body`メソッドでStringとして取得します。
 * JSONとして取得する方法はありません。手動で変換する必要があります。

詳細については[公式ドキュメントのRoutes](http://sparkjava.com/documentation#routes)と[Request](http://sparkjava.com/documentation#request)を参照ください。

**★ポイント7**
Spark Frameworkのフィルタという機能でWeb APIの前後に処理を追加することができます。
サンプルではパスが`/api`配下のすべてのリクエストに対して、レスポンスヘッダの`content-type`に`application/json;charset=UTF-8`を設定する`after`フィルタを定義しました。

`after`フィルタのほかにも`before`、`afterAfter`のフィルタがあります。フィルタの詳細については[公式ドキュメント](http://sparkjava.com/documentation#filters)を参照ください。

### 3.3. フォーマット変換のクラス

```java:JsonTransformer.java
package com.example.spark.demo;

import com.google.gson.Gson;
import spark.ResponseTransformer;

// ★ポイント8
public class JsonTransformer implements ResponseTransformer {

    private Gson gson = new Gson();

    // ★ポイント8        
    @Override
    public String render(Object model) throws Exception {
        return gson.toJson(model);
    }

    // ★ポイント9
    public <T> T fromJson(String json, Class<T> classOfT) {
        return gson.fromJson(json, classOfT);
    }
}
```

**★ポイント8**
`spark.ResponseTransformer`インタフェースを実装したクラスを定義します。
このインタフェースの目的はAPIメソッドの処理結果（★ポイント6のラムダ式の結果）をHTTPレスポンスに書き込むStringに変換することです。
`@Override`が付与されている通り`render`にこの処理を実装します。
今回は公式ドキュメントに記載のあった処理そのままで、GSONを利用してJSONフォーマットに変換しています。

**★ポイント9**
`ResponseTransformer`の名前の通り、本来はレスポンスのデータ変換のためのクラスですが、リクエスト時のJSONからのオブジェクト変換もここで変換することにしました。（GSONのオブジェクトを使いまわしたかっただけです）

ちなみにSpark Frameworkにはリクエストのデータ変換を行う`RequestTransformer`みたいなものは存在しません。

### 3.4. 業務ロジックのクラス

```java:TodoService.java
package com.example.spark.demo;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

// ★ポイント10
public class TodoService {

    private Map<String, Todo> store = new HashMap<String, Todo>();

    public Todo find(String todoId) {
        return store.get(todoId);
    }

    public void delete(String todoId) {
        store.remove(todoId);
        System.out.println("delete todoId : " + todoId);
    }
    
    public Todo update(Todo todo) {
        Todo updatedTodo = store.get(todo.getTodoId());
        if (updatedTodo != null) {
            updatedTodo.setTodoTitle(todo.getTodoTitle());
            updatedTodo.setFinished(todo.isFinished());
        }
        return updatedTodo;
    }

    public Todo create(Todo todo) {
        String todoId = UUID.randomUUID().toString();
        Todo registeredTodo = new Todo(todoId, todo.getTodoTitle(), new Date(),
                false);
        store.put(todoId, registeredTodo);
        System.out.println("registeredTodo : " + registeredTodo);
        return registeredTodo;
    }
}
```

**★ポイント10**
TODOアプリの業務ロジックを実装します。といってもSpark Frameworkの機能を利用していないので適当なダミー処理です。今回はDBアクセスを行わず、Mapを利用したインメモリのCRUD処理としました。

## 4. 実行可能jarの作成

マイクロサービスなので実行もシンプルにしたいと思います。
`uber.jar`のように実行可能な１つのjarファイルとしてビルドしたいと思います。

```xml:pom.xml
<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.spark.demo</groupId>
  <artifactId>spark-demo</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>spark-demo</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <!-- add spark framework -->
    <dependency>
      <groupId>com.sparkjava</groupId>
      <artifactId>spark-core</artifactId>
      <version>2.7.1</version>
    </dependency>
    <!-- add gson -->
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.2</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <!-- java8 -->
  <properties>
    <java.version>1.8</java.version>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <maven.compiler.source>${java.version}</maven.compiler.source>
  </properties>
  <!-- add for executable jar -->
  <build>
    <plugins>
      <plugin>
          <artifactId>maven-assembly-plugin</artifactId>
          <executions>
            <execution>
              <phase>package</phase>
              <goals>
                <goal>single</goal>
              </goals>
            </execution>
          </executions>
          <configuration>
            <archive>
              <manifest>
                <addClasspath>true</addClasspath>
                <mainClass>com.example.spark.demo.App</mainClass>
              </manifest>
            </archive>
            <descriptorRefs>
              <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
          </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```

実行可能jarファイルができたら`java -jar`で実行します。

```console
C:\tmp\spark\spark-demo>java -jar target/spark-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

## 5. さいごに

今回はSpark Frameworkを利用したマイクロサービスの実装について説明しました。
非常にシンプルで手軽に実装でき、また一つの実行可能jarファイルとしてビルドもできるので、リリースも簡単かと思います。
なお、サンプルでは入力チェックを省いていますが実際には必要となる処理です。JSONとして入力チェックを行う場合は[java-json-tools/json-schema-validator](https://github.com/java-json-tools/json-schema-validator)、Javaとして入力チェックを行う場合は[Bean Validation](http://beanvalidation.org/)がお勧めです。