Raible's Wiki

Raible Designs
Wiki Home
News
Recent Changes

AppFuse

Homepage
  - Korean
  - Chinese
  - Italian
  - Japanese

QuickStart Guide
  - Chinese
  - French
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish
  - Japanese

User Guide
  - Korean
  - Chinese

Tutorials
  - Chinese
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish

FAQ
  - Korean

Latest Downloads

Other Applications

Struts Resume
Security Example
Struts Menu

Set your name in
UserPreferences

Edit this page


Referenced by
Articles
Articles_cn
Articles_pt
Articles_zh
CreateManager_zh
ValidationAndList_zh




JSPWiki v2.2.33

[RSS]


Hide Menu

CreateActions_zh


Part III: 创建 Struts Actions和JSPs - 在AppFuse架构下创建Struts 的Actions和JSPs。

本教程依赖于Part II: 创建新的Manager

关于本教程

本教程将向你展示如何创建一个Struts Action,一个JUnit Test(使用 StrutsTestCase),和一个form的JSP,我们创建的这个Action将与教程创建Managers创建的PersonManager交互。

通常情况下,AppFuse使用Struts作为它的web框架,作为1.6+,你可以使用Spring或者WebWork作为web框架,在1.7, 增加了对JSFTapestry的支持。

如果希望安装struts以外的web框架,只需转到extras目录下你所期望安装的框架目录下,在相应目录下的README.txt文件会有进一步的说明,针对其他几个框架的教程列在下面。

让我们从创建一个新的Struts Action和JSP作为开始。

我在“真实世界”中实际操作的方式用蓝色斜体表示。

目录

  • [1] 为创建generate PersonForm对Person添加XDoclet Tags
  • [2] 使用XDoclet创建骨架JSPs
  • [3] 创建测试PersonAction的PersonActionTest
  • [4] 创建PersonAction
  • [5] 运行PersonActionTest
  • [6] 清理JSP来使它更好看
  • [7] 创建Canoo WebTests来测试模拟浏览器的actions

