折角なので Apache Wicket 8 についてまとめておきたい

  • 3
    いいね
  • 0
    コメント

この記事は Java EE Advent Calendar 2016 の 12/23 担当分です。

昨日は @HirofumiIwasaki さんの「JavaOne 2016を経てのJava EE 8 (revised)の将来像」でした。明日は @kikutaro さんです。

はじめに

さて、最近はホットなトピックになることが少ない、Javaのコンポーネント指向Webフレームワークですが、2000年代後半から継続して開発が続けれている代表的なものには、JSF、Apache Wicket、Vaadin などが挙げられます。

Apache Wicketの利用事例は、Twitterアカウントなどで公開されているものは海外のものが中心ですが、国内では多賀城市のアーカイブサイト警察庁のサイトの意見箱でも使われているという報告が見られます。Wicket自体も、コミュニティによる開発が継続的に続けられており、 Java 8 に対応する Wicket 8.0 の開発版がリリースされはじめています。

Wicket 8.0 では、Java 8のLambda式がWicketのコンポーネントやモデルの中で使えるようになります。Javaプログラマが開発しやすいWebフレームワークというスタンスに惚れ込んだWicketユーザ達にとって「いつ来るの?」とやきもきしながら待ち望んだ改善と言えます。

そんなことを背景に、この記事では、Wicket 8のマイグレーションガイド:Migration to Wicket 8.0 や、ApacheCon EU 2016でのコミッタによる資料を参考に、今後リリースされる Wicket 8.0 からのコードの書き方の変化をまとめておきたいと思います。

なお、記事の内容は執筆時点の Wicket 8.0.0-M2 を想定しており、今後変更になる可能性もあります。

動作環境の変更

Wicket 8.0 requires at least Java 8
Wicket 8.0 requires Servlet 3.1 (Jetty 9.2+, Apache Tomcat 8+, JBoss WildFly 10+)

(Migration to Wicket 8.0 より)

Wicket 8.0 では、Java 8 が必須の環境となります。これにあわせてベースとなるServletも3.1にバージョンアップされており、サーブレットコンテナの対応バージョンに注意が必要になります1

Modelの変更点

LambdaModel

大きな変更点として、LambdaModelが追加されました。

これはその名の通り、Java 8 のLambda式やメソッド参照で ModelObject の getter/setter 処理を指定できるModelです。

// Wicket 8.0 からのLambdaModelの例
Order order = new Order();
IModel<String> model = LambdaModel.of(order::getUserName, order::setUserName);

こうすると、第1引数のメソッド参照が getModelObject(), 第2引数のメソッド参照がsetModelObject(String) 処理の中身の処理として扱われます。

このModelには、PropertyModelの代替として利用できる意図があるようです。

// いままでのPropertyModelの例
Order order = new Order();
IModel<String> model = new PropertyModel(order, "userName");

PropertyModelは、インスタンスのフィールドやsetter/getterと連携できる利便性の反面、参照先のフィールド変数を文字列で指定しなくてはならないことに作成時も修正時も面倒さがありました。LambdaModelを使えば、この面倒さを解決してそのまま同じ事ができますね2

コンポーネントでのModelの設定

Labelをはじめとするコンポーネントは、Model(やその内部のオブジェクト)を渡す事で動作します。コンポーネントのModelやObjectを渡す時にも、Lambda式やメソッド参照が使えるようになっています。

Order order = new Order();

// これまで(getUserNameを直接渡しても内部でModelでのラップはしてくれる)
add(new Label("foo", Model.of(order.getUserName())));
add(new Label("foo", order.getUserName()));

// Wicket 8.0 からは、メソッド参照やLambda式でModelやObjectを渡せる
add(new Label("foo", order::getUserName));
add(new Label("foo", () -> {...}));

Modelのファクトリメソッド

Modelにファクトリメソッド(of)が標準的に備えられました。ofの中はLambda式やメソッド参照が使えます。

IService service = new Service();

// これまで
IModel<List<Dish>> model = new LoadableDetachableModel<List<Dish>>() {
  private static final long serialVersionUID = 1380883852804223557L;

  @Override
  protected List<Dish> load() {
    return service.fetchDishes();
  }
};

// Wicket 8.0 からの書き方。LoadableDetachableModelが1行になって、とてもうれしい。
IModel<List<Dish>> model = LoadableDetachableModel.of(service::fetchDishes);

このように、使いどころによって便利だけれども、毎回無名クラス化しなくてはいけなかった LoadableDetachableModel などが1行で書けます。これはとてもうれしい。個人的にModelの変更点の中ではこれが最も利点が大きく感じました。

ただし一方で、AbstractReadOnlyModelは非推奨になり、今後は代わりにIModelの無名クラスを使うことが推奨されています。このあたりは Wicket 8.0 にマイグレーションするときに、一括検索・置換する手間が必要そうです。

