Murayama blog.

プログラミングと、その次の話

フレームワーク初学者のためのSpark Framework入門

最近、Javaのマイクロフレームワークがきてるようです。

http://postd.cc/java-micro-frameworks-the-new-trend-you-cant-ignore/

Javaフレームワーク事情

Javaに限らず、Webアプリケーション開発の世界ではフレームワークを使って開発することが一般的です。JavaにはSpringフレームワークや標準のJavaEEなど多くのフレームワークが存在しますが、これらのフレームワークは様々な要件を満たすため、多くの機能が含まれています。そのため、フレームワーク初学者にとって、フレームワークとはどういうものかを学習するという意味では全体像が掴みにくく、学習コストの高いものになっています。

昔はStrutsを勉強して、それからHibernateを勉強して、それからSpringやSeasar2を勉強する、という流れがありました。最近はいきなりSpringから入ることが多い?ようなので大変だろうなと思っています。Spring Bootもよさげですけどね。

マイクロフレームワーク

2016年現在、Webフレームワークの標準を作っているのはやはりRuby on Railsでしょう。Railsからインスパイヤされた(であろう)Webフレームワークは言語を問わず数多く存在します。これらは大変素晴らしいフレームワークですが、小さなWebアプリを開発する場合、たとえば簡易な掲示板を作成するようなシーンにおいては、やや大げさなものになります。

Railsが登場した後、Ruby界隈で、SinatraというWebアプリケーションフレームワークが注目されるようになります。SinatraRailsとは違って、シンプルで軽量なフレームワークです。簡単に言えば、HTTPリクエストと処理を関連づけるルーティング処理だけを実装したフレームワークです。またDSLで簡潔に処理を記述できるところも特徴の一つです。具体的には次のようなコードを実装します。

require 'sinatra'

get '/hi' do
  "Hello World!"
end

初めて見るとRubyのコードなの?という印象を受けるかもしれませんが、これはRubyの文法にしたがって記述されています。getというキーワードがありますが、これはHTTPのGETリクエストを処理することを意味しており、'/hi'の部分はURLを意味しています。つまり、GETリクエストで'/hi'というURLが要求された場合、あとのdo - endブロックが実行され、レスポンスとして"Hello World"が返却される仕組みになっています。

特定のドメイン(目的)に特化したコード表記法をDSLDomain Specific Language)と呼びます。Sinatraの場合はHTTPのルーティングに特化した表記法と考えることができます。こうすることで直感的で読みやすいコードを作ることができます。

Spark Frameworkとは

SparkはJavaで実装されたマイクロフレームワークです。Java8構文に従うことで少ない労力でWebアプリケーションを開発できます。

http://sparkjava.com/

ドキュメントが充実しているので、APIの使い方を一通り見ておくと良いでしょう。

http://sparkjava.com/documentation.html

チュートリアル1 - Hello World

ここではMavenでプロジェクトを作成し、Sparkフレームワークを使ったWebアプリケーションを作成します。また、Eclipseで開発できるようにプロジェクトをカスタマイズします。

まずはMavenでプロジェクトの雛形を作成します。グループIDはcom.example、アーティファクトIDはspark-appとしています。

$ mvn archetype:generate -DgroupId=com.example -DartifactId=spark-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

プロジェクトが作成されたら、cd spark-appを忘れないように実行しておきましょう。

次にpom.xmlを開いて、タグの中に以下の定義(Sparkフレームワーク)を追記します。

<dependency>
  <groupId>com.sparkjava</groupId>
  <artifactId>spark-core</artifactId>
  <version>2.5</version>
</dependency>

次にEclipseで作業できるように以下のmvnコマンドを実行します。

$ mvn eclipse:eclipse

これはeclipseプラグインeclipseゴールを指定しています。mvnコマンドにはフェーズやゴールといった引数を指定できます。mvn eclipse:eclipseによってEclipseプロジェクトとして必要な設定ファイルが作成できます。

mvn eclipse:cleanコマンドを実行すると設定ファイルを削除できます。

次にApp.javaを開いて、以下のように処理を実装します。import static spark.Spark.*; の指定を忘れないようにしてください。

package com.example.app;

import static spark.Spark.*;

import spark.Request;
import spark.Response;
import spark.Route;

public class App {
    public static void main(String[] args) {
        get("/hello", new Route() {
            @Override
            public Object handle(Request req, Response resp) throws Exception {
                return "Hello World";
            }
        });
    }
}

完成したApp.javaを実行します。サーバが起動するので以下のURLにアクセスします。

http://localhost:4567/hello

ブラウザに"Hello World"が表示されればOKです。

結果を確認したらApp.javaを忘れずに停止しておきましょう。Consoleビューの赤いボタンを押すとApp.javaは停止します。標準ではTomcatのようにオートリロードされないので、コードを修正する度にプログラムを再起動する必要があります。

チュートリアル2 - リクエストパラメータの表示

mainメソッドの中に2つ目のルーティング( "/echo")を追加してみましょう。ここではクエリパラメータではなく、URLの一部をリクエストパラメータと受け取るようにしています。

       get("/echo/:name", new Route() {
            @Override
            public Object handle(Request req, Response resp) throws Exception {
                return "echo:" + req.params("name");
            }
        });

完成したApp.javaを実行します。サーバが起動するので以下のURLにアクセスします。

http://localhost:4567/echo/Hello

ブラウザに"echo:Hello"が表示されればOKです。

チュートリアル3 - ビューテンプレートの使用

Thymeleafのインストール

