OceaBase开发者大会落地上海!4月20日共同探索数据库前沿趋势!报名戳 了解详情
写点什么

创建并扩展 Apache Wicket Web 应用

  • 2010-06-28
  • 本文字数:13389 字

    阅读完需:约 44 分钟

简介

Apache Wicket 是一个功能强大、基于组件的轻量级 Web 应用框架,能将展现和业务逻辑很好地分离开来。你能用它创建易于测试、调试和支持的高质量 Web 2.0 应用。假设其他团队交付了一个基于 Wicket 的应用,你必须扩展该应用,但又不能修改他们的代码;或者你必须要交付一个模块化的 Web 应用,能让其他团队很容易地扩展和定制。本文介绍的正是如何在不引入多余源代码、标记和配置的情况下解决此问题。我们用 maven-war-plugin 合并项目,用 wicketstuff-annotations 动态装载网页,用 Spring 框架作为控制反转(IoC)容器,以此达到该目的,并借助 wicket-spring-annot 项目和 Maven 依赖的微调对应用进行增强。

本文旨在展示如何从头开始设计和构建一个高度模块化、可扩展、基于 Wicket 的 Web 应用。文章会指导读者完成这一过程的所有步骤,从编写初始的 Maven POM 文件、选择必需的依赖开始,直到完成组件的配置、服务的自动装配(autowire)及网页的装载。

本文包括两个 Maven 管理的示例应用——Warsaw 和 Global。Warsaw 是进行了全面配置的 Web 应用,带有两个简单的 Web 页面。Global 依赖于 Warsaw 项目,引入了一个服务和几个新的 Web 页面,还修改了 Warsaw 组件的拷贝。这两个 Web 应用都打包为 WAR 文件,并进行了配置,能在 Jetty 或其它 Servlet 容器中运行。在命令行运行 mvn jetty:run-war 命令即可轻松启动这两个应用。

用例

假设有一个 Web 应用是基于 Wicket 应用框架构建的,你需要创建这个已有应用的定制版本。举例来说,你需要在主页面的页眉添加链接,以链接到外部资源。要实现该功能,你可以创建一个新的 Wicket 面板组件,将其实例添加到需要的网页中。如果这是应用主版本的功能,就很简单了。但要是不允许你引入任何功能变化,只能访问现有应用的源代码和资源,那你该如何完成这一任务呢?解决这个问题的方式有好几种。本文接下来将对其中之一展开深入讨论。

Maven 的 WAR 插件

Java 编写的简单 Web 应用可发布为一个 WAR 文件,里面包含编译好的类、JSP 和 XML 文件、静态网页及其他资源。使用 maven-war-plugin 插件可以完成几个 WAR 文件的合并。我们需要做的只是在应用的 pom.xml 文件中为 WAR 文件设置打包属性,并设置对另一个 WAR 文件的依赖。本文使用了两个示例应用——主应用 Warsaw 和依赖于 Warsaw 项目的 Global。清单 1 和清单 2 分别显示了 Warsaw 项目和 Global 项目中 pom.xml 的基本版本。

清单 1:Warsaw 项目中 Maven pom.xml 文件的基本版本。

复制代码
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.modular</groupId>
<artifactId>warsaw</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>Modular Wicket Warsaw Project</name>
<dependencies>
<!-- Warsaw 项目的依赖配置 -->
</dependencies>
</project>

清单 2:Global 项目中 Maven pom.xml 文件的基本版本,Global 依赖于 Warsaw。

复制代码
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.modular</groupId>
<artifactId>global</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>Modular Wicket Global Project</name>
<dependencies>
<dependency>
<groupId>com.example.modular</groupId>
<artifactId>warsaw</artifactId>
<version>1.0</version>
<type>war</type>
</dependency>
</dependencies>
</project>

两个项目编译、打包之后,生成的 WAR 文件(warsaw-1.0.war 和 global-1.0.war)几乎是相同的,尽管 Global 项目还没有任何类和资源。重要的是,两个 WAR 归档文件中都有全部的依赖库和配置。

根据 Java 规范,classpath 不能指定 WAR 文件。这就意味着在编译时,Global 项目无法访问 Warsaw 项目中定义的类,所以在 Global 项目中,我们不能像常规类组件那样扩展或使用 Warsaw 定义的类。要解决这一问题,我们必须重新设置 maven-war-plugin 的一项缺省配置,该设置如下面的清单 3 所示。