IService service = new Service();

// これまで
IModel<Integer> model = new AbstractReadOnlyModel<Integer>() {
  private static final long serialVersionUID = 2631631544271526736L;

  @Override
  public Integer getObject() {
    return new Random().nextInt();
  }
};

// Wicket 8.0 から、AbstractReadOnlyModelは非推奨。IModelの無名クラスに切り替える。
IModel<Integer> model = new IModel<Integer>() {
  private static final long serialVersionUID = -2636918853427797619L;

  @Override
  public Integer getObject() {
    return new Random().nextInt();
  }
};

コンポーネントの変更点

Link, Buttonでのlambda式の利用

ページ間を移動するLinkや、Foamで値をPostするButtonコンポーネントでは、クリック時のイベントを制御するonClick()・onSubmit()メソッドをオーバーライドするために、無名クラス化することが一般的でした。

Wicket8ではLambda式を使うことで、無名クラス化しなくてもLinkやButtonが作れるようになっています。

// これまで
Link<Void> link = new Link<Void>("toHogePage") {
  private static final long serialVersionUID = 3391006051307639762L;

  @Override
  public void onClick() {
    // 諸々の処理
    setResponsePage(new HogePage(model));
  }
};

// Wicket 8.0 からのLinkコンポーネント
add(Link.onClick("toHogePage", (l) -> {
  // 諸々の処理
  setResponsePage(new toHogePage(model));
}));

// ページ移動だけなら1行になります
add(Link.onClick("toHogePage", (l) -> setResponsePage(new toHogePage(model))));
// これまで
Button button = new Button("toHogePage") {
  private static final long serialVersionUID = 7447947126462635005L;

  @Override
  public void onSubmit() {
    // このあたりに諸々の処理が入る
    setResponsePage(new HogePage(model));
  }
};

// Wicket 8.0 からのButtonコンポーネント
Button button = Button.onSubmit("toHogePage", (b) -> {
  // このあたりに諸々の処理が入る
  setResponsePage(new HogePage(model));
}));

// こちらもページ移動だけなら1行になります
Button button = Button.onSubmit("toHogePage", (b) -> setResponsePage(new CompletionPage(orderModel)));

lambda式の引数 lb は、Linkやbuttonインスタンス自身の参照です。クリック後にLinkやButton(もしくはそこから取得できるModelやForm)などを操作したい場合に備えて、Lambda式の中で再利用できるようになっています。

Ajax系のLinkやButtonも同様です。例として、フォームの送信時に wmc という名前のコンポーネントをAjaxで更新するようなボタンがあったとしたら、

// これまで
AjaxButton button = new AjaxButton("button") {
  private static final long serialVersionUID = 796871957135885247L;

  @Override
  protected void onSubmit(AjaxRequestTarget target) {
    target.add(getPage().get("wmc"));
  }

  @Override
  protected void onError(AjaxRequestTarget target) {
    target.add(getForm().get("feeedback"));
  }
};

// Wicket 8.0 からは、Ajax系のLink, Buttomも簡潔に書ける。
// 第2引数のLambda式がonSubmit, 第3引数のLambda式がonError。
AjaxButton button = AjaxButton.onSubmit("onSubmit",
  (b, t) -> t.add(getPage().get("wmc")),
  (b, t) -> t.add(b.getForm().get("feedback")));

の様になります。Wicket8のLambda式の引数 b はボタン自身、引数 t は、AjaxRequestTargetです。

これらのコンポーネントも、よく使うけど無名クラスを大量生産する原因になっていたので、(最短)1行で書けるようになるととてもうれしいですね。

Behavior

Ajaxなどでコンポーネントにふるまいや機能を付け加えるBehaviorにも、ファクトリメソッドとlambdaによって簡略に書けるようになっている部分があります。

下は、よくある(?)1秒ごとにコンポーネント自身を更新するようなBehaviorつきのLabelコンポーネントの例です。

// Wicket 8.0 からの AbstractAjaxTimerBehavior。変数 t は AjaxRequestTarget。
add(new Label("timer", LoadableDetachableModel.of(LocalDateTime::now))
  .setOutputMarkupId(true)
  .add(AbstractAjaxTimerBehavior.onTimer(ONE_SECOND, (t) -> t.add(getPage().get("timer")))));

これまでは、こうしたBehaviorを用意するときにも無名クラスや、サブクラスを作成する必要がありました。Wicket 8.0 ではこうした無名クラスを回避して、よりコードを少なく簡略に書くことができる工夫が施されています。

コード例

ここまで紹介した事例も踏まえて、簡単な送信フォームの例を作ってみました(importは割愛)。Wicketを使ったことがある方にとっては、Javaのコードが短くなっていることが実感できるのではないでしょうか。

送信ページ

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
  <meta charset="utf-8"/>
