enkanとkotowari 〜 Java9時代の新しいマイクロフレームワーク

  • 148
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

現在ではSpring Bootの手軽さに軒並み飲み込まれた感がありますが、Javaのマイクロフレームワークはちょっとしたブームでした。
Javaのマイクロフレームワーク ― この新トレンドは見逃せない

enkanは、RackやExpress.jsで実装されているミドルウェアパターンをJavaで実装した、現在ファーストリリースへ向け開発中の新しいマイクロフレームワークです。

Java9のREPLを活用して開発・運用を楽にする機能も実装(予定)です。

なぜ今さら新しいWebフレームワークを?

Spring BootやJava EEは、高度なDIとたくさんのアノテーションでフレームワーク内部の動きはブラックボックス化されます。これを完全にブラックボックスのまま、実用的なWebアプリケーションを完成させるのは、実際のところ難しく、フレームワークの内部を覗きにいかなくてはなりませんが、これが結構ハマりポイントになるようです。

また、これらの高度なDIコンテナ技術は(デフォルトだと)大量のクラスをスキャンしたり、多くの設定をしたりで、アプリケーションの高速な起動から離れていく方向にあるように思えます。JVMの起動がだいぶん高速化された現代において、アプリケーション自体が高速に起動するフレームワークが必要だと考えたわけです。

そこでClojureにミニマルでSimple made easyを地で行く、ductというフレームワークがあるので、このフレームワークとアーキテクチャスタックをJavaに移植し、それをベースにその他必要なものを開発する至りました。(ringを和訳してenkan(円環)です)

特長

コンセプトは大きく分けて3つあります。

  1. Minimal (Simple made easy)
  2. Ease of development
  3. Ease of operation

Minimal

:small_blue_diamond: ミドルウェアパターン

Java以外の言語では、Webのフレームワークはミドルウェアパターンを実装したものが隆盛しています。Rack、Express/connect、ring-clojureのようなものはミドルウェアを適用していくところだけを基本機能と持っていて、ミドルウェアによって機能を足していくしかけです。Servlet APIのFilterと似ていますが、Session管理やHTTPリクエストのパース処理などもすべてミドルウェアで構成されるので、Webアプリケーションの構成が理解しやすく、拡張がより容易な点がメリットです。

  • 特定のリクエストをSorryページに飛ばす
  • セッション管理
  • フラッシュスコープ
  • クッキーのデコード・エンコード
  • クエリーストリングの解析
  • トレースログの出力
  • Railsライクなルーティング (kotowari)
  • フォームへのバインディング (kotowari)
  • JSR-303 を使ったフォームのバリデーション (kotowari)
  • コントローラへのディスパッチ (kotowari)

などが、ミドルウェアとして提供され、これを組み合わせることでWebアプリケーションの基本機能が実現できます。

:small_blue_diamond: 設定ファイルレス

enkanでは一切の設定ファイルを無くし、必要ならばJavaで書く方針にしています。環境によって変わるものは12-factor appにしたがい、環境変数で指定します。

// Routing
Routes routes = Routes.define(r -> {
    r.get("/").to(ExampleController.class, "index");
    r.post("/login").to(LoginController.class, "login");
    r.resource(CustomerController.class);
}).compile();

app.use(builder(new MethodOverrideMiddleware())
        .set(MethodOverrideMiddleware::setGetterFunction, "_method")
        .build());
app.use(new ResourceMiddleware());
app.use(new RenderTemplateMiddleware());
app.use(new RoutingMiddleware(routes));
app.use(new DomaTransactionMiddleware<>());
app.use(new FormMiddleware());
app.use(new ValidateFormMiddleware());
app.use(new HtmlRenderer());
app.use(new ControllerInvokerMiddleware(injector));

デフォルトから挙動を変える場合も、汎用ビルダーを使って、そこそこスマートかつValidatableに設定を記述できます。

:small_blue_diamond: ブラックボックスを避け、黒魔術を使わない