清单 3:将以下配置添加到 Warsaw 项目的 Maven pom.xml 文件中。

复制代码
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<attachClasses>true</attachClasses>
</configuration>
</plugin>
<plugins>
<build>

启用 attachClasses 选项可以把 JAR 文件(warsaw-1.0-classes.jar)和标准的 WAR 文件同时安装到 Maven 仓库中。要访问该 JAR 文件,我们需要像清单 4 所示的那样修改 Global 项目的依赖列表。

清单 4:Global 项目的 Maven pom.xml 文件中,修改后的依赖设置。

复制代码
<dependencies>
<dependency>
<groupId>com.example.modular</groupId>
<artifactId>warsaw</artifactId>
<version>1.0</version>
<type>war</type>
</dependency>
<dependency>
<groupId>com.example.modular</groupId>
<artifactId>warsaw</artifactId>
<version>1.0</version>
<type>jar</type>
<classifier>classes</classifier>
<scope>provided</scope>
</dependency>
</dependencies>

可以看到,Global 项目用 Warsaw WAR 创建最终的 Web 归档文件,出于编译需要,还使用了 Warsaw 的类(打包在 JAR 里)。我们将属性 classifier 设置为 classes,以此定义该从仓库中选择哪个工件。将 scope 设置为 provided,则是告诉 Maven 只在编译时需要该工件,运行时则从其他地方获得。“其他地方”当然就是指 Warsaw 项目的 WAR 工件,WAR 插件会将 WAR 和 JAR 合并在一起。现在已经正确配置了依赖关系,那我们就开始构建派生的 Wicket 应用吧。

Wicket 框架介绍

要开始 Apache Wicket 之旅,建议你构建、研究一下 Apache Wicket 的 QuickStart 应用。如果你觉得这个框架有用且有趣,也推荐你读一读《Wicket in Action》这本书。Wicket 框架中,主应用类必须继承 org.apache.wicket.protocol.http.WebApplication,Web 页面可以在主应用类的 init() 方法中进行装载。该技术很常用,但也有一个不利之处。如果主应用类是在基项目(这里是 Warsaw)里定义的,那依赖应用(Global)就不能添加新的 Web 页面了。当然,我们可以在其他项目中再一次继承该类,但接着还要修改 web.xml 中对该类的引用,如清单 5 所示。该问题的一个解决方法是引入一个系统,该系统能自动发现、装载 classpath 里 JAR 包中的 Wicket 网页。示例应用使用了 WicketStuff 注解驱动的解决方案。

清单 5:Wicket QuickStart 应用的 web.xml 文件片段。

复制代码
<filter>
<filter-name>wicket.base</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
<param-name>applicationClassName</param-name>
<param-value>com.example.modular.warsaw.WicketApplication</param-value>
</init-param>
</filter>

WicketStuff 注解

这个软件包提供了智能的装载机制,利用 @MountPath 类注解和 AnnotatedMountScanner 工具类可以简化 Wicket 网页的添加和注册。从清单 6 和清单 7 可以看到,配置非常简单。

清单 6:WicketStuff AnnotatedMountScanner 的使用示例。

复制代码
// package 和 import 信息
public class WicketApplication extends WebApplication {
public Class getHomePage() {
return HomePage.class;
}
@Override
protected void init() {
super.init();
new AnnotatedMountScanner().scanPackage("com.example.modular.**.pages").mount(this);
}
}

Wicketstuff-annotations 项目使用 Sping 框架的 PathMatchingResourcePatternResolver 来查找 classpath 中匹配的资源。在上面的例子里,解析器会扫描 com.example.modular 包里所有的“pages”子包,找到所有使用 @MountPath 注解的类。

清单 7:WicketStuff @MountPath 注解的使用示例。

复制代码
package com.example.modular.warsaw.pages;
import org.apache.wicket.markup.html.WebPage;
import org.wicketstuff.annotation.mount.MountPath;
@MountPath(path = "warsaw")
public class WarsawPage extends WebPage {
// 现在不需要什么内容
}

