はじめての 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.java が com.example パッケージにあるなら、
HomePage.html も src/main/resources/com/example/ に置きます。
この1:1の対応関係が Wicket の根幹です。
WicketApplication.java
アプリケーションのエントリーポイントです。
Wicket アプリケーション全体の設定を管理するクラスで、WebApplication を継承します。
Servlet コンテナが起動すると、WicketFilter(または WicketServlet)がこのクラスをインスタンス化し、
アプリケーションのライフサイクルが始まります。
getHomePage()- ルートURL(/)にアクセスされたときに表示するページクラスを返すinit()- アプリケーション起動時に1回だけ呼ばれる初期化メソッド。URL マウント、セキュリティ設定、カスタム設定などを行う
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+ の名前空間を使用します。
<?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 で生成されるコードは以下のようになっています。
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 フレームワークの基盤クラスで、以下のような機能を提供します。
- セッション管理(
WebSessionの生成と管理) - リクエストサイクルの制御
- ページの解決とレンダリング
- セキュリティ設定
- 各種カスタマイズポイント
getHomePage() メソッド
getHomePage() は唯一の抽象メソッドであり、必ずオーバーライドする必要があります。
このメソッドが返すクラスが、ルートURL(/)にアクセスしたときに表示されるページになります。
getHomePage() はページのクラスオブジェクト(Class<? extends WebPage>)を返す点に注意してください。
ページのインスタンスではなくクラスを返すことで、Wicket がリクエストごとに適切なインスタンスを生成・管理できます。
init() メソッド
init() はアプリケーション起動時に1回だけ呼ばれる初期化メソッドです。
ここで様々な設定を行います。典型的な用途を以下に示します。
@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
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
<!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 によるバインディングです。動作の仕組みを整理します。
-
Java 側:
add(new Label("message", "Hello, Wicket!"))でwicket:id="message"のコンポーネントを追加 -
HTML 側:
<p wicket:id="message">で対応する HTML 要素を定義 - レンダリング時: Wicket がコンポーネントツリーと HTML テンプレートを照合し、Java 側のデータで HTML を書き換える
Java 側で add() したコンポーネントと HTML 側の wicket:id は完全に一致させる必要があります。
片方にだけ存在し、もう片方にない場合はエラーになります。この規則を「コンポーネントと マークアップの1:1対応」と呼びます。
カウンターアプリを作る
ここからは実際に手を動かしてアプリを作ります。 まずは、ボタンをクリックするとカウントが増える「カウンターアプリ」を作りましょう。 Hello World よりも実践的で、Wicket の状態管理とイベント処理の基本が理解できます。
CounterPage.java
Java クラスとして CounterPage を作成します。
Label でカウント値を表示し、Link でクリックイベントを処理します。
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++;
}
});
}
}
このコードのポイントを解説します。
-
private int count = 0;- ページのフィールドとしてカウント値を保持します。Wicket はページインスタンスをセッションに保存するため、ページ遷移せずにリクエストを繰り返しても値が保持されます。 -
new Label("count", () -> String.valueOf(count))-Labelの第2引数にラムダ式(IModel)を渡しています。これにより、レンダリングのたびに最新のcount値が表示されます。固定文字列を渡した場合は更新されません。 -
setOutputMarkupId(true)- HTML 要素にid属性を出力するよう設定します。後で AJAX によるコンポーネント更新を行う場合に必要となる設定です。 -
new Link<Void>("increment")- 匿名内部クラスでLinkを作成し、onClick()メソッドをオーバーライドしてクリック時の処理を定義しています。クリックするとページ全体がリロードされ、countがインクリメントされた状態で再描画されます。
Label に直接文字列を渡す(new Label("count", "0"))と、その値は固定になります。
動的に値を変えたい場合は、ラムダ式やモデルオブジェクト(IModel)を使用しましょう。
ラムダ式 () -> String.valueOf(count) は、IModel<String> の簡潔な記法です。
CounterPage.html
対応する HTML テンプレートを作成します。Java クラスと同じパッケージに配置してください。
<!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 を生成します。
// Label: wicket:id="count"
add(new Label("count", ...));
// Link: wicket:id="increment"
add(new Link<>("increment") {
...
});
<!-- Label に対応 -->
<span wicket:id="count">0</span>
<!-- Link に対応 -->
<a wicket:id="increment">
カウントアップ
</a>
アプリケーションへの登録
作成した CounterPage にブラウザからアクセスできるよう、WicketApplication の init() で URL をマウントします。
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/counter で CounterPage にアクセスできるようになります。
mountPage() を使わなくても、Wicket のデフォルト URL パターン
(/wicket/bookmarkable/com.example.CounterPage)でアクセスは可能です。
しかし、mountPage() を使うことで、わかりやすく短い URL を設定できるため、
実用的なアプリケーションでは必ず URL マウントを使いましょう。
動作確認
Start.java を実行してアプリケーションを起動し、ブラウザで http://localhost:8080/counter にアクセスしてみましょう。
- 初期表示: 「現在のカウント: 0」と「カウントアップ」リンクが表示されます。
- リンクをクリック: 「カウントアップ」をクリックするたびに、カウント値が 1, 2, 3... と増加します。
- 状態の保持: カウント値はサーバーサイドのセッションに保持されているため、ブラウザのリロードボタンを押しても値は維持されます(ブラウザの戻るボタンを押すと、Wicket のページバージョニングにより前の状態に戻ることもあります)。
このカウンターアプリはページ全体をリロードして動作します。
後の章で紹介する AjaxLink を使えば、ページ全体のリロードなしにカウント表示だけを更新することができます。
挨拶アプリを作る
次に、フォーム処理の基本を学ぶために「挨拶アプリ」を作成します。
テキストフィールドに名前を入力して送信すると、「こんにちは、[名前]さん!」と表示されるシンプルなアプリです。
Form、TextField、Label、PropertyModel の基本的な使い方を体験できます。
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
<!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 は以下を自動的に行います。
- CSRF トークンの付与と検証
- フォーム内の各フィールドへの入力値の転記(モデルへのバインディング)
- バリデーションの実行
onSubmit()の呼び出し
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<>(...)) でテキストフィールドをフォームに追加しています。
フォーム内のコンポーネント(TextField、Button など)は、
ページに直接 add() するのではなく、Form に add() してください。
フォーム送信時のデータバインディングは、Form の子孫コンポーネントに対してのみ行われます。
WicketApplication の init() メソッドに URL マウントを追加して、動作確認してみましょう。
@Override
public void init() {
super.init();
mountPage("/counter", CounterPage.class);
mountPage("/greeting", GreetingPage.class);
}
http://localhost:8080/greeting にアクセスし、名前を入力して送信ボタンを押すと、
「こんにちは、[入力した名前]さん!」と表示されます。
開発モードとデプロイモード
Wicket には RuntimeConfigurationType という列挙型で定義される2つの動作モードがあります。
開発効率とパフォーマンスのバランスを、このモード設定で制御します。
DEVELOPMENT モード
開発時に使用するモードです。以下の特徴があります。
- 詳細なエラーページ: 例外発生時にスタックトレース、コンポーネントツリー、マークアップの状態を詳細に表示
- マークアップの自動リロード: HTML テンプレートを変更すると、アプリを再起動しなくても次のリクエストで反映される
- AJAX デバッグウィンドウ: AJAX リクエストの内容を確認できるデバッグパネルがページ下部に表示される
- リソースの圧縮無効: JavaScript/CSS の圧縮・結合が無効化され、デバッグが容易
- 起動時の警告バナー: コンソールに「開発モードで起動しています」という警告が表示される
********************************************************************
*** 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 モード
本番環境で使用するモードです。以下の特徴があります。
- 簡素なエラーページ: ユーザー向けのシンプルなエラーページが表示される(スタックトレースは非表示)
- マークアップのキャッシュ: HTML テンプレートがキャッシュされ、ファイルI/Oが削減される
- リソースの最適化: JavaScript/CSS の圧縮・結合が有効化される
- デバッグ機能の無効化: AJAX デバッグウィンドウなどの開発者向け機能が無効化される
- パフォーマンス向上: 各種チェックが省略され、処理速度が向上する
本番環境では必ず DEPLOYMENT モードに設定してください。 DEVELOPMENT モードのまま本番デプロイすると、セキュリティリスク(詳細なエラー情報の露出)と パフォーマンス低下が発生します。
モードの切り替え方法
モードを切り替える方法はいくつかあります。以下に優先度の高い順に紹介します。
方法1: システムプロパティで指定(推奨)
JVM の起動引数でシステムプロパティを設定します。これが最も一般的な方法です。
# 開発モード
-Dwicket.configuration=development
# デプロイモード
-Dwicket.configuration=deployment
方法2: web.xml で指定
<context-param>
<param-name>configuration</param-name>
<param-value>deployment</param-value>
</context-param>
方法3: WicketFilter の init-param で指定
<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 クラスでオーバーライド
@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 でコンポーネントを追加しています。
対処法
- HTML の
wicket:id属性と Java 側の ID 文字列を完全に一致させる - 大文字・小文字の違いにも注意する(
"Count"と"count"は別物) - タイプミスがないか両方のファイルを確認する
add(new Label("count", ...));
<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 側に存在する必要があります。
対処法
- Java コードで該当する
wicket:idのコンポーネントをadd()しているか確認する - コンポーネントを追加する親が正しいか確認する(
Formの子はform.add()で追加する必要がある) - HTML で不要な
wicket:id属性が残っていないか確認する
HTML ファイルが見つからない
エラーメッセージ
org.apache.wicket.markup.MarkupNotFoundException:
Markup of type 'html' for component 'CounterPage' was not found.
原因
Java クラスに対応する HTML テンプレートファイルが見つかりません。 Wicket は Java クラスと同じパッケージ(クラスパス上)に同名の HTML ファイルがあることを期待します。
対処法
- ファイル名: Java クラス名と HTML ファイル名が一致しているか確認する(
CounterPage.javaに対してCounterPage.html) - パッケージ: HTML ファイルが Java クラスと同じパッケージに配置されているか確認する(
src/main/resources/com/example/CounterPage.html) - 大文字小文字: ファイル名の大文字・小文字が正確に一致しているか確認する(Linux 環境では大文字・小文字が区別される)
- ビルドパス:
src/main/resourcesがビルドパスに含まれているか確認する(Maven プロジェクトなら通常は自動的に含まれる)
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 でないオブジェクトをフィールドに持っていると、このエラーが発生します。
対処法
- ページのフィールドに設定するオブジェクトは
Serializableを実装する - シリアライズ不要なフィールドには
transientキーワードを付ける - データベース接続やサービスクラスなど、シリアライズできないオブジェクトは
@SpringBean(Wicket-Spring 連携時)やLoadableDetachableModelを使って管理する
シリアライゼーションの問題は、開発モードでは getDebugSettings().setSerializeSessionCheck(true)
を設定することで早期に検出できます。本番環境でいきなりエラーになる前に、開発時に確認する習慣をつけましょう。
この章のまとめ: quickstart プロジェクトの構成を理解し、カウンターアプリと挨拶アプリを作成しました。
Wicket 開発の基本サイクルは「(1) Java クラスを作成 → (2) 対応する HTML テンプレートを作成 → (3) WicketApplication に URL マウントを登録 → (4) 動作確認」です。
次章では、Label、Link、WebMarkupContainer など、Wicket の基本コンポーネントをさらに詳しく見ていきます。