</head>
<body>
<h1>注文フォーム</h1>
<form wicket:id="form">
  <dl>
    <div wicket:id="feedback"></div>
    <dt>お名前</dt>
    <dd><input type="text" wicket:id="userName"></dd>
    <dt>お食事</dt>
    <dd>以下の<span wicket:id="numberOfDish"></span>品から一つお選びください。</dd>
    <dd>
      <div wicket:id="dish"></div>
    </dd>
  </dl>
  <div>
    <button type="submit" wicket:id="onSubmit">注文する</button>
  </div>
</form>
</body>
</html>
public class OrderPage01 extends WebPage {
  private static final long serialVersionUID = -8412714440456444538L;

  public OrderPage01() {
    IService service = new Service();
    IModel<Order> orderModel = CompoundPropertyModel.of(Model.of(new Order()));

    IModel<List<Dish>> dishes = LoadableDetachableModel.of(service::fetchDishes);

    // 階層が面倒なのでqueueを使っている
    queue(new Form<>("form", orderModel));
    queue(new FeedbackPanel("feedback"));
    queue(new TextField<>("userName").setRequired(true));
    queue(new Label("numberOfDish", dishes.getObject()::size));
    queue(new RadioChoice<>("dish", dishes).setSuffix("<br/>").setRequired(true));
    queue(Button.onSubmit("onSubmit", (b) -> setResponsePage(new CompletionPage(orderModel))));
  }

}

確認(完了)ページ

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
  <meta charset="utf-8"/>
</head>
<body>
<h1>確認画面</h1>
<p><span wicket:id="userName"></span> さんは <span wicket:id="dish"></span> を注文しました</p>
<div><a wicket:id="toIndexPage">戻る</a></div>
</body>
</html>
public class CompletionPage extends WebPage {
  private static final long serialVersionUID = 3057979294465321296L;

  public CompletionPage(IModel<Order> orderModel) {
    setDefaultModel(CompoundPropertyModel.of(orderModel));
    add(new Label("userName"));
    add(new Label("dish"));
    add(Link.onClick("toIndexPage", (link) -> setResponsePage(IndexPage.class)));
  }
}

その他

無名クラスはどのくらい回避できるようになったか?

残念ながら、全てのModel,コンポーネント,Behaviorに無名クラス化を回避する手段が実装されたわけではありません。

ListViewなども無名クラス化して利用するコンポーネントの代表格ですが、特に改善は行われておらず、旧来のとおり無名クラス化して利用する必要があります。

また、たとえLinkやButtonコンポーネントであっても、onInitialize()onConfigure()など、無名クラス化したコンポーネントの初期設定を行うメソッドなどは旧来のままです。したがって、Ajaxの更新対象として設定するような(つまり、setOutputMarkupId(boolean) を設定するような)コンポーネントや、Validation、Visibilityの設定が必要なコンポーネントを生成するような場合には、今までどおり無名クラス化する必要があります3

ただ、そういった詳細な制御を必要としない、どのページにもありそうなLink、Button、Modelが簡略に書けるだけでも、使い心地が良くなっていると思います。

独自のFunctionalInterface

これまで紹介した内容を実現するために、Wicket 8.0 では独自のFunctionalInterfaceを準備しています。WicketBiConsumer, WicketBiFunction, WicketConsumer, WicketFunction, WicketSupplier です。ただしこれらは、Java標準のFunctionalInterfaceを継承してシリアライズ可能にしただけのものです。

また、IModelもFunctionalInterfaceになっていて、detach時の処理をはじめとするデフォルトメソッドや、filterやmapといったModelからModelを生成するための中間処理のためのデフォルトメソッドが用意されています。

これらは、自分でModelやコンポーネントを作ったり改良していく時に利用できそうですね。

おわりに

Wicket 7.x → 8.0 にむけて、他にも細かい変更点がたくさんあるのですが、この記事では、日頃からプログラミングする中で最も気になる、Modelやコンポーネントの記載の簡略化の部分や、lambdaを使って無名クラスが回避できるようになる部分の更新についてまとめました。

すでにWicketで開発しているプロダクトのマイグレーションや、今後、何らかの形で Wicket 8.0 以降で開発することになった方への参考になれば幸いです。

なお、コード例はgithubにおいてあります。


  1. でも未だにweb.xmlが残っています... 

  2. 個人的にはどちらにしろCompoundPropertyModelを使ってしまうのですが... 

  3. setOutputMarkupId(boolean)などの設定用メソッドを、メソッドチェーンの形で繋げていくこともできるのですが、これらのメソッドの戻り値にはComponent型が設定されていることがあり、子コンポーネントをaddする時にキャストが必要不可欠になったりしてきます。queueをうまく使ったり、キャストを許容して進めていくことはできます。 

この投稿は Java EE Advent Calendar 201623日目の記事です。