借助这一技术,我们在 Warsaw 和 Global 项目中都能定义并装载 Web 页面。Maven 的 WAR 插件确保部署的 Global 能包含这些类,目录结构与 Warsaw 的保持一致。即使 Web 页面位于不同的包里,比如 com.example.modular.warsaw.pages 和 com.example.modular.global.pages,AnnotatedMountScanner 也能确保正确装载了网页。但我们要是在 Global 项目的 com.example.modular.warsaw.pages 包里新建一个 Web 页面,会发生什么呢?这种情况存在风险,就是该页面会覆盖 Warsaw 项目里定义的同名页面。反过来说,这种做法却有助于我们从 Global 项目级别修改 Warsaw 项目的组件。

替换组件

用 Maven 的 WAR 插件替换类、配置或其他资源听起来是个很糟糕的主意。在大多数情况下也的确如此。不过在个别情况下也没有更为简单的选择。如果我们快速检索一遍 Wicket Web 应用的源代码,就会发现大部分 Wicket 组件都是用 new 关键字实例化的。这种技术很常见,也被视为标准做法。那我们如何在派生项目里修改这些类的行为呢?你的第一反应也许是利用 Spring 的 IoC 容器把组件注入到特定的 Web 页面,尝试使用 IoC 容器内置的 Bean 替换机制。这听起来不错,但我们在 Wicket-Spring 集成项目的 Wiki 页面上看到,“借助 IoC 容器注入依赖产生的问题,大部分是因为 Wicket 是一个非托管的框架,而且 Wicket 组件和模型往往会被序列化”。简言之,就是 Wicket 不会管理组件的生命周期,而序列化则可能导致一些严重的问题,比如在集群中。即便我们找到了该问题的解决方法,那 XHTML 标记文件又怎么办呢?每个模板文件都关联到一个特定的 Java 类文件。举例来说,/com/example/modular/pages/WarsawPage.html 绑定到 com.example.modular.pages.WarsawPage 类。要解决这个问题,我们还需要一种机制来妥善处理这些关联。比如说,该机制要能动态替换、实例化类,还可以与负责绑定标记文件的 Wicket 机制交互。这种机制可以单独拿出来开篇讲述,这里先略过。

我们看到,这个问题的确让应用的扩展变得复杂起来。正如我在文章开头写的,我们可以试试 maven-war-plugin 的缺省重写功能,不要经常修改基项目。

Global 项目中,maven-war-plugin 重写了 Warsaw 项目的文件,而没有任何确认对话框或警告信息。这是插件的缺省设置,这种情况需要这样使用。要想了解它在实际中是如何工作的,请看附带的示例应用代码。

回到 Spring

Spring 是个强大而有用的框架,我们可以在基于 Wicket 的应用中使用它。Wicket-Spring 项目的贡献者提供了一个出色的 Spring 集成机制,很容易用于 Wicket Web 应用。它提供了用 Spring IoC 容器往 Web 组件注入依赖的一些方式。本文提供的示例应用(Warsaw 和 Global)选择了基于注解的方法。可惜该方法不能用来注入 Wicket 组件,但它能将服务注入到这些 Wicket 组件中。

要充分利用这一特性,我们首先要修改 Warsaw 应用的 Servlet 配置文件,以使用 Wicket 和 Spring。清单 8 中最有趣的部分是定义了带有两个参数的 contextConfigLocation。第一个是定义主应用上下文的 WEB-INF/applicationContext.xml,该文件定义了 Wicket Web 应用中的 Bean。我们也可以在该文件中定义应用使用的服务或 DAO 类。classpath*:META-INF/example/extra-ctx.xml 则表明,classpath 里所有 META-INF/example 目录下的 extra-ctx.xml 文件也是应用上下文文件,可以定义更多的 Spring 类。更重要的是,Global 项目也会使用 Warsaw 项目的 Servlet 配置文件 web.xml,所以 Warsaw 项目、Global 项目、以及两个应用使用的所有 jar 文件都会查找 extra-ctx.xml 文件。这就是 Global 项目几乎不需要编写 Web 应用配置文件的原因。Global 项目引入了一个名为 RandomTzService 的示例服务,来解释如何做到这一点。RandomTzService 服务只有一个方法,返回随机选中时区的标识符。我们的应用使用基于注解的方法注入 Spring 类。

清单 8:Warsaw 应用的 web.xml 文件片段。

复制代码
<context-param>
<description>Spring Context</description>
<param-name>contextConfigLocation</param-name>
<param-value>
<string>WEB-INF/applicationContext.xml</string>
<string>classpath*:META-INF/example/extra-ctx.xml</string>
</param-value>
</context-param>
<servlet>
<servlet-name>WebClientApplication</servlet-name>
<servlet-class>org.apache.wicket.protocol.http.WicketServlet</servlet-class>
<init-param>
<param-name>applicationFactoryClassName</param-name>
<param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
</init-param>
</servlet>

