第四戒: 最初の奇跡 〜 Hello World は祈りである

「最初の Hello World は祈りである。神はその祈りに応え、ブラウザに文字を表示されるであろう」 ーー Wicket 聖典 第四戒より

この章では、Maven archetype で生成された quickstart プロジェクトの各ファイルを読み解き、 構造を理解した上で、Hello World から一歩進んだ「カウンターアプリ」と「挨拶アプリ」を実際に作成します。 Wicket の基本的な開発の流れを体験しましょう。

quickstart プロジェクトの構成

前章の環境構築で Maven archetype を使ってプロジェクトを生成しました。 ここでは、生成されたプロジェクトの主要ファイルを一つずつ確認していきます。 典型的な quickstart プロジェクトのディレクトリ構成は以下のようになります。

プロジェクト構成
myapp/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/
    │   │       ├── WicketApplication.java
    │   │       └── HomePage.java
    │   ├── resources/
    │   │   └── com/example/
    │   │       └── HomePage.html
    │   └── webapp/
    │       └── WEB-INF/
    │           └── web.xml
    └── test/
        └── java/
            └── com/example/
                └── Start.java

Wicket では、Java クラスと HTML テンプレートが同じパッケージに配置されることが重要です。 HomePage.javacom.example パッケージにあるなら、 HomePage.htmlsrc/main/resources/com/example/ に置きます。 この1:1の対応関係が Wicket の根幹です。

WicketApplication.java

アプリケーションのエントリーポイントです。 Wicket アプリケーション全体の設定を管理するクラスで、WebApplication を継承します。 Servlet コンテナが起動すると、WicketFilter(または WicketServlet)がこのクラスをインスタンス化し、 アプリケーションのライフサイクルが始まります。

HomePage.java + HomePage.html

quickstart で生成されるデフォルトのトップページです。 Java クラス(ロジック)と HTML ファイル(テンプレート)のペアで構成されます。 Wicket ではすべてのページがこのペア構造で作られます。

Start.java

組み込み Jetty サーバーのランチャーです。 src/test/java に配置されており(テストスコープ)、開発時に手軽にアプリケーションを起動するために使います。 main() メソッドを実行するだけで、Jetty が起動し、ブラウザからアクセスできるようになります。

Start.java はテストスコープにあるため、本番デプロイ時の WAR ファイルには含まれません。 開発時の利便性のためだけに存在するクラスです。Eclipse から直接 Run As > Java Application で実行できます。

web.xml

サーブレットコンテナの設定ファイルです。 WicketFilter を登録し、すべてのリクエストを Wicket に転送するよう構成します。 Wicket 10 では Jakarta Servlet 5.0+ の名前空間を使用します。