为创建generate PersonForm对Person添加XDoclet Tags [#1]

我们首先在web层为Struts创建PersonForm对象,为此,我们需要对Person.java添加标签来产生我们的Struts ActionForm,在Person.java的JavaDoc添加@struts.form标签(如果你需要一个实例,可以参考User.java):


* @struts.form include-all="true" extends="BaseForm"

我们扩展org.appfuse.webapp.form.BaseForm,因为它的方法toString()中会调用log.debug(formName)来打印易读的Form信息。
如果你没有把"org.appfuse"包换成你的"com.company"或者没有把你的模型类放到默认的包,你必须确认在@struts.form中的标签对org.appfuse.webapp.form.BaseForm使用完全的引用。

使用XDoclet创建骨架JSPs [#2]

在这一步,你将会创建来显示Person对象信息的JSP页面,它会包括Strut的JSP标签用来表现Person.java每一个属性,AppGen工具建立在一个StrutsGen工具上,用来做这件事,这个StrutsGen工具起初由Erik Hatcher编写,它只是一些XDoclet模版和一些附加类,所有的这些文件在extras/appgen目录里。

以下是生成JSP和包含标签及表单元素属性文件的简单步骤

  • 在命令行环境下,转到"extras/appgen"目录
  • 执行ant -Dobject.name=Person -Dappgen.type=pojo会在extras/appgen/build/gen产生一些文件,事实上,它会产生本教程所需的所有文件,但是我们只获取我们所需要的那些。
    • web/WEB-INF/classes/Person.properties (form元素的标签)
    • web/pages/personForm.jsp (察看单个文件的JSP文件)
    • web/pages/personList.jsp (察看People列表的JSP文件)
  • 把Person.properties的内容拷贝到web/WEB-INF/classes/ApplicationResources.properties,这是所有form中需要的标题关键字,以下是你将要添加到ApplicationResources.properties的内容的例子:

# -- person form --
personForm.id=Id
personForm.firstName=First Name
personForm.lastName=Last Name

person.added=Person has been added successfully.
person.updated=Person has been updated successfully.
person.deleted=Person has been deleted successfully.

# -- person list page --
personList.title=Person List
personList.heading=Persons

# -- person detail page --
personDetail.title=Person Detail
personDetail.heading=Person Information
  • 拷贝personForm.jsp到web/pages/personForm.jsp,拷贝personList.jsp到web/pages/personList.jsp,注意,所有新建文件的第一个字母是小写的。
"pages"目录中的文件在部署环境下会放到"WEB-INF/pages"目录下,容器会对所有WEB-INF目录下的文件提供安全保护,这种保护针对客户端的请求,而通过Struts ActionServlet的转发可以访问,把这些JSPs文件放到WEB-INF目录下确保它们只会被Actions访问,而不会被客户或者彼此直接访问,这允许安全保护转移到Action,这样可以保证保护更有效,脱离表示层这个基础。

AppFuse的web应用安全机制确认所有的*.html形式的访问是被保护的(除了/signup.html和/passwordHint.html),这保证了客户必须通过Action来访问JSP(或者至少pages中的一些)。

注意:如果你想为某一页定制CSS,你必须在文件的开头添加<body id="pageName"/>,这会被SiteMesh看到并且应用到最终的页面,然后你可以一页一页的定制你的CSS,就像如下的方式:
body#pageName element.class { background-color: blue } 
  • 在ApplicationResources.properties中添加JSP文件出现的标题和题头,在生成的JSPs里有两处标题(浏览器顶端显示)的关键字,这些字段提供了personDetail.title和personDetail.heading关键字名。

如上,我们在文件里添加"personForm.*"关键字,我们为什么使用personDetail而不是personForm作为标题呢?最好的一个原因是我们需要区分form的标签和页面上的文本,另一个原因是*Form.*的形式给你数据库里所有的字段更好的展现形式。

最近我有一个客户期望数据库里的所有字段是可查询的,这样作就会比较容易,我只需要查看ApplicationResources.properties中的保存的所有关键字来寻找"Form.",然后记录下来,在用户界面下,用户只需要输入想查找的项目和字段。我对能够在项目里区分对待Form和Detail感到高兴!

创建测试PersonAction的PersonActionTest [#3]

为了给PersonAction创建StrutsTestCase Test,首先要在test/web/**/action目录下创建PersonActionTest.java文件:


package org.appfuse.webapp.action;

import org.appfuse.Constants;
import org.appfuse.webapp.form.PersonForm;

public class PersonActionTest extends BaseStrutsTestCase {
    
    public PersonActionTest(String name) {
        super(name);
    }

    public void testEdit() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Edit");
        addRequestParameter("id""1");
        actionPerform();

        verifyForward("edit");
        assertTrue(request.getAttribute(Constants.PERSON_KEY!= null);
        verifyNoActionErrors();
    }

    public void testSave() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Edit");
        addRequestParameter("id""1");

        actionPerform();

        PersonForm personForm =
            (PersonFormrequest.getAttribute(Constants.PERSON_KEY);
        assertTrue(personForm != null);
        
        setRequestPathInfo("/savePerson");
        addRequestParameter("method""Save");

        // update the form from the edit and add it back to the request
        personForm.setLastName("Feltz");
        request.setAttribute(Constants.PERSON_KEY, personForm);

        actionPerform();

        verifyForward("edit");
        verifyNoActionErrors();
    }

    public void testRemove() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Delete");
        addRequestParameter("id""2");
        actionPerform();

        verifyForward("mainMenu");
        verifyNoActionErrors();
    }
}

你需要为src/dao/**/Constants.java添加PERSON_KEY变量,这个"personForm"与struts-config.xml中的名称相对应。


    /**
     * The request scope attribute that holds the person form.
     */
    public static final String PERSON_KEY = "personForm";

如果你尝试运行测试,你会得到几个NoSuchMethodErrors,所以,让我们定义PersonAction的edit、save和delete方法。

创建PersonAction [#4]

在src/web/**/action,创建PersonAction.java,有下面的内容:


package org.appfuse.webapp.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;

import org.appfuse.model.Person;
import org.appfuse.service.PersonManager;
import org.appfuse.webapp.form.PersonForm;

/**
 * @struts.action name="personForm" path="/editPerson" scope="request"
 *  validate="false" parameter="method" input="mainMenu"
 */
public final class PersonAction extends BaseAction {
    
    public ActionForward cancel(ActionMapping mapping, ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response)
    throws Exception {
        return mapping.findForward("mainMenu");
    }

    public ActionForward delete(ActionMapping mapping, ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'delete' method");
        }

        ActionMessages messages = new ActionMessages();
        PersonForm personForm = (PersonFormform;

        // Exceptions are caught by ActionExceptionHandler
        PersonManager mgr = (PersonManagergetBean("personManager");
        mgr.removePerson(personForm.getId());

        messages.add(ActionMessages.GLOBAL_MESSAGE,
                     new ActionMessage("person.deleted"));

        // save messages in session, so they'll survive the redirect
        saveMessages(request.getSession(), messages);

        return mapping.findForward("mainMenu");
    }

    public ActionForward edit(ActionMapping mapping, ActionForm form,
                              HttpServletRequest request,
                              HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'edit' method");
        }

        PersonForm personForm = (PersonFormform;

        // if an id is passed in, look up the user - otherwise
        // don't do anything - user is doing an add
        if (personForm.getId() != null) {
            PersonManager mgr = (PersonManagergetBean("personManager");
            Person person = mgr.getPerson(personForm.getId());
            personForm = (PersonFormconvert(person);
            updateFormBean(mapping, request, personForm);
        }

        return mapping.findForward("edit");
    }

    public ActionForward save(ActionMapping mapping, ActionForm form,
                              HttpServletRequest request,
                              HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'save' method");
        }

        // Extract attributes and parameters we will need
        ActionMessages messages = new ActionMessages();
        PersonForm personForm = (PersonFormform;
        boolean isNew = ("".equals(personForm.getId()));

        if (log.isDebugEnabled()) {
            log.debug("saving person: " + personForm);
        }

        PersonManager mgr = (PersonManagergetBean("personManager");
        Person person = (Personconvert(personForm);
        mgr.savePerson(person);

        // add success messages
        if (isNew) {
            messages.add(ActionMessages.GLOBAL_MESSAGE,
                         new ActionMessage("person.added"));

            // save messages in session to survive a redirect
            saveMessages(request.getSession(), messages);

            return mapping.findForward("mainMenu");
        else {
            messages.add(ActionMessages.GLOBAL_MESSAGE,
                         new ActionMessage("person.updated"));
            saveMessages(request, messages);

            return mapping.findForward("edit");
        }
    }
}

你会注意到以上的代码有许多对convert的调用来转化PersonForm到Person,这个转化convert方法在BaseAction.java(调用ConvertUtil.convert())中,并且使用BeanUtils.copyProperties 来实现POJOs → ActionForms和ActionForms → POJOs的转化。

如果你运行Eclipse,你需要对project运行"refresh"来显示PersonForm,它在build/web/gen目录中,这个目录必须是project的源目录之一,这是Eclipse看到和导入PersonForm的唯一方式,因为它是通过XDoclet产生并且不在通常的源目录中,你可以在build/web/gen/org/appfuse/webapp/form/PersonForm.java找到它。
BaseAction里你可以定义自己的转换器(例如 DateConverter),因此BeanUtils.copyProperties能够知道Strings → Objects的转化。如果你的POJOs存在Lists(例如 父子关系),你需要使用convertLists(java.lang.Object)方法手工完成这个转化。

现在你需要添加edit forward和savePerson action-mapping,这两个早就在PersonActionTest中提到,为此,可以在PersonAction.java添加一些额外的XDoclet标签,在类的声明前面加入这些注释,你一定已经有了editPerson action-mapping的XDoclet标签,我在这里展示你会在类中看到的所有的XDoclet标签。


/**
 * @struts.action name="personForm" path="/editPerson" scope="request"
 *  validate="false" parameter="method" input="mainMenu"
 
 * @struts.action name="personForm" path="/savePerson" scope="request"
 *  validate="true" parameter="method" input="edit"
 
 * @struts.action-forward name="edit" path="/WEB-INF/pages/personForm.jsp"
 */
public final class PersonAction extends BaseAction {

action-mappings中editPersonsavePerson主要区别是savePerson的XDoclet标签的validation标记是true(见validation="true"),注意"input"属性必须指向到一个forward,而不是个路径(例如/editPerson.html),如果你希望edit同save一样是是用用save的路径也是可以的,只要确保validate="false",然后在你的"save"方法中,你可以执行form.validate()来处理错误。 你一定注意到了这里是用PersonManager的方式与PersonManagerTest中的一样,PersonAction和PersonManagerTest都是PersonManagerImpl的客户,这很有意义。

本教程几乎所有的事情都搞定了,让我们来运行这些测试!

运行PersonActionTest [#5]

如果你查看PersonActionTest,你会发现所有的测试依赖于数据库中一条id=1的记录(testRemove依赖于id=2),所以要添加那些样本数据(metadata/sql/sample-data.xml),我会添加到底部,顺序并不重要(目前),因为它们互相并不存在依赖关系。

  <table name='person'>
    <column>id</column>
    <column>first_name</column>
    <column>last_name</column>
    <row>
      <value>1</value>
      <value>Matt</value>
      <value>Raible</value>
    </row>
    <row>
      <value>2</value>
      <value>James</value>
      <value>Davidson</value>
    </row>
  </table>

DBUnit会在执行测试前加载这些数据,所以这些记录PersonActionTest可以得到。

如果现在你执行ant test-web -Dtestcase=PersonAction - 所有事情会按计划执行,在作尝试前请确定Tomcat已经运行。

BUILD SUCCESSFUL
Total time: 1 minute 21 seconds

清理JSP来使它更好看 [#6]

让我们开始清理personForm.jsp,修改<html:form>的action为"savePerson",这保证验证起作用,同时修改focus属性,从focus=""改为focus="firstName",这样在页面打开后光标会落到firstName字段(使用JavaScript)。

现在你如果执行ant db-load deploy,启动Tomcat,然后在你的浏览器输入http://localhost:8080/appfuse/editPerson.html?id=1,你会看到如下信息:

personForm-final.png
注意:如果你修改了web中的任何文件,请使用deploy-web,否则,使用deploy来编译和部署。

最后,为了使页面更加的用户友好,你也许希望为用户在form上面添加一段信息,只需在personForm.jsp页的顶端添加(使用 <fmt:message>)。

[可选的] 创建Canoo WebTests来测试模拟浏览器的actions [#7]

本教程最后的步骤(可选的)是创建Canoo WebTest来测试JSPs。
我说是可选的,是因为你可以通过浏览器运行同样的测试。

你可以使用如下的URLs来测试不同actions的adding、editing和saving。

Canoo测试非常的灵活,只需要配置XML文件,为了测试add、edit、save和delete,打开test/web/web-tests.xml添加如下的XML,你一定注意到这些片断的target名称是PersonTests,会运行所有相关的测试。

我使用CamelCase的target命名方式(vs. 传统的小写,下划线分割的方式)是因为当拼写-Dtestcase=Name时,我会经常习惯于使用CamelCase方式来命名我的JUnit Tests。


<!-- runs person-related tests -->
<target name="PersonTests"
    depends="EditPerson,SavePerson,AddPerson,DeletePerson"
    description="Call and executes all person test cases (targets)">
    <echo>Successfully ran all Person JSP tests!</echo>
</target>

<!-- Verify the edit person screen displays without errors -->
<target name="EditPerson"
    description="Tests editing an existing Person's information">
    <webtest name="editPerson">
        &config;
        <steps>
            &login;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
        </steps>
    </webtest>
</target>

<!-- Edit a person and then save -->
<target name="SavePerson"
    description="Tests editing and saving a user">
    <webtest name="savePerson">
        &config;
        <steps>
            &login;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
            <setinputfield description="set lastName" name="lastName" value="Canoo"/>
            <clickbutton label="Save" description="Click Save"/>
            <verifytitle description="Page re-appears if save successful"
                text=".*${personDetail.title}.*" regex="true"/>
            <verifytext description="verify success message" text="${person.updated}"/>
        </steps>
    </webtest>
</target>

<!-- Add a new Person -->
<target name="AddPerson"
    description="Adds a new Person">
    <webtest name="addPerson">
        &config;
        <steps>
            &login;
            <invoke description="click Add Button" url="/editPerson.html"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
            <setinputfield description="set firstName" name="firstName" value="Abbie"/>
            <setinputfield description="set lastName" name="lastName" value="Raible"/>
            <clickbutton label="${button.save}" description="Click button 'Save'"/>
            <verifytitle description="Main Menu appears if save successful"
                text=".*${mainMenu.title}.*" regex="true"/>
            <verifytext description="verify success message" text="${person.added}"/>
        </steps>
    </webtest>
</target>

<!-- Delete existing person -->
<target name="DeletePerson"
    description="Deletes existing Person">
    <webtest name="deletePerson">
        &config;
        <steps>
            &login;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <clickbutton label="${button.delete}" description="Click button 'Delete'"/>
            <verifytitle description="display Main Menu" text=".*${mainMenu.title}.*" regex="true"/>
            <verifytext description="verify success message" text="${person.deleted}"/>
        </steps>
    </webtest>
</target>

完成添加后,你一定可以在Tomcat运行时运行ant test-canoo -Dtestcase=PersonTests,或者使用ant test-jsp -Dtestcase=PersonTests让Ant为你启动/关闭Tomcat,为了在所有的Canoo测试中包括PersonTests,把它作为"run-all-tests"的依赖target。

你必须注意到Canoo没有客户端的日志,如果你期望看到一些信息,你可以在每一个target的</canoo>和</target>之间添加如下代码。

<loadfile property="web-tests.result" 
    srcFile="${test.dir}/data/web-tests-result.xml"/>
<echo>${web-tests.result}</echo>
BUILD SUCCESSFUL
Total time: 11 seconds


Next Up: Part IV: 增加校验功能和列表页面 - 给personForm的firstName和lastName字段添加必添的验证逻辑和显示person所有记录的页面。


Attachments:


Go to top   Edit this page   More info...   Attach file...
This page last changed on 06-Nov-2006 13:52:59 MST by DuanYongming.