清单 9:Global 应用的 extra-ctx.xml 文件片段,包含注解服务的本地化内容。

复制代码
<context:annotation-config />
<context:component-scan base-package="com.example.modular.global.service" />

RandomTzService 的实现放在 com.example.modular.global.service 包里,并使用 Spring 的 @Service 注解。根据清单 9 的定义,RandomTzService 的实现能被自动发现。要在应用中使用该服务,我们只需利用 Spring 的自动装配(autowire)机制,在需要注入该服务的属性上使用 @Autowired 注解就可以了。首先,在 Web 应用的类里,我们必须添加负责将依赖注入 Wicket 组件的组件实例化监听器。这听起来很复杂,不过 wicket-spring-annot 项目提供了能实现这一切的 SpringComponentInjector 类。清单 10 展示了这些代码。

清单 10:Warsaw 项目的主应用类,其中示范了 Spring 类注入机制和基于注解的 Web 页面扫描器的使用。

复制代码
// package 和 imports 信息
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
import org.wicketstuff.annotation.scan.AnnotatedMountScanner;
public class WicketApplication extends WebApplication {
@Override
protected void init() {
super.init();
addComponentInstantiationListener(new SpringComponentInjector(this));
new AnnotatedMountScanner().scanPackage("com.example.modular.**.pages").mount(this);
}
// 其它方法
}

要将服务注入 Wicket 组件类中,我们必须使用 wicket-spring-annot 项目定义的 @SpringBean,对正确类型的属性进行注解。当该组件实例化时,SpringComponentInjector 会查找使用 @SpringBean 注解的属性,并注入所需的依赖关系。我们不必担心被注入依赖的序列化问题,因为依赖都表示为序列化的代理,能自动完成序列化。在使用该方法时,我们必须记住该机制只支持访问器注入,不支持基于构造函数参数的注入。

AJAX 登场

现在完成了主要的组成部分,我们就能用 Web 2.0 组件对应用进行增强了。Wicket 框架对 AJAX 有良好的本地支持,即便该技术可有可无。Wicket 缺省更胜任传统的要求,但要为新的组件或现有的组件添加 AJAX 支持,Wicket 也很容易做到。你甚至不用编写任何 JavaScript 代码,就有可能创建动态的 Web 页面。用户仍然可以使用标准的 JS 脚本,并为个别 Wicket 组件添加 JavaScript 函数调用。要添加 JS 函数调用,可以在 Java 代码中用编程的方式完成,这与网页中动态添加 CSS 是一样的。

Wicket 框架带有一套可重用的 AJAX 行为和组件。最简单的例子是 AjaxFallbackLink。AjaxFallbackLink 在禁用或不支持 JavaScript 的 Web 浏览器中也能使用。在这种情况下,点击一个链接就能重新加载整个页面。正如清单 11 所示的那样,创建一个传统的链接非常简单。该示例类是个 Wicket 面板,带有一个能点击的链接,还有一个显示链接点击次数的标签。

清单 11:带有传统链接的 Wicket 面板。

复制代码
// package 信息
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.PropertyModel;
public class ClassicApproach extends Panel {
private int clickCount = 0;
public ClassicApproach(String pId) {
super(pId);
add(new Label("clickLabel", new PropertyModel(this, "clickCount")));
add(new Link("link") {
@Override
public void onClick() {
clickCount++;
}
});
}
}

用户点击该链接时,整个 Web 页面会被重新加载。很多情况下,我们都想在后台执行此操作,只更新或改变页面的一部分内容。使用 AJAX 能轻松做到这一点,如清单 12 所示。

清单 12:带有 AJAX 链接的 Wicket 面板。

复制代码
// package 信息
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.PropertyModel;
public class AjaxApproach extends Panel {
private int clickCount = 0;
public AjaxApproach(String pId) {
super(pId);
final Label label = new Label("clickLabel", new PropertyModel(this, "clickCount"));
label.setOutputMarkupId(true);
add(label);
add(new AjaxFallbackLink("link") {
@Override
public void onClick(AjaxRequestTarget pTarget) {
clickCount++;
if (pTarget != null) {
pTarget.addComponent(label);
}
}
});
}
}

