Hatena::ブログ(Diary)

やさしいデスマーチ このページをアンテナに追加 RSSフィード

札幌のWebエンジニアの綴る日常と開発の日々。
GoogleAppEngine/slim3/Django/NetBeans/Swing/JavaFXを中心にお届け。

2011-01-16

Slim3 pluginでScenic3の使い方

| 22:33 |  Slim3 pluginでScenic3の使い方を含むブックマーク

Slim3には簡単にプロジェクトを作成する為に使えるEclipse pluginがあります。@tomotaro1065 さんが中心になって作られていますが、ご厚意でScenic3の対応もしていただいています。ですが、自分で使ってみて使い方が解らないのではないか?と気付きました。そこで、Scenic3の仕組みを含めてチュートリアルを書く事にします。内容は後でドキュメントに反映させるつもりです。

Scenic3ってなにさ?

scenic3は t2 frameworkのようなPageクラスをslim3で実現するslim3の拡張ライブラリです。

Scenic3はSlim3を薄くラップしたライブラリで、Slim3設計思想である「"Simple" and "Less Is More"」を踏襲しつつ、1つのPageクラスに複数のアクションメソッドを記述できるようになります。spin-upへの影響は最小限になるようにデザインされています。

何が嬉しいの?

Slim3ではリクエストのパスからControllerを動的に生成するアプローチを採用しています。これはGAEというプラットフォーム上では最善の方法の1つです。しかし、リクエストパスの数だけコントローラクラスを生成しなければならないという事になります。クラスの責務としては1コントローラ=1クラスとする事は1つの選択です。しかし、あるモデルのCRUDに対応するアクションを作るならば、同じクラスに記述してまとめるのも1つの選択肢です。Scenic3では後者の設計を採用する場合に、効果的な方法を提供します。

プロジェクトの作成

New - Project - Slim3 Projectと進んだ後、プロジェクトの種別から「Use MVC of Slim3 with Scenic3」を選択します。通常のSlim3プロジェクトと同様にProject NameとRoot Packageを入力してください。

f:id:shuji_w6e:20110116222650p:image

Slim3プロジェクトとの違い

Slim3プロジェクトとの違いは次の3点です。

自動生成されたプロジェクトの雛形には作成するページクラスの雛形としてFrontPageが生成されています。

scenic3-x.x.x.jar

scenic3-x.x.x.jarはScenic3が必要とする唯一のライブラリです。実行時に必要となりますので、WEB-INF/libに配置されています。

Java CompilerのAnnotation Processingでのscenic3の登録

APTを行う為にscenic3-x.x.x.jarが、Java CompilerのAnnotation ProcessingFactory Pathとして登録されています。APTコンパイル時に幾つかのクラスを生成するために使用されます。また、Slim3のルートパッケージをScenic3に伝えるために、slim3.rootPackageというパラメータがOptionで指定されています。

web.xml

FrontControllerを差し替える必要があるため、FrontControllerがorg.slim3.controller.ScenicFrontControllerに変更されています。プロジェクトで固有の拡張を行う場合は、ScenicFrontControllerのサブクラスを指定してください。

    <filter>
        <filter-name>FrontController</filter-name>
        <filter-class>org.slim3.controller.ScenicFrontController</filter-class>
    </filter>

Pageクラス

Pageクラスはアノテーションを付与した ScenicPageクラスのサブクラスです。

Scenic3を使う場合はControllerのサブクラスではなくScenic3のサブクラスとなりますが、runメソッドの中身についてはほとんど同じように記述できます。したがって、Slim3を使ったコントローラを作る事とScenic3を使ったPageクラスを作る事で覚えることに違いはほとんどありません。

現在のプラグインでは、Pageクラスはルートパッケージ直下に配置されていますが、自分の場合はpageパッケージを作成して配置しています。Pageクラスはどのパッケージにあっても構いません。

自動生成されたPageクラス

package example.scenic3;

import org.slim3.controller.Navigation;

import scenic3.ScenicPage;
import scenic3.annotation.ActionPath;
import scenic3.annotation.Default;
import scenic3.annotation.Page;
import scenic3.annotation.Var;

@Page("/")
public class FrontPage extends ScenicPage {
    // /view/100  /view/200
    @ActionPath("view/{id}")
    public Navigation view(@Var("id") String id) {
        super.request.setAttribute("id", id);
        return forward("/view.jsp");
    }

    // /
    @Default
    public Navigation index() {
        return forward("/index.jsp");
    }
}
Page, ActionPathアノテーション