オブジェクトの生成が暗黙的に行われたり、書いた覚えがないけど設定がされていて上手く動いている、なんてのは便利な半面、ハマりのポイントになりかねません。

enkanでは必要最小限のDI機能をもち、それ以外は使う人が明示的にオブジェクト生成するように設計してあります。それでいながら、動的Mixin汎用ビルダーなどによって、記述量を減らす設計になっています。

:small_blue_diamond: アノテーションを最小限に

設定ファイルを少なくするために、アノテーションをコードに貼り付けるのでは問題は解決してはいないですよね。ものによっては設定が分散してしまって全体像が分かりにくくなる弊害のほうが大きくなるような気がしています。

特にルーティングの設定なんてものは、コントローラメソッド毎にぺたぺたマッピングを貼り付けるのではなく、全体像を見ながら決めたいですよね。

enkan+kotowariを使った典型的なCRUDのコントローラは以下のようなものですが、必要なアノテーションはコンポーネントをDIするための@Injectと、トランザクション制御用の@Transactionのみです。

public class CustomerController {
    @Inject
    private TemplateEngine templateEngine;

    @Inject
    private DomaProvider daoProvider;

    @Inject
    private BeansConverter beans;

    public HttpResponse index() {
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        List<Customer> customers = customerDao.selectAll();
        return templateEngine.render("customer/list",
                "customers", customers);
    }

    public HttpResponse show(Parameters params) {
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        Customer customer = customerDao.selectById(params.getLong("id"));
        return templateEngine.render("customer/show", "customer", customer);
    }

    public HttpResponse newForm() {
        return templateEngine.render("customer/new",
                "customer", new CustomerForm());
    }

    @Transactional
    public HttpResponse create(CustomerForm form) {
        if (form.hasErrors()) {
            return templateEngine.render("customer/new", "customer", form);
        }
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        Customer customer = beans.createFrom(form, Customer.class);
        customerDao.insert(customer);
        return redirect(getClass(), "index", SEE_OTHER);
    }

    public HttpResponse edit(Parameters params) {
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        Customer customer = customerDao.selectById(params.getLong("id"));
        CustomerForm form = beans.createFrom(customer, CustomerForm.class);
        return templateEngine.render("customer/edit",
                "id", params.getLong("id"),
                "customer", form);
    }

    @Transactional
    public HttpResponse update(Parameters params, CustomerForm form) {
        if (form.hasErrors()) {
            return templateEngine.render("customer/edit",
                    "id", params.getLong("id"),
                    "customer", form);
        }
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        Customer customer = customerDao.selectById(params.getLong("id"));
        beans.copy(form, customer);
        customerDao.update(customer);
        return redirect(getClass(), "show?id=" + customer.getId(), SEE_OTHER);
    }

    @Transactional
    public HttpResponse delete(Parameters params) {
        CustomerDao customerDao = daoProvider.getDao(CustomerDao.class);
        Customer customer = customerDao.selectById(params.getLong("id"));
        customerDao.delete(customer);

        return redirect(getClass(), "index", SEE_OTHER);
    }

}

:small_blue_diamond: 依存ライブラリを最小限に

フレームワーク自体がミニマルだからといって、多くのライブラリに依存していては、クラウドへのデプロイなどで結局時間を喰う原因になります。

コンポーネントを除く、enkanのコア機能の依存ライブラリは、JavaEE7のAPIとslf4jのAPIのみです。Undertowコンポーネントを使えば、Servlet APIにすら依存しなくなります。

:small_blue_diamond: シングルインスタンス

ミドルウェアやコンポーネント、コントローラはすべてシングルインスタンスです。特にコンポーネントはJavaの従来のDIコンテナのように、複数のスコープを持てないので、スコープの違いによる問題は発生しないことになります。

Ease of Development (開発容易性)

:small_blue_diamond: ホットデプロイ

Enkanはクラスローダー破棄型のクラスリローディング機能をもっています。REPLから/resetコマンドを打てば、1秒かからず最新のクラスに入れ替わります。/autoresetにしておけば、クラスの変更を自動検出してホットデプロイ出来るようになります。