比较这两种方法,我们看到只修改了少量的内容。AJAX 链接的 onClick 方法里,我们接收了一个 AjaxRequestTarget 对象。AjaxRequestTarget 对象用来标识 AJAX 请求中将被更新的组件。如果我们使用的 Web 浏览器不支持 JavaScript,AjaxRequestTarget 对象就会为 null。除此之外,源代码几乎是相同的,而且清单 13 所示的 XHTML 标记在两种方法中都能使用。仅用了几行 Java 代码,我们就能更新 Web 页面的组件,而无需刷新整个页面。多亏了 Wicket 内置的 AJAX 引擎,我们才不用编写单独的 JavaScript 代码,或手动链接到 JavaScript 库。后台会自动添加所有基本的 JavaScript 功能。

下面的清单 13 展示了嵌套在 HTML span 标记里的 Wicket 标签。如果 Web 浏览器支持 JavaScript,就只会更新该标记的内容。

清单 13:显示链接点击次数的标记代码,传统链接和 AJAX 链接都适用。

复制代码
<a href="#" wicket:id="link">This link</a> has been clicked <span wicket:id="clickLabel">[link label]</span> times.

要查看更多 Wicket 支持 AJAX 的例子,请参考 Global 项目的时区面板。其中一个面板使用了定时更新特定组件的 AJAX 行为。正如清单 14 所示的一样,给 Wicket 组件添加这样的行为实在是轻而易举。

清单 14:定时更新关联组件的 AJAX 行为。

复制代码
someWicketComponent.add(new AjaxSelfUpdatingTimerBehavior(Duration.seconds(1)));

Wicket Library 的例子部分能找到一些很好的 Wicket AJAX 组件示例。这些小应用都已部署,不用下载就能看出它们的功能。Java 源代码和 XHTML 标记文件也包括在内。

测试 Wicket 组件

现在的 Java Web 框架有一个重要特性,就是无需将应用部署到容器,就能对展现层进行单元测试。Wicket 提供了 WicketTester 辅助类,使用该辅助类可以实现能在展示层直接运行的单元测试。与协议级的测试框架(JWebUnit 或 HtmlUnit 等)相比,我们能完全控制页面和各个组件,能在 Servlet 容器外对它们进行单元测试。这种做法能使带有功能测试的测试驱动开发(TDD)成为可能,并变得快速、可靠。

使用 WicketTester 创建、运行单元测试非常快速、简单,如下所示。

清单 15:使用 JUnit 和 WicketTester 的简单 Wicket 单元测试。

复制代码
// package 和 imports 信息
import junit.framework.TestCase;
import org.apache.wicket.util.tester.WicketTester;
public class MyHomePageTest extends TestCase {
public void testRenderPage() {
WicketTester tester = new WicketTester();
tester.startPage(MyHomePage.class);
tester.assertRenderedPage(MyHomePage.class);
}
}

在这个示例中,WicketTester 为了测试而创建了一个虚拟的 Web 应用。实际应用则是操作一个 Wicket 应用类的具体实例。比如在 Warsaw 和 Global 项目里,我们主要依靠 Spring 来装载 Web 页面、注册服务。因此,我们可以为所有 Wicket 相关的单元测试创建一个测试基类。

清单 16:Warsaw 和 Global 项目的 Wicket 单元测试基类。

复制代码
package com.example.modular;
import junit.framework.TestCase;
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
import org.apache.wicket.spring.injection.annot.test.AnnotApplicationContextMock;
import org.apache.wicket.util.tester.WicketTester;
import org.springframework.context.ApplicationContext;
import com.example.modular.warsaw.WicketApplication;
public abstract class BaseTestCase extends TestCase {
protected WicketTester tester;
protected ApplicationContext mockAppCtx;
protected WicketApplication application;
@Override
public void setUp() {
mockAppCtx = new AnnotApplicationContextMock();
application = new WicketApplication();
SpringComponentInjector sci = new SpringComponentInjector(application, mockAppCtx);
application.setSpringComponentInjector(sci);
tester = new WicketTester(application);
}
}

我们从清单 16 看出,虚拟的应用上下文和标准的 Wicket 应用实例已经创建好了。一切就绪后,我们就能测试页面组件了。WicketTester 提供的方法可以进行很多操作,比如点击链接、填写并提交表单、检查标签或其他组件,不一而足。这些方法大多将组件 ID 作为第一个参数。出现嵌入组件时,用冒号分隔组件 ID。