実行時に選択されるPageクラスとActionメソッドは、Pageクラスに記述されたPageアノテーションとActionPathに記述されたパスで決まります。サンプルではルートパスの下にあり、viewで始まるアクションはviewメソッドが実行され、それ以外の全てのパスはindexが実行されます。また、{id}という書式を使う事で、パスに含まれるパラメータをキャプチャする事が可能です。

Slim3ではRooting機能を使って実現しますが、Scenic3では直感的な記述で、かつメソッドパラメータとして受け取ることが出来るので便利になっています。同様にリクエストパラメータやHttpServletRequestが欲しい場合は、メソッド引数に記述するだけでOKです。

詳細はドキュメントを確認してください。

仕組み

Scenic3ではPageクラスからAPTでControllerクラスを生成します。

生成されたControllerクラスは直ぐに確認できますが、単純にPageクラスのメソッドに処理を委譲しているに過ぎません。

次のようなControllerクラスは、メソッド毎に生成されます。

// Controller for example.scenic3.FrontPage#view
// @javax.annotation.Generated
public final class _view_id extends scenic3.ScenicController {

    private final example.scenic3.FrontPage page;

    public _view_id() {
        this.page = new example.scenic3.FrontPage();
    }

    @Override
    public final org.slim3.controller.Navigation run() throws Exception {
        setupPage(page);
        return page.view(super.var("id"));
    }
    // 以下略
}

しかし、全てのパターンをspin-up時に解析してしまうとパフォーマンス上の問題が発生します。

そこで、パターンマッチングに関しては別のクラスを用意するアプローチをとっています。

そのマッチングクラスを登録するのが次で説明するMatcherとAppUrlsです。

Matcher

Matcherは、リクエストのパスから自動生成されたControllerを選択するためのクラスで、Pageクラス毎に生成されます。

// @javax.annotation.Generated
public class FrontPageMatcher extends scenic3.UrlMatcherImpl {
    // 中略
    // Constractor.
    private FrontPageMatcher() {
        super("/");
        super.add(new scenic3.UrlPattern("/", "view/{id}"), "example.scenic3.controller._view_id");
        super.add(new scenic3.UrlPattern("/", ""), "example.scenic3.controller.$Index");
    }
}

このようにリクエストパスのマッチングパターンを生成し、対応するクラスの名前を文字列で保持します。

クラス名で保持しないのはクラスローディングを極力遅らせるためです。

AppUrls

AppUrlsは、Matcherを複数持つコンテナです。

このクラスは自動生成しない為、新しいPageを作成したら手動でMatcherを登録する必要があります。

import scenic3.UrlsImpl;
import example.scenic3.controller.matcher.FrontPageMatcher;

public class AppUrls extends UrlsImpl {

    public AppUrls() {
        excludes("/css/*");
        add(FrontPageMatcher.get());
        // TODO Add your own new PageMatcher
    }
}

理由としては除外パスの設定はユーザ毎に行う為と、マッチングを賭ける順序はプログラマで制御する方が良いからです。自分でもたまに登録を忘れますのでご注意ください。

Test

Scenic3ではテストもページクラス単位で可能です。

import scenic3.tester.PageTestCase;

public class FrontPageTest extends PageTestCase {

    @Test
    public void index() throws Exception {
        tester.start("/");
        assertThat(tester.getActionMethodName(), is("index"));
        assertThat(tester.getDestinationPath(), is("index.jsp"));
    }
}

PageTestCaseを使用するだけで、書き方はほとんど変わりません。

影響を受けたフレームワークとか

PageとActionPathのアイディアはT2フレームワークそのものです。開発の経緯としてはT2フレームワークGAE上で使っている内にパフォーマンスの問題に直面し、「ならば最適に作り直そう」という流れです。APTで自動生成し、動的に生成しないポリシーは、Slim3から受け継いでいます。URLのマッチングの仕組みはDjangoからヒントを得ています。マッチングの仕組みについてはScalaからヒントを得ました。

最後に

Slim3にScenic3を組み合わせることで、コードの見通しがぐっと良くなります。

Controllerは自動生成されるため、メソッド名を変えても直ぐに反映されるでしょう。

パスとコントローラクラスが1:1の場合に発生するリファクタリングのやりにくさとコントローラクラスの爆発を避けることが可能です。勿論,設計ポリシー的にはControllerを作る方が良いのでケースバイケースで利用を検討してみてください。少なくともScenic3を使う為に必要な知識は今回のエントリーで紹介した事くらいです。

