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を入力してください。

Slim3プロジェクトとの違い

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

  • scenic3-x.x.x.jar
  • Java CompilerのAnnotation Processingでのscenic3の登録
  • web.xml

自動生成されたプロジェクトの雛形には作成するページクラスの雛形として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 ProcessingのFactory 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つの目標としています。