現在の仕組みは、Seasar2のホットデプロイと同じで、原則開発向けの機能ですが、SO_REUSEPORT(Java9から通常APIで使えるようになる?)を利用した本番環境向けのホットデプロイ機能も実装予定です。

:small_blue_diamond: 設定ミスの警告

Webアプリを作っていると、フレームワークやルーティングの設定ミスで、それに気付かず時間を使っちゃうことがままあります。Enkanでは設定ミス専用のExceptionがあり、設定ミスがあると丁寧なメッセージですぐに修正することができるでしょう。

image

こういう具合に、設定ミスの場合は問題と解決策がセットで出るようになります。

Ease of Operation (運用容易性)

:small_blue_diamond: 高速な起動

REPLを立ち上げた状態から1秒〜3秒で起動し、HTTPリクエストを受付可能になります。
ポイントは一切のクラススキャンを廃止し、明示的なオブジェクトの生成と依存関係の宣言するようにしていいるところにあります。

:small_blue_diamond: メトリクス

これはコンポーネントの機能の1つですが、DropwizardのMetrisを使って、システムの状態をREPLより取得できるようになります。

enkan> /metrics
-- Active Requests ----------------------------------
             count = 0
-- Errors ------------------------------------
             count = 0
         mean rate = 0.00 events/s
     1-minute rate = 0.00 events/s
     5-minute rate = 0.00 events/s
    15-minute rate = 0.00 events/s
-- Request Timer -----------------------------
             count = 408
         mean rate = 1.87 calls/sec
     1-minute rate = 1.87 calls/sec
     5-minute rate = 1.10 calls/sec
    15-minute rate = 0.42 calls/sec
               min = 0.00 sec
               max = 0.16 sec
              mean = 0.00 sec
            stddev = 0.01 sec
            median = 0.00 sec
              75% <= 0.00 sec
              95% <= 0.00 sec
              98% <= 0.00 sec
              99% <= 0.01 sec
            99.9% <= 0.16 sec

:small_blue_diamond: REPL

もっとも他のフレームワークと違うところがREPLでしょうか。Enkanには標準でREPLが付いていて、これでアプリケーションの起動・停止、各種メトリクスのモニタリング、ミドルウェアの振る舞いの動的編集が可能になります。

現段階では、なんちゃってREPL(pseudo repl)が付いているのみですが、近い将来jshellが使えるようにする予定です。

REPLを起動し、enkanアプリケーションに接続し、/startコマンドを実行すると、サーバが瞬時に起動します。

