Ubuntu 22.04 安装 Apache Wicket 指南

Ubuntu 22.04 从零集成 Apache Wicket:apt 安装 OpenJDK 17 与 Maven,引入 wicket-core 依赖,以内嵌 Jetty 启动并用 curl 验证页面、表单与登录,含 JVM 堆与卸载。

Apache WicketApache Wicket2026/10/12

Ubuntu 22.04 安装 Apache Wicket 指南

Apache Wicket 是组件化的 Java Web 应用框架,以库的形式通过 Maven 引入工程。本文以 Ubuntu 22.04 为例,完成安装 JDK 与 Maven、创建工程、引入 wicket-core 依赖、编写含 Label、Link、Form 组件的最小应用,并以内嵌 Jetty 启动、用 curl 验证页面渲染与登录往返的完整流程。所有命令与回显均来自真实服务器实测。

一、环境信息

项目值
操作系统Ubuntu 22.04.5 LTS
JDKOpenJDK 17.0.20.1(apt 仓库 openjdk-17-jdk)
Maven3.6.3(apt 仓库 maven)
Apache Wicket10.11.0(wicket-core,Maven Central 当前稳定版)
内嵌 Jetty11.0.24(随工程依赖引入)
应用监听端口8080

二、前置条件

  1. 拥有 root 权限或可执行 sudo 的账号。
  2. 服务器可访问公网软件源与 Maven Central 仓库。
  3. 确认 8080 端口未被占用:
ss -lntp | grep 8080

无输出即表示端口空闲。

三、版本选型说明

  1. Wicket 10.x 要求 Java 17 及以上、Jakarta Servlet 5.0 及以上,故选 OpenJDK 17 与 Jetty 11 系列(11.0.24 内置 Jakarta Servlet 5.0 支持)。
  2. wicket-core 版本以 Maven Central 元数据为准,查询命令与实测回显:
curl -s https://repo1.maven.org/maven2/org/apache/wicket/wicket-core/maven-metadata.xml | grep -E "<latest>|<release>"

实测回显:

    <latest>10.11.0</latest>
    <release>10.11.0</release>
  1. Ubuntu 22.04 官方源中的 openjdk-17-jdk 与 maven 即可满足要求,无需额外安装二进制包。

四、部署步骤

4.1 安装 OpenJDK 17 与 Maven

apt-get update
apt-get install -y openjdk-17-jdk maven curl

实测回显(结尾部分):

Setting up maven (3.6.3) ...
Processing triggers for libgdk-pixbuf-2.0-0:amd64 (2.42.8+dfsg-1ubuntu0.5) ...

验证版本:

java -version

实测回显:

openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
mvn -version | head -3

实测回显:

Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64

4.2 创建 Wicket 工程

创建工程目录并直接初始化本文的最小工程结构(脚手架生成方式可参考文末参考链接中的官方快速开始文档):

mkdir -p /root/lab/wicket-demo/src/main/java/com/example
mkdir -p /root/lab/wicket-demo/src/main/resources/com/example
mkdir -p /root/lab/wicket-demo/src/main/webapp/WEB-INF

4.3 引入 wicket-core 依赖

创建 /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>

依赖说明:wicket-core 为框架本体;jetty-webapp 用于内嵌 Jetty 本地启动;slf4j-simple 提供日志实现;war 插件固定 3.5.1 以兼容 JDK 17。

4.4 编写最小应用

在 src/main/java/com/example 下创建四个类。

WicketApplication.java(应用入口,声明首页、自定义会话、挂载 /login 路由):

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);
    }
}

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;
    }
}

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);
            }
        });
    }
}

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"));
            }
        }
    }
}

4.5 编写模板、资源与启动类

在 src/main/resources/com/example 下创建两个 HTML 模板。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>

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>

本地化资源文件(中文内容使用 unicode 转义,避免编码问题)。HomePage.properties:

home.greeting=Welcome, {0}!
home.guest=guest
home.echo=Hello, {0}. The form has been submitted.

HomePage_zh_CN.properties:

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

创建 src/main/webapp/WEB-INF/web.xml(Jakarta Servlet 5.0 规范):

<?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>

创建 src/main/java/com/example/Start.java(内嵌 Jetty 启动类):

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 初始化参数,否则请求会报 filter path was not configured 错误。

4.6 编译打包

cd /root/lab/wicket-demo && mvn package

实测回显(结尾部分):

[INFO] Building war: /root/lab/wicket-demo/target/wicket-demo.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

确认产物:

ls -la target/wicket-demo.war

实测回显:

-rw-r--r-- 1 root root 6723275 Oct 12 09:34 target/wicket-demo.war

4.7 以内嵌 Jetty 启动

通过 exec 插件在 8080 端口启动,同时通过 MAVEN_OPTS 限定 JVM 堆(256 至 512MB):

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 &

约 20 秒后查看启动日志:

