30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

1. Spark Frameworkとは

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

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

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

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

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アプリの仕様

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でブランクプロジェクトを作成します。

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を利用することにしました

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です。

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

3. ソースコード

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

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メソッドに切り出しました。
今回はよく利用しそうな以下の3点の初期設定を行いました。

  • サーバポートの変更

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のクラス

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)のクラスのインスタンスを生成します。
2つのクラスについては後程説明します。

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

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

詳細については公式ドキュメントのRoutesRequestを参照ください。

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

afterフィルタのほかにもbeforeafterAfterのフィルタがあります。フィルタの詳細については公式ドキュメントを参照ください。

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

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. 業務ロジックのクラス

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のように実行可能な1つのjarファイルとしてビルドしたいと思います。

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で実行します。

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、Javaとして入力チェックを行う場合はBean Validationがお勧めです。

30
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?