Tag Archives: Apache Wicket Web

[repost ]创建并扩展Apache Wicket Web应用

original:http://www.infoq.com/cn/articles/modular-wicket

简介

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.zipmodular-warsaw.zipmodular-appendix.zip)。

参考资料

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