Wicket のアーキテクチャ
Apache Wicket 10 の設計思想とアーキテクチャを深く理解しましょう。Wicket はコンポーネントベースのWebフレームワークであり、Swingのようなデスクトップ GUI 開発の考え方を Web に持ち込んだ独自の設計を持っています。このページでは、コンポーネントツリー、HTML と Java の対応関係、リクエスト処理の流れ、セッション管理など、Wicket を使いこなすための核心的な概念を解説します。
コンポーネントツリーの概念
Wicket の最も基本的な設計原則は、サーバーサイドにコンポーネントツリーを構築することです。ブラウザが表示する HTML の DOM ツリーと同様に、Wicket は Java オブジェクトによるコンポーネントの階層構造をサーバー上に保持します。各コンポーネントは wicket:id 属性を通じて HTML 要素と結びつけられ、それぞれが自身の状態とロジックを持ちます。
Swing との類似性: Java の Swing で JFrame に JPanel を追加し、さらにその中に JButton や JLabel を配置するのと同じ考え方です。Wicket では WebPage にコンポーネントを add() し、コンテナコンポーネントの中にさらに子コンポーネントを追加していきます。
以下は、Wicket アプリケーションにおけるコンポーネントツリーの概念図です。
WebApplication (アプリケーションのルート)
│
├── HomePage (WebPage)
│ ├── Label [wicket:id="pageTitle"]
│ ├── HeaderPanel (Panel) [wicket:id="header"]
│ │ ├── Label [wicket:id="siteName"]
│ │ └── BookmarkablePageLink [wicket:id="homeLink"]
│ ├── Form [wicket:id="searchForm"]
│ │ ├── TextField [wicket:id="query"]
│ │ └── Button [wicket:id="submitBtn"]
│ ├── ListView [wicket:id="itemList"]
│ │ ├── ListItem[0]
│ │ │ ├── Label [wicket:id="itemName"]
│ │ │ └── Label [wicket:id="itemPrice"]
│ │ ├── ListItem[1]
│ │ │ ├── Label [wicket:id="itemName"]
│ │ │ └── Label [wicket:id="itemPrice"]
│ │ └── ...
│ └── FooterPanel (Panel) [wicket:id="footer"]
│ └── Label [wicket:id="copyright"]
│
├── UserPage (WebPage)
│ └── ...
│
└── AdminPage (WebPage)
└── ...
この構造にはいくつかの重要な特徴があります。
- WebApplication はアプリケーション全体のルートです。1つの Web アプリケーションにつき1つの
WebApplicationサブクラスが存在します。 - WebPage はブラウザに表示される1つのページに対応します。ユーザーがページにアクセスすると、そのページクラスのインスタンスが生成されます。
- 各コンポーネント(Label、Link、Form、Panel など)はページ内の具体的な UI 要素に対応し、それぞれが固有の
wicket:idを持ちます。 - コンテナコンポーネント(Panel、Form、ListView など)は子コンポーネントを格納でき、再帰的なツリー構造を形成します。
wicket:id の一意性: wicket:id は同じ親コンポーネント内で一意でなければなりません。ただし、異なる親の下であれば同じ ID を使用できます。上の例で itemName が各 ListItem 内で繰り返し使われているのはこのためです。
コンポーネントツリーの各ノードは Java オブジェクトであるため、通常の Java プログラミングと同様に、フィールドの保持、メソッドの呼び出し、継承やポリモーフィズムの活用が可能です。これが Wicket の「純粋な Java による Web 開発」というコンセプトの基盤となっています。
HTML と Java の1:1対応
Wicket の設計において最も特徴的なのは、Java クラスと HTML ファイルが1:1で対応するという原則です。他のフレームワークのようなテンプレート言語(JSP の EL 式、Thymeleaf の th: 属性など)は一切使用しません。HTML は純粋な XHTML であり、Wicket 固有の要素は wicket:id 属性だけです。
ファイル命名規則
Wicket では、WebPage やパネルのサブクラスを作成すると、同じパッケージに同じ名前の HTML ファイルを配置する必要があります。
src/main/java/
└── com/example/app/
├── HomePage.java ← Java クラス
├── UserProfilePage.java ← Java クラス
└── HeaderPanel.java ← Java クラス
src/main/resources/ (または src/main/java/ に直接)
└── com/example/app/
├── HomePage.html ← 対応する HTML
├── UserProfilePage.html ← 対応する HTML
└── HeaderPanel.html ← 対応する HTML
HTML ファイルの配置場所: Maven プロジェクトでは、HTML ファイルは src/main/resources に Java クラスと同じパッケージ階層で配置するのが一般的です。ただし、src/main/java に直接配置することも可能です(その場合は Maven の resource フィルタリング設定が必要です)。
対応の具体例
以下に、Java クラスと HTML ファイルの対応関係を示します。左が Java コード、右が対応する HTML テンプレートです。
public class HomePage extends WebPage {
public HomePage() {
add(new Label("message", "Hello, Wicket!"));
add(new Link<Void>("actionLink") {
@Override
public void onClick() {
// handle click
}
});
}
}
<html xmlns:wicket="http://wicket.apache.org">
<body>
<p wicket:id="message">placeholder</p>
<a wicket:id="actionLink">Click me</a>
</body>
</html>
この対応関係を詳しく見てみましょう。
new Label("message", "Hello, Wicket!")は、HTML 内のwicket:id="message"を持つ<p>タグに対応します。レンダリング時にplaceholderというテキストがHello, Wicket!に置き換えられます。new Link<Void>("actionLink")は、wicket:id="actionLink"を持つ<a>タグに対応します。Wicket はこのリンクのhref属性を自動的に生成し、クリック時にサーバーサイドのonClick()メソッドが実行されます。- HTML 内の
placeholderやClick meは開発中のプレビュー用テキストです。デザイナーはこの HTML をそのままブラウザで開いてレイアウトを確認できます。
デザイナーとの協業: Wicket の HTML テンプレートは純粋な XHTML であるため、Web デザイナーは特別なツールや知識なしにテンプレートを編集できます。wicket:id 属性は無視されるため、ブラウザで直接開いてもプレビューとして正しく表示されます。これは Wicket の大きな利点の一つです。
Java 側で add() したコンポーネントの wicket:id と HTML 側の wicket:id が一致しない場合、Wicket は実行時に MarkupException をスローします。Java クラスに追加したすべてのコンポーネントに対応する HTML 要素が必要であり、逆に HTML 内のすべての wicket:id に対応する Java コンポーネントも必要です。
WebApplication クラス
役割と責務
WebApplication クラスは Wicket アプリケーション全体のエントリーポイントであり、アプリケーションの設定を一元管理する場所です。すべての Wicket アプリケーションは org.apache.wicket.protocol.http.WebApplication を継承したクラスを1つ持つ必要があります。
主な責務は以下の通りです。
- ホームページの指定:
getHomePage()メソッドでアプリケーションのトップページを返す - 初期化処理:
init()メソッドでマウントポイントの設定、セキュリティ設定、コンポーネントのスキャン設定など - URL マッピング:
mountPage()でページに分かりやすい URL を割り当てる - アプリケーション全体の設定: マークアップ設定、デバッグ設定、リソース設定など
主な設定項目
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.RuntimeConfigurationType;
public class WicketApplication extends WebApplication {
/**
* ホームページのクラスを返す(必須)。
* ルートURL "/" にアクセスしたときにこのページが表示される。
*/
@Override
public Class<? extends WebPage> getHomePage() {
return HomePage.class;
}
/**
* アプリケーションの初期化処理。
* サーバー起動時に一度だけ呼ばれる。
*/
@Override
public void init() {
super.init();
// URLマウント: /user/profile でアクセス可能にする
mountPage("/user/profile", UserProfilePage.class);
mountPage("/products/${productId}", ProductDetailPage.class);
mountPage("/about", AboutPage.class);
// 開発モード時のデバッグ設定
if (getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT) {
getDebugSettings().setDevelopmentUtilitiesEnabled(true);
}
// マークアップ設定
getMarkupSettings().setStripWicketTags(true);
getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
// セキュリティ設定: CSP (Content Security Policy)
getCspSettings().blocking().disabled();
// ページストアの設定(後述)
setPageManagerProvider(new DefaultPageManagerProvider(this) {
@Override
protected IPageStore newPersistentStore() {
return new DiskPageStore(
getApplicationName(),
getStoreSettings().getFileStoreFolder(),
getStoreSettings().getMaxSizePerSession()
);
}
});
}
}
URL マウントのパターン: mountPage("/products/${productId}", ProductDetailPage.class) のように、URL に変数を含めることができます。${productId} 部分はページのコンストラクタで PageParameters を通じて受け取れます。これにより、RESTful な URL 設計が可能です。
WicketFilter(または WicketServlet)を web.xml もしくは Jakarta Servlet のプログラム的な方法で登録し、上記の WebApplication クラスを指定することでアプリケーションが起動します。
<filter>
<filter-name>wicket</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
<param-name>applicationClassName</param-name>
<param-value>com.example.app.WicketApplication</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wicket</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
リクエスト処理の流れ
Wicket がブラウザからのリクエストをどのように処理するかを理解することは、デバッグやパフォーマンスチューニングにおいて非常に重要です。以下に、リクエストからレスポンスまでの流れをステップごとに解説します。
処理ステップ詳細
-
ブラウザがリクエストを送信
ユーザーがリンクをクリックするか、フォームを送信するか、URL を直接入力してリクエストが送信されます。
-
WicketFilter がリクエストをインターセプト
Jakarta Servlet コンテナ(Tomcat、Jetty など)が受け取ったリクエストを
WicketFilterが捕捉します。Wicket が処理すべきリクエストかどうかを判定し、Wicket のリクエスト処理に引き渡します。 -
RequestCycle(リクエストサイクル)が開始
RequestCycleオブジェクトが生成され、リクエストの処理が始まります。このオブジェクトは1つのリクエスト/レスポンスのライフサイクル全体を管理します。 -
IRequestMapper が URL を解析
リクエストの URL を解析し、対応するリクエストハンドラ(
IRequestHandler)を決定します。ブックマーク可能な URL の場合はページクラスへのマッピング、コールバック URL の場合は既存ページインスタンスへのマッピングが行われます。 -
ページインスタンスの解決
初回アクセスの場合は新しいページインスタンスが生成されます。リンクのクリックやフォーム送信のようなコールバックの場合は、セッション内のページストアから既存のページインスタンスが復元(デシリアライズ)されます。
-
コンポーネントツリーがリクエストを処理
該当するコンポーネントのイベントハンドラ(
onClick()、onSubmit()など)が呼び出されます。コンポーネントのモデルが更新され、必要に応じてページの状態が変更されます。 -
レスポンスのレンダリング
コンポーネントツリーを走査しながら、各コンポーネントが対応する HTML マークアップと結合されます。コンポーネントの
onRender()が呼ばれ、モデルの値が HTML に反映されます。 -
ページインスタンスのシリアライズと保存
レンダリング後のページインスタンスがシリアライズされ、ページストアに保存されます。これにより、次のリクエストで同じページ状態を復元できます。
-
HTML がブラウザに送信
生成された HTML がレスポンスとしてブラウザに返され、ユーザーに表示されます。
Browser WicketFilter RequestCycle Page
│ │ │ │
│── HTTP Request ────────▶│ │ │
│ │── create ───────────▶│ │
│ │ │── resolve ──────▶│
│ │ │ │
│ │ │── processEvent ─▶│
│ │ │ (onClick等) │
│ │ │ │
│ │ │── render ───────▶│
│ │ │◀── HTML ─────────│
│ │ │ │
│ │ │── serialize ────▶│
│ │ │ (PageStore) │
│ │◀─ response ──────────│ │
│◀── HTML Response ───────│ │ │
リクエストリスナー
RequestCycle にはリスナーを登録でき、リクエスト処理の各段階でカスタム処理を挟むことができます。
public class MyRequestCycleListener
extends AbstractRequestCycleListener {
@Override
public void onBeginRequest(RequestCycle cycle) {
// リクエスト開始時の処理(ログ記録など)
long startTime = System.currentTimeMillis();
cycle.setMetaData(START_TIME_KEY, startTime);
}
@Override
public void onEndRequest(RequestCycle cycle) {
// リクエスト終了時の処理(処理時間の計測など)
Long startTime = cycle.getMetaData(START_TIME_KEY);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("Request processed in {} ms", duration);
}
}
@Override
public IRequestHandler onException(
RequestCycle cycle, Exception ex) {
// 例外発生時のハンドリング
log.error("Request failed", ex);
return new RenderPageRequestHandler(
new PageProvider(ErrorPage.class));
}
}
リスナーの登録: WebApplication.init() の中で getRequestCycleListeners().add(new MyRequestCycleListener()) を呼び出すことでリスナーを登録できます。ロギング、パフォーマンス計測、トランザクション管理などに活用できます。
セッションとページストア
Wicket のアーキテクチャにおいて、セッション管理とページストアは最も重要な概念の一つです。Wicket はステートフルなフレームワークであり、ユーザーが操作するページの状態をサーバーサイドに保持します。これにより、ブラウザの「戻る」ボタンへの対応やフォームの二重送信防止といった、Web 開発で厄介な問題を自然に解決します。
ページバージョニング
Wicket はページのバージョニング機能を備えています。ユーザーがページ上で操作を行うたびに、ページの新しいバージョンがページストアに保存されます。
操作の流れ:
1. ユーザーが HomePage にアクセス
→ PageStore に HomePage (version 0) が保存される
→ URL: /wicket/bookmarkable/com.example.HomePage
2. ユーザーがフォームを送信
→ PageStore に HomePage (version 1) が保存される
→ URL: /wicket/page?0-1.IFormSubmitListener-form
3. ユーザーがリンクをクリック
→ PageStore に HomePage (version 2) が保存される
→ URL: /wicket/page?0-2.ILinkListener-actionLink
4. ユーザーが「戻る」ボタンを押す
→ ブラウザが version 1 の URL をリクエスト
→ PageStore から version 1 が復元される
→ version 1 時点の正しい状態でページが表示される
この仕組みのおかげで、ブラウザの「戻る」ボタンを押したときに壊れたページが表示されるという、従来の Web アプリケーションでよくある問題が解消されます。
メモリとのトレードオフ: ページバージョニングは便利ですが、各ページバージョンがシリアライズされて保存されるため、メモリ(またはディスク)を消費します。大量のユーザーが同時アクセスするアプリケーションでは、ページストアのサイズ制限やセッションのタイムアウト設定を適切に行う必要があります。
ページストアの管理は IPageStore インターフェースが担います。Wicket 10 ではいくつかの実装が提供されています。
- InMemoryPageStore: ページをメモリ内に保持します。高速ですが、セッションのメモリ消費が大きくなります。
- DiskPageStore: ページをディスクにシリアライズして保存します。メモリ消費を抑えつつ、多数のページバージョンを保持できます。
- InSessionPageStore: HttpSession 内にページを直接保存します。セッションレプリケーションが必要なクラスタ環境で使用されることがあります。
- CryptingPageStore: 他のページストアをラップし、シリアライズされたページデータを暗号化します。セキュリティ要件の高いアプリケーション向けです。
DiskPageStore
DiskPageStore は本番環境でよく使われるページストアです。メモリ上のキャッシュとディスクストレージを組み合わせ、メモリ使用量を制御しつつ、十分な数のページバージョンを保持します。
@Override
public void init() {
super.init();
// ページストアのディスク保存先を設定
getStoreSettings().setFileStoreFolder(
new File("/var/wicket-pagestore")
);
// セッションあたりの最大サイズ (デフォルト: 10MB)
getStoreSettings().setMaxSizePerSession(
Bytes.megabytes(10)
);
}
シリアライズのコスト: ページストアへの保存にはページインスタンスのシリアライズが必要です。そのため、ページ内のすべてのコンポーネントとモデルオブジェクトが Serializable を実装している必要があります。シリアライズできないオブジェクトを参照するとエラーが発生します。開発モードでは Wicket が自動的にシリアライズチェックを行い、問題を早期に検出できます。
ステートフル vs ステートレス
Wicket はデフォルトでステートフルなフレームワークですが、ステートレスなページやコンポーネントもサポートしています。両者を理解し、適切に使い分けることが効率的な Wicket アプリケーション開発の鍵です。
ステートフルページ
ステートフルページは Wicket のデフォルトの動作です。ページインスタンスがセッションのページストアに保存され、ユーザーの操作ごとにバージョンが管理されます。
public class ShoppingCartPage extends WebPage {
private List<Item> cartItems = new ArrayList<>();
public ShoppingCartPage() {
// カート内容の表示(ステートフル:cartItems が保持される)
add(new ListView<Item>("items", cartItems) {
@Override
protected void populateItem(ListItem<Item> item) {
item.add(new Label("name",
item.getModelObject().getName()));
item.add(new Label("price",
item.getModelObject().getPrice()));
// 削除リンク(コールバック → ステートフル)
item.add(new Link<Void>("remove") {
@Override
public void onClick() {
cartItems.remove(item.getModelObject());
}
});
}
});
}
}
ステートフルページの特徴は以下の通りです。
- ページインスタンスがセッションに保存される
- ブラウザの「戻る」ボタンで前の状態に正しく戻れる
- フォームの二重送信が自然に防止される
- コンポーネントにコールバック(onClick、onSubmit)を設定できる
- URL にはセッション固有のパラメータが含まれる(ブックマーク不向き)
ステートレスページ
ステートレスページはセッションにページインスタンスを保存しません。リクエストのたびに新しいインスタンスが生成されます。SEO が重要なページや、セッションメモリを節約したいページに適しています。
public class ProductListPage extends WebPage {
public ProductListPage(PageParameters params) {
// ステートレスにするには、すべてのコンポーネントを
// ステートレスにする必要がある
String category = params.get("category")
.toString("all");
// BookmarkablePageLink はステートレスなリンク
add(new BookmarkablePageLink<Void>("homeLink",
HomePage.class));
// ステートレスフォーム
StatelessForm<Void> form =
new StatelessForm<Void>("searchForm") {
@Override
protected void onSubmit() {
// フォーム送信処理
}
};
form.add(new TextField<String>("query",
Model.of("")));
add(form);
// 外部リンクもステートレス
add(new ExternalLink("wicketSite",
"https://wicket.apache.org"));
}
/**
* このページがステートレスであることを明示的に宣言。
* すべてのコンポーネントがステートレスでないと
* 実行時に警告が出る。
*/
@Override
protected void setStatelessHint(boolean isStateless) {
super.setStatelessHint(true);
}
}
ステートレスページの特徴は以下の通りです。
- セッションにページインスタンスが保存されない
- URL がブックマーク可能で、SEO に適している
- サーバーのメモリ消費が少ない
- 通常の
Link(コールバック型)は使用できず、BookmarkablePageLinkやStatelessFormを使う - ブラウザの「戻る」ボタンによる問題が発生しない(状態を持たないため)
使い分けの指針
使い分けの基本ルール:
- ステートフルを選ぶ場合: ユーザーとの対話が多いページ(管理画面、ダッシュボード、ショッピングカート、複雑なフォーム)。状態管理の恩恵が大きく、開発効率が向上します。
- ステートレスを選ぶ場合: 公開ページ(トップページ、製品一覧、ヘルプページ)、SEO が重要なページ、大量のユーザーがアクセスするページ。セッションメモリの節約が重要な場面に適しています。
// ステートレスリンク: 他のページへの遷移(URLがブックマーク可能)
// URL 例: /wicket/bookmarkable/com.example.ProductPage?id=42
PageParameters params = new PageParameters();
params.add("id", 42);
add(new BookmarkablePageLink<Void>("productLink",
ProductPage.class, params));
// ステートフルリンク: コールバック(URLがセッション依存)
// URL 例: /wicket/page?3-1.ILinkListener-deleteLink
add(new Link<Void>("deleteLink") {
@Override
public void onClick() {
// サーバーサイドのロジックを実行
productService.delete(productId);
setResponsePage(ProductListPage.class);
}
});
Wicket の名前空間
Jakarta 名前空間
Wicket 10 は Jakarta EE の名前空間を使用しています。これは Wicket 9 までの javax.servlet パッケージから jakarta.servlet パッケージへの移行を意味します。この変更は Jakarta EE 9 以降の標準に準拠するためのものです。
// Wicket 9 以前 (Java EE / javax 名前空間)
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.FilterConfig;
// Wicket 10 (Jakarta EE / jakarta 名前空間)
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.FilterConfig;
移行時の注意: Wicket 9 から Wicket 10 にアップグレードする場合、すべての javax.servlet インポートを jakarta.servlet に変更する必要があります。また、Servlet コンテナも Jakarta EE 対応のバージョン(Tomcat 10.1 以降、Jetty 12 以降など)を使用してください。
Wicket 10 が要求する主な環境要件は以下の通りです。
- Java 17 以上(LTS バージョン推奨)
- Jakarta Servlet 6.0 以上(Tomcat 10.1+、Jetty 12+、WildFly 27+ など)
- Jakarta EE 10 互換のアプリケーションサーバーまたは組み込みコンテナ
HTML テンプレートの名前空間
Wicket の HTML テンプレートでは、Wicket 固有の要素や属性を使用するために xmlns:wicket 名前空間を宣言します。
<html xmlns:wicket="http://wicket.apache.org">
<head>
<title>My Wicket Page</title>
</head>
<body>
<!-- wicket:id 属性でコンポーネントを紐付け -->
<h1 wicket:id="title">Page Title</h1>
<!-- wicket:enclosure で条件付き表示 -->
<wicket:enclosure child="errorMessage">
<div class="error">
<span wicket:id="errorMessage">Error</span>
</div>
</wicket:enclosure>
<!-- wicket:container は出力されないコンテナ -->
<wicket:container wicket:id="conditionalBlock">
<p>This block is conditionally visible.</p>
</wicket:container>
<!-- wicket:message で国際化メッセージ -->
<span wicket:id="greeting">
<wicket:message key="greeting.text">
Default greeting
</wicket:message>
</span>
<!-- wicket:child / wicket:extend でテンプレート継承 -->
</body>
</html>
主な Wicket 名前空間要素の一覧です。
wicket:id-- HTML 要素と Java コンポーネントを紐付ける属性wicket:enclosure-- 子コンポーネントの可視性に基づいてブロック全体の表示/非表示を制御wicket:container-- HTML 出力に痕跡を残さないコンテナ要素wicket:message-- プロパティファイルからの国際化メッセージを埋め込むwicket:head-- Panel や Fragment 内で<head>セクションにコンテンツを追加wicket:childとwicket:extend-- テンプレートの継承(親テンプレートと子テンプレート)wicket:fragment-- 同一 HTML ファイル内で再利用可能なマークアップ断片を定義
wicket タグの除去: getMarkupSettings().setStripWicketTags(true) を WebApplication.init() で設定すると、最終的な HTML 出力から wicket:id 属性や Wicket 固有のタグが除去されます。本番環境ではこの設定を有効にして、クリーンな HTML を出力することが推奨されます。
実務でハマりやすい注意点
-
問題アクセス数が増えるとメモリ消費が想定より膨らみ、URLも長く扱いにくくなる。運用開始後に性能面で気づくことが多い。原因Ajax コンポーネントや状態保持の多い作りによって、ページが意図せず stateful になっている。ページインスタンスがサーバ側に多く残る。対策公開ページは stateless を基本方針にし、stateful が必要な画面だけ明示的に限定する。設計段階でどこまで状態を持つか決める。
-
問題bot の巡回で不要なリクエストが増え、サーバ負荷が高くなる。夜間だけCPUが上がるような症状として現れる。原因stateful URL が大量に生成されると、クローラがそれぞれ別ページとして辿ってしまう。結果として同種ページへのアクセスが増殖する。対策robots 制御とリンク設計を見直し、公開領域は stateless URL を中心に構成する。クロールさせるURLを意図的に絞る。