尚、さらにJSPも使いたくないよという人にはpirkaengineも組み合わせることがオススメです。こちらについてはまた別の機会に書こうと思います。また、pirka-mobileを組み合わせるとガラケー向けのサイトがGAEでサクサクかけるようになる、というのを1つの目標としています。

調査員調査員 2011/02/03 11:22 こんにちは。
Scenic3を利用しようかと調査しているところなのですがMatcherの自動生成について質問です。
テストで「foo.TestPage(@Page("/"))」と「foo.bar.TestPage(@Page("/test"))」を作成してみたところ、Controllerは両方生成されたのですが、MatcherはTestPageMatcherが一つだけ生成され"/test"の設定しかありませんでした。
別パッケージ同名Pageクラスはサポートされていないのでしょうか?

shuji_w6eshuji_w6e 2011/02/03 12:33 コメントありがとうございます。

>別パッケージ同名Pageクラスはサポートされていないのでしょうか?
現在はサポートしていません。
理由としては、
・基本的には同一階層にPageクラスを配置する事を想定
・Matcherの命名規則やパッケージが無駄に複雑になる
・EtupikraCMSで使っている範囲では必要がないように感じた
といった所です。

尚、EtupirkaCMSでは次のようなルールでPageクラスを運用しています。
<rootPackage>.page.XxxPage .. 基本ページ群
<rootPackage>.plugins.<pllugin_name>.PluginNameXxxPage .. プラグイン用ページ群
パッケージ対応が必要であれば検討しますが、いかがでしょうか?

調査員調査員 2011/02/03 13:55 回答ありがとうございます。Matcherの件了解しました。
こちらが想定していた使い方は、
・PC用のページ -> foo.p.MainPage
・携帯用のページ -> foo.m.MainPage
・スマフォ用のページ -> foo.s.MainPage
といったような感じなので、それぞれのPageクラスを一意の名前に変更する事でも問題ありません。
(将来的に対応してもらえたら嬉しいですが、それほど重要ではありません)

で、連続で申し訳ないですがもう一件ほど。
AppUrlsにMatcherを登録する順番で実行されるPageクラスのメソッドが変わってしまう現象が発生しています。

例として以下の用なページを作成します。(詳細は省略)

@Page("/")
FrontPage {
 @Default
 public Navigation index() { ... }

 @ActionPath("foo")
 public Navigation foo() { ... }
}

@Page("/foo")
FooPage {
 @Default
 public Navigation index() { ... }

 @ActionPath("{param}")
 public Navigation param(@Var("param") String param) { ... }
}

AppUrlsに「FrontPageMatcher、FooPageMatcher」の順で登録したところ以下のようになりました。
/ -> FrontPage#index
/foo -> FrontPage#foo
/foo/ -> FrontPage#index
/foo/1234 -> FrontPage#index

次に「FooPageMatcher、FrontPageMatcher」の順で登録したところ今度は以下のようになりました。
/ -> FrontPage#index
/foo -> FrontPage#foo
/foo/ -> FooPage#index
/foo/1234 -> FrontPage#param

こちらが期待した動きは後者の方なのですが、登録順によって切り替わってしまうのは想定した動きなのでしょうか?

shuji_w6eshuji_w6e 2011/02/03 22:20 >同名Pageクラスの対応について
デフォルトでは現行のままですが、パッケージを指定できるようなオプションでの対応を検討します。

>Matcherの挙動について
コメントの通りの挙動は想定通り(登録順にマッチング)です。
なので、浅い階層のパスを持ったPageを下に登録する事が推奨です。
チェック等を行っても良いのですが、spin-up時の処理を可能な限り減らしたい理由で行っていません。

ロジック的には@Pageのパスで先頭マッチを行い、最初に対応するMatcherを選択します(最大Matcher回の比較)
続けて、@Page内の各@Actionでのマッチを行います(最大でAction回の比較)
なので/のPageを先頭に持ってくると、全てマッチしてしまうのです。
これもマッチングの処理を少なくしてパフォーマンスを落とさないための仕様になっています。

この辺りは今週中にドキュメントに補足しておきますね。

調査員調査員 2011/02/04 09:24 >パッケージを指定できるようなオプションでの対応を検討します。
ありがとうございます。

>コメントの通りの挙動は想定通り(登録順にマッチング)です。
spin-up対応なのですね、納得しました。

>この辺りは今週中にドキュメントに補足しておきますね。
よろしくお願いします。

丁寧な回答ありがとうございました。
Scenic3とPirkaに大変期待しています。

トラックバック - http://d.hatena.ne.jp/shuji_w6e/20110116/1295184829