清单 17:WicketTester 辅助方法的用法示例。

复制代码
// 打开页面以便测试
tester.startPage(InfoPage.class);
// 检查页面是否已渲染
tester.assertRenderedPage(InfoPage.class);
// 检查给定组件的类
tester.assertComponent("leftPanel", LeftTextPanel.class);
// 检查标签是否正确显示
tester.assertLabel("leftPanel:header", "My info page");
// 根据名称获取 Wicket 组件
Component someLink = tester.getComponentFromLastRenderedPage("someLink");
// 点击链接
tester.clickLink("leftPanel:redirectLink");
// 检查页面是否发生了变化
tester.assertRenderedPage(AfterRedirectPage.class);

Wicket 还提供了专门测试表单的工具类 FormTester。我们可以借助于该工具类以编程的方式设置文本框、复选框、单选按钮的值,甚至从下拉列表中进行选择。也可以模拟文件的上传和表单的定期提交。

清单 18:FormTester 辅助方法的用法示例。

复制代码
// 打开页面以便测试
tester.startPage(MyFormPage.class);
tester.assertComponent("form", TheForm.class);
// 检查 status 标签
tester.assertLabel("status", "Please fill the form");
// 准备表单测试类
FormTester formTester = tester.newFormTester("form");
// 检查 name 和 age 是否为空
assertEquals("", formTester.getTextComponentValue("name"));
assertEquals("", formTester.getTextComponentValue("age"));
// 填写 name 和 age
formTester.setValue("name", "Bob");
formTester.setValue("age", "30");
// 提交表单
formTester.submit("submitButton");
// 检查 status 标签是否发生了变化
tester.assertLabel("status", "Thank you for filling the form");

表单验证的测试也可以在单元测试中完成(清单 19 展示了表单验证的测试代码)。

清单 19:使用 WicketTester 和 FormTester 测试空表单提交。

复制代码
// 检查是否没有错误消息
tester.assertNoErrorMessage();
// 重置 name 和 age
formTester.setValue("name", "");
formTester.setValue("age", "");
// 提交空表单
formTester.submit("submitButton");
// 检查错误消息是否正常显示出来
tester.assertErrorMessages(new String[] {"Field 'name' is required.", "Field 'age' is required."});

Wicket 测试工具有个很有趣的特性,就是能脱离 Web 页面测试单独的组件。假设一个 Web 页面由几个面板组成,比如 leftPanel、centerPanel、header 和 footer。这些组件分别由不同类的实例来展示。如果每个面板都可见,而且它们之间的通讯处理妥当,我们就可以在页面级别进行测试。如果同一个组件用在好几个 Web 页面,那我们就应该对这些组件进行单独测试。在这种情况下,我们可以使用 WicketTester#startPanel(Panel) 方法,如清单 20 所示。

清单 20:使用 WicketTester 辅助方法测试单独的面板。

复制代码
// 创建用于测试的面板
tester.startPanel(new TestPanelSource(){
public Panel getTestPanel(String pId) {
return new CenterPanel(pId);
}
});
// 检查 header 标签是否正确显示
tester.assertLabel("panel:header", "This is center panel");

借助于 Wicket 的 TestPanelSource 类,我们可以根据测试需要延迟初始化给定的面板。执行初始化后,我们就可以测试面板特定的行为了。必须记住,组件初始化时要带着 panel 作为组件 ID,所以访问组件的子组件时我们必须使用 panel: 作为 ID 前缀。

测试 Wicket 的 Web 页面和组件并非难事。Wicket 框架提供的实用工具类可以测试内容渲染、导航和数据流。当然,编写富组件的单元测试需要经验、时间和精力,但这也是应用开发的一个重要组成部分。在测试驱动开发中,跟团队成员执行的手动测试相比,自动测试的编写应该放在首要位置。

我们可以在 Apache Wiki 页面上找到一些有关单元测试的有用提示。《Wicket in Action》一书也包含测试相关的内容,可以作为很好的测试入门。

选择 Apache Wicket

有了上述信息,你应该自己尝试一下 Wicket 框架。本文为了简单起见,省略了几个重要主题,比如组件模型和安全。幸好网络上有一些 Wicket 的介绍文章和指南。了解 Wicket 用户中流行的做法总归是件好事儿。