Sparkは様々なビューテンプレートをサポートしています。ここではSpringでよく利用されるThymeleafを使ってみます。Thymeleafの詳細な情報については以下の公式サイトを参考にしてください。

SparkフレームワークからThymeleafを利用するためにMavenのpom.xmlに以下のライブラリを追記します。Sparkフレームワークの定義の下に追記すると良いでしょう。

<dependency>
  <groupId>com.sparkjava</groupId>
  <artifactId>spark-template-thymeleaf</artifactId>
  <version>2.3</version>
</dependency>

それからThymeleafのビューテンプレートを配置するディレクトリを作成しておきましょう。アプリケーションのルートフォルダで以下のコマンドを実行します。

$ mkdir -p src/main/resources/template

Windowsの場合は mkdir src¥main¥resources¥templateとしてください。

次にEclipseに更新を反映するために以下のmvnコマンドを実行します。

$ mvn eclipse:eclipse

その後Eclipse側でプロジェクトを選択して、Refreshを選択してください。thymeleafが反映されます。

Thymeleafを使うには

mainメソッドの中に3つ目のルーティング( "/books")を追加してみましょう。/booksではshopNameとbooksをモデルとして作成し、ビューにレンダリングします。

       get("/books", new TemplateViewRoute() {
            @Override
            public ModelAndView handle(Request req, Response resp) throws Exception {
                Map<String, Object> model = new HashMap<String, Object>();
                model.put("shopName", "MyBookStore");

                List<String> books = Arrays.asList("Java Book", "PHP Book", "Ruby Book");
                model.put("books", books);
                return new ModelAndView(model, "index");
            }
        }, new ThymeleafTemplateEngine());

getメソッドの第2引数にはこれまでのRouteインタフェースではなく、TemplateViewRouteインタフェースを指定している点に注意してください。TemplateViewRouteインタフェースは戻り値にModelAndViewオブジェクトを返却します。ModelAndViewにはバインドするモデルと、レンダリングするビューのプレフィックスを指定します。プレフィックスには"index"を指定しているので、src/main/resources/templatesディレクトリにあるindex.htmlが呼び出されるようになります。

ビューテンプレート(index.html)の作成

Thymeleafのビューテンプレートindex.htmlを作成します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>BookStore</title>
</head>
<body>
    <h1 th:text="${shopName}">BookStore</h1>
    <ul>
        <li th:each="book : ${books}" th:text="${book}">hoge</li>
    </ul>
</body>
</html>

ThymeleafはXMLのバリデーションが働くのでタグの閉じ忘れに注意してください。

それではApp.javaを実行します。サーバが起動するので以下のURLにアクセスします。

http://localhost:4567/books/

ショップタイトルと本の一覧が表示されればOKです。

例外処理について

ビューファイル名を間違えたり、Thymeleafで正しくデータバインディング出来ない場合は例外が発生します。標準では例外のスタックトレースが表示されないので、ルーティング定義の後に、以下のように例外ハンドラを定義しておきましょう。

    exception(Exception.class, new ExceptionHandler() {
      @Override
      public void handle(Exception e, Request req, Response resp) {
        e.printStackTrace();
        resp.status(200);
        resp.body(e.getMessage());
      }
    });

これでルーティングの中で例外が発生しても、コンソールにスタックトレースが表示されるようになります。

App.java

ここまでのApp.javaプログラムを再掲しておきます。

package com.mycompany.app;

import static spark.Spark.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import spark.ExceptionHandler;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.TemplateViewRoute;
import spark.template.thymeleaf.ThymeleafTemplateEngine;

public class App {
    public static void main(String[] args) {

        get("/hello", new Route() {
            @Override
            public Object handle(Request req, Response resp) throws Exception {
                return "Hello World";
            }
        });

        get("/echo/:name", new Route() {
            @Override
            public Object handle(Request req, Response resp) throws Exception {
                return "echo:" + req.params("name");
            }
        });

        get("/books", new TemplateViewRoute() {
            @Override
            public ModelAndView handle(Request req, Response resp) throws Exception {
                Map<String, Object> model = new HashMap<String, Object>();
                model.put("shopName", "MyBookStore");

                List<String> books = Arrays.asList("Java Book", "PHP Book", "Ruby Book");
                model.put("books", books);
                return new ModelAndView(model, "index");
            }
        }, new ThymeleafTemplateEngine());

        exception(Exception.class, new ExceptionHandler() {
            @Override
            public void handle(Exception e, Request req, Response resp) {
                e.printStackTrace();
                resp.status(200);
                resp.body(e.getMessage());
            }
        });
    }
}

マイクロフレームワーク、シンプルでいいですね。

Java8で書く

もっとシンプルです。

package com.mycompany.app;

import static spark.Spark.exception;
import static spark.Spark.get;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import spark.ModelAndView;
import spark.template.thymeleaf.ThymeleafTemplateEngine;

public class App {
    public static void main(String[] args) {

        get("/hello", (req, resp) -> "Hello World");

        get("/echo/:name", (req, resp) -> "echo:" + req.params("name"));

        get("/books", (req, resp) -> {
            Map<String, Object> model = new HashMap<String, Object>();
            model.put("shopName", "MyBookStore");
            List<String> books = Arrays.asList("Java Book", "PHP Book", "Ruby Book");
            model.put("books", books);
            return new ModelAndView(model, "index");
        } , new ThymeleafTemplateEngine());

        exception(Exception.class, (e, req, resp) -> {
            e.printStackTrace();
            resp.status(200);
            resp.body(e.getMessage());
        });
    }
}

Java8いいですね。