この記事は Java Advent Calendar 2014 - Qiita における三日目の記事です。
ラムダ式を駆使してSSR対応のフレームワークを作ってみた
この記事では、僕が作った軽量なウェブフレームワークであるところのSidenを紹介します。
依存ライブラリを極力増やしたくないので、画面を構成するための機能やデータベースアクセスを行うための機能は搭載していません。
とりあえず使ってみよう
SidenはjcenterにデプロイしてありますのでビルドツールとしてGradleを使っている場合、以下のように依存性を宣言します。
apply plugin: 'java'
repositories.jcenter()
dependencies {
compile 'ninja.siden:siden-core:0.6.0'
}
sourceCompatibility = targetCompatibility = 1.8
簡単なHTTPレスポンス
Appクラスにどんなメソッドが定義されているのかを把握するとSidenを使えるようになります。
以下の例ではコンストラクタとgetメソッドとlistenメソッドを使っています。
getメソッドはHTTPのGETメソッドに対応しています。第一引数としてリクエストパスを設定します。これはsinatra風味になっているのでパスの途中で:(コロン)を使うとRequest#paramsメソッドでパスパラメータとして取り出せます。
JAX-RS等の真面目なURI Template実装だと{(ブレイス)をガンガン使う感じになって辛みがあるので、こういう仕様になっています。
import ninja.siden.App;
public class Main {
public static void main(String[] args) {
App app = new App();
app.get("/hello/:name",
(req, res) -> String.format("Hello %s !!",
req.params("name").get()));
app.listen();
}
}
Appクラスのlistenメソッドを使うとサーバがリクエストの待ち受けを開始します。デフォルトでは8080ポートが開きますので、このコードを実行したら
http://localhost:8080/hello/john
にアクセスすると、以下のようなレスポンスになります。
Cache-Control:no-cache, no-store, must-revalidate
Connection:keep-alive
Content-Length:13
Content-Type:text/plain; charset=UTF-8
Date:Mon, 01 Dec 2014 07:49:00 GMT
Expires:0
Pragma:no-cache
X-Content-Type-Options:nosniff
X-Frame-Options:SAMEORIGIN
X-XSS-Protection:1; mode=block
Hello john !!
デフォルトでは、開発モードになっているのでブラウザキャッシュが効き辛くなるようにCache-Control
、Pragma
、Expires
のヘッダーが設定されています。
また、X-Content-Type-Options
、X-Frame-Options
、X-XSS-Protection
ヘッダーを自動的に設定するため、ある種のJavaScriptが期待通りに動かない可能性がありますが、そういうものは危険なのでやめましょう。
より高度な機能について知りたければサンプルコードを見てください。
WebSocketを使う
次はWebSocketを使うコードを書いてみましょう。クライアントのjsやhtmlはSidenとは関係ないのでこちらを見てください。
ここではAppクラスのgetメソッドとwebsocketメソッドを使っています。
Appクラスにおけるgetメソッドの第二引数等のリクエストハンドラで返した値は標準APIの範囲であればSidenが何となく空気を読んでレスポンスに書き出します。以下のコードではjava.nio.file.Path
を返しているのでそのパスが指し示すファイルをレスポンスします。実装の詳細について知りたければRendererSelectorのコードを見てください。
import java.nio.file.Paths;
import ninja.siden.App;
public class UseWebsocket {
public static void main(String[] args) {
App app = new App();
app.get("/", (q, s) -> Paths.get("assets/chat.html"));
app.websocket("/ws").onText(
(con, txt) -> con.peers().forEach(c -> c.send(txt)));
app.listen(8181);
}
}
websocketメソッドを呼出すとWebSocketにおけるControl FrameとData Frameに対応したイベントハンドラを登録できます。この例ではData FrameとしてTextがクライアントから送信されてきた時に動作するイベントハンドラを登録しています。
第一引数であるConnectionにはWebSocketサーバを実装する上で便利なものを定義してあります。peersメソッドは同一のプロセスに接続しているWebSocketのコネクションを列挙できます。この例では、クライアントから送信されてきたデータをsendメソッドを使って配信しています。これによってチャットサーバとしての最低限の機能を実装しているのです。
React.js対応を使う
Sidenには画面を構成するような機能はありませんが、React.jsのサーバサイドレンダリングが盛り上がっているのでノリで実装してみたら意外と簡単だったので公開してあります。
React.jsのServerSideRendering(SSR)を使うには、Gradleに書いた依存ライブラリを以下のように変更します。
apply plugin: 'java'
repositories.jcenter()
dependencies {
compile 'ninja.siden:siden-react:0.2.0'
}
sourceCompatibility = targetCompatibility = 1.8
今回の説明でレンダリングするコンポーネントは以下のようなコードです。
/** @jsx React.DOM */
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
SidenでReact.jsを使うにはReactクラスをインスタンス化します。コンストラクタの第一引数はレンダリングしたいコンポーネントの名前です。今回はHelloMessage
ですね。二番目の引数はレンダリングされたコンポーネントのdivタグに設定するid属性です。クライアントサイドにおける動作とサーバサイドにおける動作の辻褄を合せるために必要です。
三番目の引数には複数のファイルを指定しています。最初のconsole-polyfill.jsは内部で利用しているJava8のNashornがconsoleオブジェクトをサポートしていないため必要です。次に本命のReact.js。僕が動作確認済なのは0.12.0です。サーバサイドレンダリング時に使うAPIがゴソっと変わっているので、これより古いバージョンのReact.jsは動きません。
これらは内部に抱え込んでもいいのですが最新のバージョンに追従する事や他の依存ライブラリ込みでビルドすることを考えると必要なものは個別に定義する方が良いと考えています。
今回は特にビルドしていないので、react-toolsで変換だけした後のjsを指定しています。
以下の例では、リクエストがくるたびにReactクラスのtoHtmlメソッドを使ってHTMLをレンダリングしています。toHtmlメソッドの引数型はStringになっていますが、必ずJSONをシリアライズした後のStringである必要があります。このようなインターフェースになっているのは、JavaにおけるJSONシリアライザはいくつもあり、そのどれも一長一短なので僕がライブラリを決めてしまうのが嫌だったからです。
import java.nio.file.Paths;
import java.util.Arrays;
import ninja.siden.App;
import ninja.siden.react.React;
public class UseReactSSR {
public static void main(String[] args) {
React rc = new React("HelloMessage", "content", Arrays.asList(
Paths.get("assets", "console-polyfill.js"),
Paths.get("assets", "react.js"),
Paths.get("build", "hello.js")));
App app = new App();
app.get("/", (q, s) -> {
String props = "{\"name\":\"john\"}";
return "<html><body>" + rc.toHtml(props) + "</body></html>";
}).type("text/html");
app.listen();
}
}
参考の為にオススメのJSONシリアライザを挙げておきます。
今週のオススメはboonです。理由は最も速度が速くメモリフットプリントが小さく、扱い易いAPIでコードベースが小さいからです。
より高度で実践的なサーバサイドレンダリングのサンプルが見たい方はコチラをどうぞ。
まとめ
Java8におけるラムダ式はJavaにおけるコードの表現に大きな変化をもたらしました。
また、ラムダ式に対応しているライブラリやフレームワークは徐々に増えてきているものの既存のメジャーなコードベースの多くはラムダ式でも動くだけでラムダ式を使いこなせてはいません。
この変化に対応する為には誰かが書いた記事を読むだけでなく、多くのコードを書いて動かすしかありません。
この記事を読んだ皆様がラムダ式を使ったコードを書きたくなるように願っています。