首先,深刻理解面向对象编程至关重要。每个 Wicket 应用的源代码中都会发现匿名的内部类。这种技术有助于敏捷开发——不需要创建许多只在一个组件中使用的具体类。不过这些匿名内部类要是嵌套得太深,看起来会很混乱。尤其是名为 SomeClass$2$2$1 的类引发异常的时候。我们还可以使用封装和继承给现有页面和组件添加新的行为。如果没有坚实的面向对象设计基础,我们很快就会迷失在对象、关系和类的世界里。

我们知道,Wicket 是一个基于组件的框架。每个组件由一个 Java 类,以及一组具有相同名称、不同扩展名的文件表示。这些文件有标记、CSS、消息资源等。默认情况下,这些文件按相同的目录结构存放。如果我们开发的应用有非常多的专用组件,还包含一些 Web 页面,那我们可能会被那么多不得不管理的文件弄到抓狂。这个问题还没有很简单的解决办法,但保持 Java 源代码与其他资源之间的分离还是个很好的做法。这也有助于应用逻辑和展现逻辑之间的分离,因为整个页面逻辑只以 Java 类的形式被包含进来。

只要我们看一看 Wicket 的 API 就会发现,这是一个不同类和方法的大集合。Wicket 框架提供了很多开箱即用的组件、行为和工具,但在大多数情况下,这也导致 Wicket 的学习曲线非常陡峭。这与 Struts、JSF 完全不同,你能找到很多关于 Struts 和 JSF 的书。如果你对不同 Java Web 框架的对比感兴趣,你可以在 Matt Raible 的主页上找到一些很不错的框架比较演讲。它们稍微有点儿陈旧,但包含的大部分信息仍然有用、有效。

总结

如果我们在互联网上搜索 Java 平台上构建 Web 应用的框架,可能会发现至少有十几种框架能满足我们的大部分需要。我们的选择往往取决于 Web 应用的需求和个人喜好。正如本文所介绍的,Wicket 是一个支持良好的框架,带有很多有用且易用的扩展。构建高度模块化的应用仅仅取决于两三个组件的基本设置。请参考示例应用的源代码,以了解 Wicket 组件在实际中是如何协作的。你只需要安装 JDK 1.5 或更高版本,还有 Maven 2,就能享受模块化 Wicket 之旅了。

关于作者

Krzysztof Smigielsk 是 ConSol* Consulting & Solutions 公司波兰分公司的一名软件开发人员。Krzysztof 毕业于波兰雅盖隆大学(Jagiellonian University),拥有计算物理学硕士学位,自 2005 年以来一直从事 Java 数值性能的相关工作,2007 年开始也涉足服务器端领域。

附录

注入服务

“回到 Spring”部分描述了 Spring 的自动装配机制,该附录对自动装配机制的用法做进一步的阐述。在基于 Warsaw 的应用中,引入 Spring 上下文文件很容易。只要这些文件的名称和路径匹配 classpath*:META-INF/example/extra-ctx.xml 模式,它们就能被自动处理。由于 Global 项目与 Warsaw 项目进行了物理上的合并,所以这一特性在 Global 项目中体现得并不是很明显。假如 Warsaw 是个独立项目,为了演示该机制的工作原理,我们创建一个名为 Appendix 的应用。Appendix 应用包含 AppendixService 及其实现,还有位于 /src/main/resources/META-INF/example 目录下的上下文文件 extra-ctx.xml。要构建该应用,我们需要解压 modular-appendix.zip 归档文件,并执行 mvn clean install 命令。这样,名为 modular-appendix-1.0.jar 的工件将被安装到本地 Maven 仓库中。

要了解如何在基于 Warsaw 的项目中使用 Appendix 应用,我们需要在该项目的 pom.xml 文件中添加新的依赖,如清单 21 所示。

清单 21:基于 Warsaw 项目的 pom.xml 文件片段。

复制代码
<dependencies>
<!-- ... ->
<dependency>
<groupId>com.example.modular</groupId>
<artifactId>modular-appendix</artifactId>
<version>1.0</version>
<type>jar</type>
</dependency>
<!-- ... ->
</dependencies>

当我们执行 mvn jetty:run-war 命令启动 Web 应用时,可以看到服务器日志里记录了新的 Bean 被发现、并被实例化——见清单 22。

清单 22:Global 项目添加 modular-appendix 依赖后的格式化日志。