src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         version="5.0">

  <filter>
    <filter-name>wicket.myapp</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
      <param-name>applicationClassName</param-name>
      <param-value>com.example.WicketApplication</param-value>
    </init-param>
    <init-param>
      <param-name>filterMappingUrlPattern</param-name>
      <param-value>/*</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>wicket.myapp</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

ポイントは applicationClassName パラメータで、ここに WicketApplication の完全修飾クラス名を指定します。 filterMappingUrlPattern/* にすることで、すべてのリクエストが Wicket で処理されます。

pom.xml

Maven のプロジェクト定義ファイルです。quickstart archetype で生成された pom.xml には、以下の主要な依存関係が含まれています。

依存関係スコープ説明
wicket-core compile Wicket のコアライブラリ。ページ、コンポーネント、モデルなど基本機能すべて
wicket-tester test WicketTester を使ったユニットテスト用ライブラリ
jakarta.servlet-api provided Jakarta Servlet API。コンテナから提供されるため provided スコープ
jetty-server test 組み込み Jetty サーバー。Start.java から利用する開発用サーバー
slf4j-api + 実装 compile ログ出力用。Wicket は SLF4J を使用する

WicketApplication を理解する

WicketApplication は Wicket アプリケーションの中心となるクラスです。 Servlet コンテナの起動時にインスタンス化され、アプリケーション全体の設定・管理を担当します。 archetype で生成されるコードは以下のようになっています。

WicketApplication.java
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.WebApplication;

public class WicketApplication extends WebApplication {

    @Override
    public Class<? extends WebPage> getHomePage() {
        return HomePage.class;
    }

    @Override
    public void init() {
        super.init();
        // 設定やURLマウントはここで行う
    }
}

WebApplication の継承

すべての Wicket アプリケーションは org.apache.wicket.protocol.http.WebApplication を継承します。 WebApplication は Wicket フレームワークの基盤クラスで、以下のような機能を提供します。

getHomePage() メソッド

getHomePage()唯一の抽象メソッドであり、必ずオーバーライドする必要があります。 このメソッドが返すクラスが、ルートURL(/)にアクセスしたときに表示されるページになります。

getHomePage()ページのクラスオブジェクトClass<? extends WebPage>)を返す点に注意してください。 ページのインスタンスではなくクラスを返すことで、Wicket がリクエストごとに適切なインスタンスを生成・管理できます。

init() メソッド

init() はアプリケーション起動時に1回だけ呼ばれる初期化メソッドです。 ここで様々な設定を行います。典型的な用途を以下に示します。

init() の典型的な使い方
@Override
public void init() {
    super.init();

    // ページの URL マウント(ブックマーク可能なURLを設定)
    mountPage("/counter", CounterPage.class);
    mountPage("/greeting", GreetingPage.class);

    // マークアップ設定
    getMarkupSettings().setStripWicketTags(true);
    getMarkupSettings().setDefaultMarkupEncoding("UTF-8");

    // 開発モード時のデバッグ設定
    if (usesDevelopmentConfig()) {
        getDebugSettings().setAjaxDebugModeEnabled(true);
    }
}

super.init() の呼び出しは必須です。これを忘れると、Wicket の内部初期化が行われず、 正しく動作しません。

init() 内で super.init() を呼び出すのを忘れないでください。 呼び忘れると、フレームワーク内部の初期化がスキップされ、原因不明のエラーが発生する可能性があります。

HomePage を理解する

quickstart で生成される HomePage は、最もシンプルな Wicket ページの例です。 Java クラスと HTML テンプレートのペアで構成されており、この構造が Wicket 開発の基本形となります。

HomePage.java

HomePage.java
package com.example;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;

public class HomePage extends WebPage {

    public HomePage() {
        add(new Label("message", "Hello, Wicket!"));
    }
}

WebPage を継承し、コンストラクタでコンポーネントを追加しています。 Label は最も基本的なコンポーネントで、テキストを表示するために使います。 第1引数の "message"wicket:id と呼ばれ、HTML テンプレート内の要素と対応させる識別子です。

HomePage.html

HomePage.html
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
    <title>HomePage</title>
</head>
<body>
    <h1>Wicket Quickstart</h1>
    <p wicket:id="message">ここにメッセージが入ります</p>
</body>
</html>

HTML テンプレートは完全な HTML ファイルです。 xmlns:wicket 名前空間を宣言し、wicket:id 属性で Java 側のコンポーネントと紐付けます。 HTML 内の "ここにメッセージが入ります" はデザイン時のプレースホルダーであり、 実行時には Java 側で設定した "Hello, Wicket!" に置き換えられます。

wicket:id バインディング

Wicket の核心は、この wicket:id によるバインディングです。動作の仕組みを整理します。

  1. Java 側: add(new Label("message", "Hello, Wicket!"))wicket:id="message" のコンポーネントを追加
  2. HTML 側: <p wicket:id="message"> で対応する HTML 要素を定義
  3. レンダリング時: Wicket がコンポーネントツリーと HTML テンプレートを照合し、Java 側のデータで HTML を書き換える

Java 側で add() したコンポーネントと HTML 側の wicket:id完全に一致させる必要があります。 片方にだけ存在し、もう片方にない場合はエラーになります。この規則を「コンポーネントと マークアップの1:1対応」と呼びます。

カウンターアプリを作る

ここからは実際に手を動かしてアプリを作ります。 まずは、ボタンをクリックするとカウントが増える「カウンターアプリ」を作りましょう。 Hello World よりも実践的で、Wicket の状態管理イベント処理の基本が理解できます。

CounterPage.java

Java クラスとして CounterPage を作成します。 Label でカウント値を表示し、Link でクリックイベントを処理します。

src/main/java/com/example/CounterPage.java
package com.example;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.Link;

public class CounterPage extends WebPage {
    private int count = 0;

    public CounterPage() {
        final Label countLabel = new Label("count", () -> String.valueOf(count));
        countLabel.setOutputMarkupId(true);
        add(countLabel);

        add(new Link<Void>("increment") {
            @Override
            public void onClick() {
                count++;
            }
        });
    }
}

このコードのポイントを解説します。

Label に直接文字列を渡す(new Label("count", "0"))と、その値は固定になります。 動的に値を変えたい場合は、ラムダ式やモデルオブジェクト(IModel)を使用しましょう。 ラムダ式 () -> String.valueOf(count) は、IModel<String> の簡潔な記法です。

CounterPage.html

対応する HTML テンプレートを作成します。Java クラスと同じパッケージに配置してください。

src/main/resources/com/example/CounterPage.html
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
    <title>カウンター</title>
</head>
<body>
    <h1>Wicket カウンター</h1>
    <p>現在のカウント: <span wicket:id="count">0</span></p>
    <a wicket:id="increment">カウントアップ</a>
</body>
</html>

HTML 内の <span wicket:id="count">0</span>0 はプレースホルダーです。 実行時には Java 側の Label コンポーネントが出力する値に置き換えられます。 同様に、<a wicket:id="increment"> は Java 側の Link コンポーネントに対応し、 Wicket が自動的に正しいリンク URL を生成します。

Java(コンポーネント追加)
// Label: wicket:id="count"
add(new Label("count", ...));

// Link: wicket:id="increment"
add(new Link<>("increment") {
  ...
});
HTML(マークアップ)
<!-- Label に対応 -->
<span wicket:id="count">0</span>

<!-- Link に対応 -->
<a wicket:id="increment">
  カウントアップ
</a>

アプリケーションへの登録

作成した CounterPage にブラウザからアクセスできるよう、WicketApplicationinit() で URL をマウントします。

WicketApplication.java(更新後)
public class WicketApplication extends WebApplication {

    @Override
    public Class<? extends WebPage> getHomePage() {
        return HomePage.class;
    }

    @Override
    public void init() {
        super.init();

        // カウンターページをマウント
        mountPage("/counter", CounterPage.class);
    }
}

mountPage("/counter", CounterPage.class) により、 http://localhost:8080/counterCounterPage にアクセスできるようになります。

mountPage() を使わなくても、Wicket のデフォルト URL パターン (/wicket/bookmarkable/com.example.CounterPage)でアクセスは可能です。 しかし、mountPage() を使うことで、わかりやすく短い URL を設定できるため、 実用的なアプリケーションでは必ず URL マウントを使いましょう。

動作確認

Start.java を実行してアプリケーションを起動し、ブラウザで http://localhost:8080/counter にアクセスしてみましょう。

  1. 初期表示: 「現在のカウント: 0」と「カウントアップ」リンクが表示されます。
  2. リンクをクリック: 「カウントアップ」をクリックするたびに、カウント値が 1, 2, 3... と増加します。
  3. 状態の保持: カウント値はサーバーサイドのセッションに保持されているため、ブラウザのリロードボタンを押しても値は維持されます(ブラウザの戻るボタンを押すと、Wicket のページバージョニングにより前の状態に戻ることもあります)。

このカウンターアプリはページ全体をリロードして動作します。 後の章で紹介する AjaxLink を使えば、ページ全体のリロードなしにカウント表示だけを更新することができます。

挨拶アプリを作る

次に、フォーム処理の基本を学ぶために「挨拶アプリ」を作成します。 テキストフィールドに名前を入力して送信すると、「こんにちは、[名前]さん!」と表示されるシンプルなアプリです。 FormTextFieldLabelPropertyModel の基本的な使い方を体験できます。

GreetingPage.java

src/main/java/com/example/GreetingPage.java
package com.example;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.PropertyModel;

public class GreetingPage extends WebPage {
    private String name = "";
    private String greeting = "";

    public GreetingPage() {
        final Label greetingLabel = new Label("greeting", () -> greeting);
        greetingLabel.setOutputMarkupId(true);
        add(greetingLabel);

        Form<Void> form = new Form<>("greetingForm") {
            @Override
            protected void onSubmit() {
                greeting = "こんにちは、" + name + "さん!";
            }
        };
        add(form);

        form.add(new TextField<>("name", new PropertyModel<>(this, "name")));
    }
}

GreetingPage.html

src/main/resources/com/example/GreetingPage.html
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
    <title>挨拶アプリ</title>
</head>
<body>
    <h1>Wicket 挨拶アプリ</h1>

    <p wicket:id="greeting">ここに挨拶が表示されます</p>

    <form wicket:id="greetingForm">
        <label>名前:</label>
        <input type="text" wicket:id="name" />
        <button type="submit">送信</button>
    </form>
</body>
</html>

コードの解説

このアプリケーションには、Wicket のフォーム処理における重要な概念がいくつか含まれています。

Form コンポーネント

Form<Void> は HTML の <form> 要素に対応するコンポーネントです。 onSubmit() メソッドをオーバーライドすることで、フォーム送信時の処理を定義します。 Wicket の Form は以下を自動的に行います。

PropertyModel

new PropertyModel<>(this, "name") は、ページオブジェクト(this)の name フィールドに テキストフィールドの値をバインドします。フォームが送信されると、Wicket は入力値を自動的に name フィールドに書き込みます。

PropertyModel はリフレクションを使用してフィールドにアクセスします。 フィールド名の文字列を間違えると実行時エラーになるため注意してください。 型安全なバインディングを行いたい場合は、後の章で紹介する LambdaModel の使用を検討しましょう。

TextField

TextField<>("name", model) は HTML の <input type="text"> に対応するコンポーネントです。 Form の子コンポーネントとして追加する点に注意してください。 add(form) でフォームをページに追加し、form.add(new TextField<>(...)) でテキストフィールドをフォームに追加しています。

フォーム内のコンポーネント(TextFieldButton など)は、 ページに直接 add() するのではなく、Formadd() してください。 フォーム送信時のデータバインディングは、Form の子孫コンポーネントに対してのみ行われます。

WicketApplicationinit() メソッドに URL マウントを追加して、動作確認してみましょう。

WicketApplication.java(挨拶ページを追加)
@Override
public void init() {
    super.init();
    mountPage("/counter", CounterPage.class);
    mountPage("/greeting", GreetingPage.class);
}

http://localhost:8080/greeting にアクセスし、名前を入力して送信ボタンを押すと、 「こんにちは、[入力した名前]さん!」と表示されます。

開発モードとデプロイモード

Wicket には RuntimeConfigurationType という列挙型で定義される2つの動作モードがあります。 開発効率とパフォーマンスのバランスを、このモード設定で制御します。

DEVELOPMENT モード

開発時に使用するモードです。以下の特徴があります。

開発モード起動時のコンソール出力
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode.              ***
***                               ^^^^^^^^^^^                    ***
*** Do NOT deploy to your live server(s) without changing this.  ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************

DEPLOYMENT モード

本番環境で使用するモードです。以下の特徴があります。

本番環境では必ず DEPLOYMENT モードに設定してください。 DEVELOPMENT モードのまま本番デプロイすると、セキュリティリスク(詳細なエラー情報の露出)と パフォーマンス低下が発生します。

モードの切り替え方法

モードを切り替える方法はいくつかあります。以下に優先度の高い順に紹介します。

方法1: システムプロパティで指定(推奨)

JVM の起動引数でシステムプロパティを設定します。これが最も一般的な方法です。

JVM 起動引数
# 開発モード
-Dwicket.configuration=development

# デプロイモード
-Dwicket.configuration=deployment

方法2: web.xml で指定

web.xml
<context-param>
    <param-name>configuration</param-name>
    <param-value>deployment</param-value>
</context-param>

方法3: WicketFilter の init-param で指定

web.xml(WicketFilter 内)
<filter>
    <filter-name>wicket.myapp</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
        <param-name>configuration</param-name>
        <param-value>deployment</param-value>
    </init-param>
    <!-- 他の init-param は省略 -->
</filter>

方法4: Application クラスでオーバーライド

WicketApplication.java
@Override
public RuntimeConfigurationType getConfigurationType() {
    return RuntimeConfigurationType.DEPLOYMENT;
}

開発と本番で設定を切り替えやすくするため、方法1のシステムプロパティを使う方法が推奨されます。 コードやデプロイ用の設定ファイルを変更せずに、起動時のパラメータだけでモードを切り替えられます。

よくあるエラーと対処法

Wicket 開発を始めたばかりの頃に遭遇しやすいエラーとその対処法をまとめます。 エラーメッセージを見たら、まずここを参照してください。

wicket:id の不一致

エラーメッセージ

エラー出力
org.apache.wicket.markup.MarkupException:
Unable to find component with id 'counter' in [Page class = CounterPage, ...]
Expected: 'counter'. Found with id(s): 'count'

原因

HTML テンプレート内の wicket:id の値と、Java コードで add() したコンポーネントの ID が一致していません。 上記の例では、HTML に wicket:id="counter" と書いているのに、Java 側では "count" という ID でコンポーネントを追加しています。

対処法

Java(正しい例)
add(new Label("count", ...));
HTML(正しい例)
<span wicket:id="count">0</span>

コンポーネント未追加

エラーメッセージ

エラー出力
org.apache.wicket.markup.MarkupException:
Tag 'span' with wicket:id='greeting' not found in
the component hierarchy of [Page class = GreetingPage]

原因

HTML テンプレートに wicket:id="greeting" の要素が存在するのに、 Java コードでそのIDのコンポーネントを add() していません。 Wicket では、HTML 内のすべての wicket:id 属性に対応するコンポーネントが Java 側に存在する必要があります。

対処法

HTML ファイルが見つからない

エラーメッセージ

エラー出力
org.apache.wicket.markup.MarkupNotFoundException:
Markup of type 'html' for component 'CounterPage' was not found.

原因

Java クラスに対応する HTML テンプレートファイルが見つかりません。 Wicket は Java クラスと同じパッケージ(クラスパス上)に同名の HTML ファイルがあることを期待します。

対処法

Eclipse で HTML ファイルの配置場所がわからなくなったら、Java クラスのパッケージ名を確認してください。 例えば com.example.pages.CounterPage なら、HTML は src/main/resources/com/example/pages/CounterPage.html に置きます。

シリアライゼーションエラー

エラーメッセージ

エラー出力
org.apache.wicket.WicketRuntimeException:
A problem occurred while serializing page [CounterPage]
...
Caused by: java.io.NotSerializableException: com.example.SomeService

原因

Wicket はページインスタンスをセッションに保存するために、ページオブジェクトをシリアライズします。 ページやコンポーネントが Serializable でないオブジェクトをフィールドに持っていると、このエラーが発生します。

対処法

シリアライゼーションの問題は、開発モードでは getDebugSettings().setSerializeSessionCheck(true) を設定することで早期に検出できます。本番環境でいきなりエラーになる前に、開発時に確認する習慣をつけましょう。

この章のまとめ: quickstart プロジェクトの構成を理解し、カウンターアプリと挨拶アプリを作成しました。 Wicket 開発の基本サイクルは「(1) Java クラスを作成 → (2) 対応する HTML テンプレートを作成 → (3) WicketApplication に URL マウントを登録 → (4) 動作確認」です。 次章では、LabelLinkWebMarkupContainer など、Wicket の基本コンポーネントをさらに詳しく見ていきます。