REPL> /connect 18080
Connected to enkan.
REPL> /start
[pool-1-thread-1] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-0 - is starting.
[pool-1-thread-1] INFO org.flywaydb.core.internal.util.VersionPrinter - Flyway 3.2.1 by Boxfuse
[pool-1-thread-1] INFO org.flywaydb.core.internal.dbsupport.DbSupportFactory - Database: jdbc:h2:mem:test (H2 1.4)
[pool-1-thread-1] INFO org.flywaydb.core.internal.command.DbValidate - Validated 1 migration (execution time 00:00.019s)
[pool-1-thread-1] INFO org.flywaydb.core.internal.metadatatable.MetaDataTableImpl - Creating Metadata table: "PUBLIC"."schema_version"
[pool-1-thread-1] INFO org.flywaydb.core.internal.command.DbMigrate - Current version of schema "PUBLIC": << Empty Schema >>
[pool-1-thread-1] INFO org.flywaydb.core.internal.command.DbMigrate - Migrating schema "PUBLIC" to version 1 - CreateCustomer
[pool-1-thread-1] INFO org.flywaydb.core.internal.command.DbMigrate - Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.059s).
2 02, 2016 7:58:35 午後 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.2.Final
[pool-1-thread-1] INFO org.eclipse.jetty.util.log - Logging initialized @2688228ms
[pool-1-thread-1] INFO org.eclipse.jetty.server.Server - jetty-9.3.5.v20151012
REPL> [pool-1-thread-1] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@5325abc3{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
[pool-1-thread-1] INFO org.eclipse.jetty.server.Server - Started @2688295ms

ルーティングやミドルウェアの状態もコマンドから参照可能です。

ミドルウェアの一覧を表示する
REPL> /middleware app list
ANY   defaultCharset (enkan.middleware.DefaultCharsetMiddleware@4929dbc3)
NONE   serviceUnavailable (enkan.middleware.ServiceUnavailableMiddleware@2ee4fa3b)
ANY   stacktrace (enkan.middleware.StacktraceMiddleware@545872dd)
ANY   trace (enkan.middleware.TraceMiddleware@1c985ffd)
ANY   contentType (enkan.middleware.ContentTypeMiddleware@1b68686e)
ANY   httpStatusCat (enkan.middleware.HttpStatusCatMiddleware@12d47c1a)
ANY   params (enkan.middleware.ParamsMiddleware@58d3a07)
ANY   normalization (enkan.middleware.NormalizationMiddleware@5b34eafc)
ANY   cookies (enkan.middleware.CookiesMiddleware@347c2ec)
ANY   session (enkan.middleware.SessionMiddleware@32424a32)
ANY   resource (enkan.middleware.ResourceMiddleware@5e73037f)
ANY   routing (kotowari.middleware.RoutingMiddleware@226c7147)
ANY   domaTransaction (enkan.middleware.DomaTransactionMiddleware@1f819744)
ANY   form (kotowari.middleware.FormMiddleware@3f325d5c)
ANY   validateForm (kotowari.middleware.ValidateFormMiddleware@791cd93e)
ANY   htmlRenderer (enkan.middleware.HtmlRenderer@383b6913)
ANY   controllerInvoker (kotowari.middleware.ControllerInvokerMiddleware@2b13e2e7)

例えば、アプリケーションを停止せずに一時的にメンテ中にし503を返すようにしたい場合、よくデータベースにフラグをもって、それをみてSorryに飛ばすかどうか判定するみたいな作りをすることがあるようですが、enkanではデータベースに聴きにいく必要はありません。ミドルウェアの設定をREPLから変えるだけです。

Sorryページに飛ばすようにする
REPL> /middleware app predicate serviceUnavailable ANY
REPL> /middleware app list
ANY   defaultCharset (enkan.middleware.DefaultCharsetMiddleware@4929dbc3)
ANY   serviceUnavailable (enkan.middleware.ServiceUnavailableMiddleware@2ee4fa3b)
ANY   stacktrace (enkan.middleware.StacktraceMiddleware@545872dd)

構成

EnkanはClojureのductのJava移植なので、ductのアーキテクチャスタックに応じてプロジェクトが別れています。

  • enkan-core: ring相当
  • enkan-system: component + nrepl 相当
  • kotowari: compojure相当

componentは、Javaの世界のふつうのコンテナ管理されるコンポーネントと似た概念ですが、その目的がもう少し絞り込まれています。
componentとは、アプリケーション全体で状態管理する必要があるハコです。そしてcomponentにはstart/stopのインタフェースが実装される必要があります。これによってサーバの再起動を安全におこなうことが出来るようになります。

image

[WIP] コンポーネント

enkanで用意されているコンポーネントは、現在主要なもので以下が利用可能です。

  • Webサーバ: Undertow, Jetty
  • DataSource: HikariCP
  • ORマッパー: Doma2
  • DBマイグレーション: Flyway
  • HTMLテンプレート: FreeMarker

まとめ

Spring BootとJava EEの2強の時代に、今さら感のある新しいフレームワークですが、起動の速さと開発・運用のしやすさではアドバンテージがあります。
そしてClojurianが仕方なくJavaを使わなきゃいけないときのベストチョイスになるよう開発進めていきたいと思います。

https://github.com/kawasima/enkan