1. Spark Frameworkとは
非常にシンプルなWebアプリケーションのフレームワークで、公式サイトでは以下のように説明されています。
Spark - A micro framework for creating web applications in Kotlin and Java 8 with minimal effort
githubで7199の評価がある(2018/3/4時点)ので、かなり使われているフレームワークのようです。
特徴としては、Webアプリケーションをラムダ式とstaticメソッドを使ってとても手軽に実装することができます。
以下は公式ドキュメントに掲載されているサンプルです。
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アプリの仕様
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でブランクプロジェクトを作成します。
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を利用することにしました
<!-- 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. アプリケーションのクラス
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のクラス
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として取得する方法はありません。手動で変換する必要があります。
詳細については公式ドキュメントのRoutesとRequestを参照ください。
★ポイント7
Spark Frameworkのフィルタという機能でWeb APIの前後に処理を追加することができます。
サンプルではパスが/api
配下のすべてのリクエストに対して、レスポンスヘッダのcontent-type
にapplication/json;charset=UTF-8
を設定するafter
フィルタを定義しました。
after
フィルタのほかにもbefore
、afterAfter
のフィルタがあります。フィルタの詳細については公式ドキュメントを参照ください。
3.3. フォーマット変換のクラス
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. 業務ロジックのクラス
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ファイルとしてビルドしたいと思います。
<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がお勧めです。