tail -8 /root/lab/demo.log

实测回显:

********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode.              ***
***                               ^^^^^^^^^^^                    ***
*** Do NOT deploy to your live server(s) without changing this.  ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************

4.8 验证页面、表单与登录

访问首页(Wicket 默认渲染策略首跳返回 302,用 -L 跟随):

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[^>]*>[^<]*"

实测回显:

<h1>Welcome, guest!
curl -s -b /tmp/ck.txt -L http://127.0.0.1:8080/ | grep -o "DEVELOPMENT\|DEPLOYMENT"

实测回显:

DEVELOPMENT

表单提交往返:

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.

登录往返(先提交错误密码观察反馈,再用正确账号 admin 与密码 q1w2e3r4):

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.
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!

中文本地化验证(新会话并携带 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>欢迎回来,访客!

4.9 JVM 堆设置与验证

堆内存通过 MAVEN_OPTS 传入(256 至 512MB),用 jps -v 验证:

jps -v | grep -v Jps

实测回显:

9205 Launcher -Xms256m -Xmx512m -Dclassworlds.conf=/usr/share/maven/bin/m2.conf -Dmaven.home=/usr/share/maven -Dlibrary.jansi.path=/usr/share/maven/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/root/lab/wicket-demo

ps -ef 同样可确认:

ps -ef | grep "Xmx512m" | grep -v grep | head -1

实测回显(节选):

/usr/bin/java -Xms256m -Xmx512m -classpath /usr/share/maven/boot/plexus-classworlds-2.x.jar org.codehaus.plexus.classworlds.launcher.Launcher -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=com.example.Start

五、配置说明

  1. 运行模式:内嵌启动通过系统属性 wicket.configuration=development 指定;war 中的 web.xml 以 context-param configuration 声明,部署到独立运行环境时建议改为 deployment。
  2. 应用入口:WicketFilter 的 init-param applicationClassName 指向 WicketApplication 全限定名;编程式注册时还需 filterMappingUrlPattern 参数。
  3. 端口:Start.java 中 new Server(8080) 可改为其他端口,修改后需重启进程。
  4. 堆内存:调整 MAVEN_OPTS 后重新启动进程生效。
  5. 会话超时:内嵌 Jetty 默认会话超时由 Servlet 规范决定,可在 web.xml 的 session-config 中显式声明,例如 30 分钟:
<session-config>
    <session-timeout>30</session-timeout>
</session-config>

六、注意事项

  1. 内嵌 Jetty 的启动方式适合开发验证,生产部署建议将 war 部署到独立的 Servlet 5 兼容运行环境(如 Tomcat 10.1)。
  2. pom.xml 必须显式固定 maven-war-plugin 3.x,否则在 JDK 17 下打包会报 Cannot access defaults field of Properties 错误。
  3. Wicket 默认渲染策略为 REDIRECT_TO_RENDER,首次 GET 与表单提交后均返回 302,curl 验证必须带 -L。
  4. 本地化资源默认按 ISO-8859-1 读取,中文内容务必采用 unicode 转义写法。
  5. exec 插件以内嵌方式启动的应用随 Maven 进程存在,停止时结束该 Maven 进程即可。

七、常见问题

Q1:请求报 500,日志出现 filter path was not configured?

编程式注册 WicketFilter 时缺少 filterMappingUrlPattern 初始化参数,按 4.5 节的 Start.java 补上即可。

Q2:访问首页返回 302 是否异常?

正常。Wicket 默认 REDIRECT_TO_RENDER 策略先重定向再渲染,curl 加 -L 或浏览器访问都能得到 200 的最终页面。

Q3:中文页面出现乱码?

检查 zh_CN 资源文件是否使用 unicode 转义;确认 WicketApplication 中已设置 defaultMarkupEncoding 与 responseRequestEncoding 均为 UTF-8。

Q4:打包时报 Cannot access defaults field of Properties?

Maven 默认解析的旧版 war 插件与 JDK 17 不兼容,在 pom.xml 的 build/plugins 中固定 maven-war-plugin 3.5.1。

八、卸载方式

停止应用进程:

kill $(jps | grep Launcher | awk "{print \$1}")

移除工程目录与本地仓库:

rm -rf /root/lab/wicket-demo /root/.m2

移除 Maven 与 JDK(如不再需要):

apt-get remove --purge -y maven openjdk-17-jdk
apt-get autoremove -y

九、参考链接

  1. Apache Wicket 官网:https://wicket.apache.org
  2. Wicket 快速开始:https://wicket.apache.org/start/quickstart.html
  3. Maven Central 上的 wicket-core:https://repo1.maven.org/maven2/org/apache/wicket/wicket-core/
  4. Jetty 11 文档:https://www.eclipse.org/jetty/documentation/jetty-11/
  5. Maven 官方文档:https://maven.apache.org/guides/index.html