复制代码
DEBUG PathMatchingResourcePatternResolver
Looking for matching resources in jar file
[file:/modular-global/target/work/webapp/WEB-INF/lib/modular-appendix-1.0.jar]
DEBUG PathMatchingResourcePatternResolver
Resolved location pattern
[classpath*:com/example/modular/appendix/service/**/*.class] to resources
[URL[jar:file:/modular-global/target/work/webapp/WEB-INF/lib/modular-appendix-1.0.jar
/com/example/modular/appendix/service/AppendixServiceImpl.class]]
DEBUG XmlBeanDefinitionReader
Loaded 2 bean definitions from location pattern
[classpath*:META-INF/example/extra-ctx.xml]
...
DEBUG DefaultListableBeanFactory
Creating shared instance of singleton bean 'appendixServiceImpl'
DEBUG DefaultListableBeanFactory
Creating instance of bean 'appendixServiceImpl'
DEBUG DefaultListableBeanFactory
Eagerly caching bean 'appendixServiceImpl' to allow for resolving
potential circular references
DEBUG DefaultListableBeanFactory
Finished creating instance of bean 'appendixServiceImpl'

要了解实现细节,请查看 Appendix 项目的源代码( modular-global.zip modular-warsaw.zip modular-appendix.zip )。

参考资料

查看英文原文: Creating and Extending Apache Wicket Web Applications


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-06-28 00:226906
用户头像

发布了 151 篇内容, 共 60.1 次阅读, 收获喜欢 18 次。

关注

评论

发布
暂无评论
发现更多内容

zookeeper-集群和zab协议

zarmnosaj

7月月更

Prometheus 发布 LTS 长期支持版本啦

耳东@Erdong

release Prometheus 7月月更

没有了可用Task slot,Flink新增任务会怎样?

程序员欣宸

Java flink 7月月更

python小知识-什么是上下文管理

AIWeker

Python python小知识 7月月更

一篇文章带你快速学会Flex布局

bo

CSS 前端 Flex 7月月更

【愚公系列】2022年7月 Go教学课程 013-常量、指针

愚公搬代码

7月月更

试着换个角度理解低代码平台设计的本质

pingan8787

Vue 前端 React 低代码平台

短视频直播系统源码

开源直播系统源码

短视频源码 直播系统源码 开源源码

strcat() - 连接字符串

謓泽

7月月更

Spring系列一:Spring基础篇

叶秋学长

读书笔记之数据密集型应用的可维护性

宇宙之一粟

设计数据密集型应用 7月月更

数据平台的发展历程

奔向架构师

大数据 7月月更

汽车电子行业开发者的内功心法:汽车软件开发V模型(瀑布模型)

不脱发的程序猿

嵌入式开发 瀑布模型 汽车软件开发 V模型

Flink实战:消费Wikipedia实时消息

程序员欣宸

Java flink 7月月更

LeetCode-数组中数字出现的次数(单身狗问题)

芒果酱

c++ C语言 数据结构算法 Leet Code 7月月更

STM32+DHT11读取温湿度数据显示

DS小龙哥

7月月更

jQuery 请求

Jason199

jquery js post GET 7月月更

qt 实现日历美化

小肉球

qt 7月月更

KUDU1.11 环境安装

怀瑾握瑜的嘉与嘉

7月月更 kudu

王者荣耀商城异地多活架构

Pengfei

Java核心技术之泛型详解

小明Java问道之路

Java 后端 泛型 Java泛型 7月月更

系统刷JavaScripit 构建前端体系(语法篇)

程序员海军

JavaScript 7月月更

小心!正则 test() 匹配的一个“坑”

掘金安东尼

正则 7月月更

使用Flutter开发小程序+App)的一种组合思路

Geek_99967b

小程序

C#入门系列(二十五) -- 接口

陈言必行

7月月更

VLAN再见,我选择用QinQ!1000字带你详细了解QinQ技术

wljslmz

VLAN 网络技术 7月月更 QinQ

Java中的设计模式

Java学术趴

7月日更

分享 15 个 Vue3 全家桶开发的避坑经验

pingan8787

Vue Vue3

QT|QLabel显示多行文本过多后显示省略号

中国好公民st

qt 7月月更

Qt | QWidget的一些总结

YOLO.

qt 7月月更

C# DataGridView数据导出Excel文件

IC00

C# 7月月更

创建并扩展Apache Wicket Web应用_Java_Krzysztof Śmigielski_InfoQ精选文章