Apache Wicket 快速入门与集成示例
以最小可运行工程演示 Apache Wicket 集成:Maven 引入 wicket-core,编写页面组件、模板与本地化资源,内嵌 Jetty 与 Tomcat 运行,用 curl 实测页面渲染、表单与登录往返。
Apache Wicket 快速入门与集成示例
本文用一个可直接运行的 Maven 工程演示 Apache Wicket 的完整集成流程:引入 wicket-core 依赖、编写应用入口与页面组件、配置模板与本地化资源,先后以内嵌 Jetty 与独立 Tomcat 两种方式运行,并用 curl 完成页面渲染、表单提交与登录往返的验证。全部命令与回显来自真实服务器实测。
一、环境信息
| 项目 | 内嵌 Jetty 场景 | Tomcat 部署场景 |
|---|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS | CentOS Linux 8 |
| JDK | OpenJDK 17.0.20.1 | OpenJDK 17.0.1+12-LTS |
| Maven | 3.6.3 | 3.9.16 |
| Apache Wicket | 10.11.0(wicket-core) | 10.11.0(wicket-core) |
| Jetty | 11.0.24 | 不适用 |
| Tomcat | 不适用 | 10.1.61 |
| 端口 | 8080 | 8080(上下文 /wicket-demo) |
二、前置条件
- 已安装 JDK 17 与 Maven(mvn -version 可正常执行)。
- 服务器可访问 Maven Central 仓库。
- 8080 端口空闲:ss -lntp | grep 8080 无输出。
三、工程结构总览
wicket-demo/
├── pom.xml
└── src/main/
├── java/com/example/
│ ├── WicketApplication.java 应用入口
│ ├── DemoSession.java 自定义会话
│ ├── HomePage.java 首页(Label、Link、Form)
│ ├── LoginPage.java 登录页(Form 表单)
│ └── Start.java 内嵌 Jetty 启动类
├── resources/com/example/
│ ├── HomePage.html 首页模板
│ ├── LoginPage.html 登录页模板
│ ├── HomePage.properties 首页文案(英文)
│ ├── HomePage_zh_CN.properties 首页文案(中文)
│ ├── LoginPage.properties 登录文案(英文)
│ └── LoginPage_zh_CN.properties 登录文案(中文)
└── webapp/WEB-INF/
└── web.xml 过滤器与运行模式声明
Wicket 约定组件类与同名 HTML 模板同包存放,本文将模板放在 resources 的相同包路径下。
四、pom.xml 与依赖引入
创建 /root/lab/wicket-demo/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>wicket-demo</artifactId>
<packaging>war</packaging>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId>
<version>10.11.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>11.0.24</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>
<build>
<finalName>wicket-demo</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</build>
</project>
要点:war 插件必须固定 3.5.1,否则 JDK 17 打包报错;resources 额外包含 src/main/java 下的非 java 文件是为了兼容模板放源码目录的团队习惯,本文模板位于 resources,该段保留不影响。
五、编写应用代码
5.1 应用入口 WicketApplication.java
package com.example;
import org.apache.wicket.Page;
import org.apache.wicket.Session;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
public class WicketApplication extends WebApplication {
@Override
public Class<? extends Page> getHomePage() {
return HomePage.class;
}
@Override
public Session newSession(Request request, Response response) {
return new DemoSession(request);
}
@Override
protected void init() {
super.init();
getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
getRequestCycleSettings().setResponseRequestEncoding("UTF-8");
mountPage("/login", LoginPage.class);
}
}
5.2 自定义会话 DemoSession.java
package com.example;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.request.Request;
public class DemoSession extends WebSession {
private static final long serialVersionUID = 1L;
private String user;
public DemoSession(Request request) {
super(request);
}
public static DemoSession get() {
return (DemoSession) WebSession.get();
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}
5.3 首页 HomePage.java(Label、Link、Form 三类组件)
package com.example;
import java.text.MessageFormat;
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.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;
public HomePage() {
String user = DemoSession.get().getUser();
if (user == null) {
user = getString("home.guest");
}
add(new Label("greeting", MessageFormat.format(getString("home.greeting"), user)));
add(new Label("mode", getApplication().getConfigurationType().name()));
add(new FeedbackPanel("feedback"));
IModel<String> nameModel = Model.of("");
Form<Void> echoForm = new Form<Void>("echoForm") {
private static final long serialVersionUID = 1L;
@Override
protected void onSubmit() {
info(MessageFormat.format(getString("home.echo"), nameModel.getObject()));
}
};
echoForm.add(new TextField<String>("name", nameModel).setRequired(true));
add(echoForm);
add(new Link<Void>("loginLink") {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
setResponsePage(LoginPage.class);
}
});
}
}
5.4 登录页 LoginPage.java(账号 admin,密码 q1w2e3r4)
package com.example;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.PasswordTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.Model;
public class LoginPage extends WebPage {
private static final long serialVersionUID = 1L;
public LoginPage() {
add(new FeedbackPanel("feedback"));
add(new LoginForm("loginForm"));
}
private static final class LoginForm extends Form<Void> {
private static final long serialVersionUID = 1L;
private final TextField<String> username;
private final PasswordTextField password;
LoginForm(String id) {
super(id);
username = new TextField<String>("username", Model.of(""));
password = new PasswordTextField("password", Model.of(""));
add(username, password);
}
@Override
protected void onSubmit() {
if ("admin".equals(username.getModelObject())
&& "q1w2e3r4".equals(password.getModelObject())) {
DemoSession.get().setUser(username.getModelObject());
setResponsePage(getApplication().getHomePage());
} else {
error(getString("login.failed"));
}
}
}
}
5.5 内嵌 Jetty 启动类 Start.java
package com.example;
import java.util.EnumSet;
import org.apache.wicket.protocol.http.WicketFilter;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import jakarta.servlet.DispatcherType;
public class Start {
public static void main(String[] args) throws Exception {
System.setProperty("wicket.configuration", "development");
Server server = new Server(8080);
ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.SESSIONS);
ctx.setContextPath("/");
ctx.setResourceBase("src/main/webapp");
ctx.setInitParameter("configuration", "development");
FilterHolder wicketFilter = ctx.addFilter(WicketFilter.class, "/*",
EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR));
wicketFilter.setInitParameter("applicationClassName", "com.example.WicketApplication");
wicketFilter.setInitParameter("filterMappingUrlPattern", "/*");
server.setHandler(ctx);
server.start();
server.join();
}
}
编程式注册 WicketFilter 必须补 filterMappingUrlPattern 参数,实测缺失时访问页面返回 500:filter path was not configured。
六、模板与本地化资源
src/main/resources/com/example/HomePage.html:
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8"/>
<title>Wicket Demo</title>
</head>
<body>
<h1 wicket:id="greeting">greeting goes here</h1>
<p>Runtime mode: <span wicket:id="mode">mode</span></p>
<ul wicket:id="feedback">
<li>feedback</li>
</ul>
<form wicket:id="echoForm">
<input type="text" wicket:id="name" placeholder="your name"/>
<input type="submit" value="Submit"/>
</form>
<a href="#" wicket:id="loginLink">Sign in</a>
</body>
</html>
src/main/resources/com/example/LoginPage.html:
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8"/>
<title>Wicket Demo Login</title>
</head>
<body>
<h1>Sign in</h1>
<ul wicket:id="feedback">
<li>feedback</li>
</ul>
<form wicket:id="loginForm">
<input type="text" wicket:id="username" placeholder="username"/>
<input type="password" wicket:id="password" placeholder="password"/>
<input type="submit" value="Login"/>
</form>
</body>
</html>
HomePage.properties(默认英文):
home.greeting=Welcome, {0}!
home.guest=guest
home.echo=Hello, {0}. The form has been submitted.
HomePage_zh_CN.properties(中文用 unicode 转义,实测直接写 UTF-8 中文会乱码):
home.greeting=\u6b22\u8fce\u56de\u6765\uff0c{0}\uff01
home.guest=\u8bbf\u5ba2
home.echo=\u4f60\u597d\uff0c{0}\uff0c\u8868\u5355\u5df2\u63d0\u4ea4\u3002
LoginPage.properties:
login.failed=Wrong username or password.
LoginPage_zh_CN.properties:
login.failed=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef\u3002
七、web.xml 与过滤器配置
src/main/webapp/WEB-INF/web.xml(Jakarta Servlet 5.0,部署场景使用 deployment 模式):
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<display-name>wicket-demo</display-name>
<context-param>
<param-name>configuration</param-name>
<param-value>deployment</param-value>
</context-param>
<filter>
<filter-name>wicket-demo</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>
</filter>
<filter-mapping>
<filter-name>wicket-demo</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
</web-app>
八、编译与内嵌 Jetty 运行验证
8.1 编译打包
cd /root/lab/wicket-demo && mvn clean package
实测回显(节选):
[INFO] Building war: /root/lab/wicket-demo/target/wicket-demo.war
[INFO] BUILD SUCCESS
[INFO] Total time: 2.226 s
8.2 启动
cd /root/lab/wicket-demo && MAVEN_OPTS="-Xms256m -Xmx512m" nohup mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=com.example.Start > /root/lab/demo.log 2>&1 &
启动日志出现以下横幅即表示就绪:
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode. ***
*** ^^^^^^^^^^^ ***
*** Do NOT deploy to your live server(s) without changing this. ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
8.3 首页与运行模式
rm -f /tmp/ck.txt && curl -s -c /tmp/ck.txt -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/
实测回显:
302
curl -s -b /tmp/ck.txt -L http://127.0.0.1:8080/ | grep -o "<h1[^>]*>[^<]*\|<span wicket:id=\"mode\">[^<]*"
实测回显:
<h1>Welcome, guest!
<span wicket:id="mode">DEVELOPMENT
302 为 Wicket 默认 REDIRECT_TO_RENDER 策略,跟随重定向后即为 200 页面。
8.4 表单回显
curl -s -b /tmp/ck.txt -L http://127.0.0.1:8080/ -o /tmp/t1.html
A=$(grep -o "action=\"[^\"]*\"" /tmp/t1.html | head -1 | sed "s/action=\"//;s/\"//") && curl -s -b /tmp/ck.txt -L --data-urlencode "p::name=Zhang San" "http://127.0.0.1:8080/${A#./}" | grep -o "Hello[^<]*"
实测回显:
Hello, Zhang San. The form has been submitted.
参数名 p::name 来自页面渲染出的 input 标签 name 属性,Wicket 10 会为表单输入生成相对路径形式的参数名。
8.5 登录往返
错误密码提交:
curl -s -b /tmp/ck.txt -L http://127.0.0.1:8080/login -o /tmp/t2.html
B=$(grep -o "action=\"[^\"]*\"" /tmp/t2.html | head -1 | sed "s/action=\"//;s/\"//") && curl -s -b /tmp/ck.txt -L --data-urlencode "username=admin" --data-urlencode "password=bad-pass" "http://127.0.0.1:8080/login${B#./login}" | grep -o "Wrong username[^<]*"
实测回显:
Wrong username or password.
正确密码提交(admin 与 q1w2e3r4):
curl -s -b /tmp/ck.txt -L http://127.0.0.1:8080/login -o /tmp/t3.html
C=$(grep -o "action=\"[^\"]*\"" /tmp/t3.html | head -1 | sed "s/action=\"//;s/\"//") && curl -s -b /tmp/ck.txt -L --data-urlencode "username=admin" --data-urlencode "password=q1w2e3r4" "http://127.0.0.1:8080/login${C#./login}" | grep -o "Welcome[^<]*" | head -1
实测回显:
Welcome, admin!
登录成功后会话中保存了用户名,首页问候语随之变化。
8.6 中文本地化
会话语言在会话创建时确定,验证中文需新会话并携带 Accept-Language 头:
rm -f /tmp/ckzh.txt && curl -s -c /tmp/ckzh.txt -o /dev/null -H "Accept-Language: zh-CN" http://127.0.0.1:8080/
curl -s -b /tmp/ckzh.txt -L -H "Accept-Language: zh-CN" http://127.0.0.1:8080/ | grep -o "<h1[^>]*>[^<]*"
实测回显:
<h1>欢迎回来,访客!
九、部署 war 到 Tomcat 验证
将 8.1 的 war 部署到 Tomcat 10.1(要求 Servlet 5 及以上):
cp /root/lab/wicket-demo/target/wicket-demo.war /opt/tomcat/webapps/
printf "CATALINA_OPTS=\"-Xms256m -Xmx512m -Dfile.encoding=UTF-8\"\n" > /opt/tomcat/bin/setenv.sh
/opt/tomcat/bin/startup.sh
实测回显:
Tomcat started.
验证(注意上下文路径为 /wicket-demo):
curl -s -o /dev/null -w "HTTP=%{http_code}\n" -L http://127.0.0.1:8080/wicket-demo/
实测回显:
HTTP=200
rm -f /tmp/tk.txt && curl -s -c /tmp/tk.txt -o /dev/null http://127.0.0.1:8080/wicket-demo/ && curl -s -b /tmp/tk.txt -L http://127.0.0.1:8080/wicket-demo/ | grep -o "<h1[^>]*>[^<]*\|<span>DEPLOYMENT</span>"
实测回显:
<h1>Welcome, guest!
<span>DEPLOYMENT</span>
web.xml 的 deployment 模式生效后,输出中的 wicket:id 调试标记被全部剥离,组件 id 混淆为 id4 之类的短名。表单与登录验证同第八节,仅需将地址前缀改为 /wicket-demo。
JVM 堆验证(jps -v):
jps -v | grep -v Jps
实测回显(节选):
677 Bootstrap -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dsun.io.useCanonCaches=false -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.rmi/sun.rmi.transport=ALL-UNNAMED -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp
十、常见问题
Q1:页面报 500 提示 filter path was not configured?
编程式注册 WicketFilter 时缺少 filterMappingUrlPattern 初始化参数,按 5.5 节补齐。
Q2:访问首页返回 302?
Wicket 默认 REDIRECT_TO_RENDER 策略先重定向再渲染,curl 加 -L 即可得到 200 页面,并非故障。
Q3:表单提交后提示必填或参数无效?
Wicket 10 的输入参数名以页面渲染出的 name 属性为准(实测首页回显框为 p::name),脚本提交时先从 HTML 中提取该值。
Q4:中文资源显示乱码?
properties 默认按 ISO-8859-1 读取,中文内容按第六节使用 unicode 转义写法;并确认 init() 中已设置 UTF-8 的 markup 与 request 编码。
Q5:打包报 Cannot access defaults field of Properties?
固定 maven-war-plugin 为 3.5.1 后重新打包,旧版插件与 JDK 17 不兼容。
十一、参考链接
- Apache Wicket 官网:https://wicket.apache.org
- Wicket 快速开始:https://wicket.apache.org/start/quickstart.html
- Wicket 用户指南:https://ci.apache.org/projects/wicket/guide/10.x/
- Maven Central 上的 wicket-core:https://repo1.maven.org/maven2/org/apache/wicket/wicket-core/
- Apache Tomcat 10.1 文档:https://tomcat.apache.org/tomcat-10.1-doc/