使用 Struts 2
关于本指南
本指南将向你展示如何利用Struts 2 创建控制界面和细节界面。列表(控制)界面有能力按列进行排序,一次可以处理25条记录。表单(细节)界面使用优雅的CSS 表单布局(蒙Wufoo授权),你也可以通过修改客户端和服务端的认证机制来增强用户体验。
 |
本指南假设你已经利用appfuse-basic-struts原型创建了一个工程并且已经阅读了Persistence and Services 指南。如果你正在使用appfuse-modular-struts 原型,请将你的思路转换到使用web module作为根目录的方法上来。如果你使用了除Struts以外的其他web框架来创建你的工程,你可能会对本指南感到困惑,这些内容对你也就没有任何用处了。 |
- Struts 2 简介
- 创建 PersonActionTest
- 创建可以获得people的PersonAction
- 创建用来显示结果的personList.jsp
- 修改PersonActionTest和PersonAction 为了edit(), save() and delete() 方法
- 创建personForm.jsp来修改person
- 修改Validation
- 创建Canoo WebTest来测试类似游览器的action
- 向菜单中添加链接
Struts 2简介
Struts 2(以前被叫做WebWork)是一个简单易行的web框架。它在一个通用的命令行框架XWork基础上构建。XWork也具有一个IoC容器,但不如Spring的特征那样完整,当然本节的内容并不包含这个。Struts 2的控制器被叫做Action,主要原因是它们必须实现Action接口。ActionSupport类就实现了这个接口,它是Struts 2中的大多数的action的父类。下图说明了Struts 2如何与一个web应用程序构架相结合。

着图片搞不定了啊!
典型的Struts 2 action包含访问模型属性的方法和返回字符串的方法。这些字符串将和struts.xml配置文件中的result names进行匹配。典型的action只包含一个execute() 方法,但要利用URL和按钮的名字来添加多个方法和control execution也是很容易的。
Struts 2使用拦截器来拦截request和response过程。这种行为和Servlet Filter极为相似,除非你可以和action直接对话。Struts 2在框架本身中使用拦截器。其中一部分对Action进行初始化,为它的population做准备,为其中的各个变量赋值并处理任何转换过程中的错误。
创建PersonActionTest
测试是任何一个应用中的重要组成部分,测试一个Struts应用是再简单不过的事情了。XWork所提供的通用命令行模式根本不依赖Servlet API。这就使得使用JUnit来测试Action变得极为容易。
在src/test/java/**/webapp/action中创建一个类PersonActionTest.java
package org.appfuse.tutorial.webapp.action;
import com.opensymphony.xwork2.ActionSupport;
import org.appfuse.service.GenericManager;
import org.appfuse.tutorial.model.Person;
import org.appfuse.webapp.action.BaseActionTestCase;
public class PersonActionTest extends BaseActionTestCase {
private PersonAction action;
@Override
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
action = new PersonAction();
GenericManager personManager = (GenericManager) applicationContext.getBean("personManager");
action.setPersonManager(personManager);
Person person = new Person();
person.setFirstName("Jack");
person.setLastName("Raible");
personManager.save(person);
}
public void testSearch() throws Exception {
assertEquals(action.list(), ActionSupport.SUCCESS);
assertTrue(action.getPersons().size() >= 1);
}
}
这个类现在还不能编译,因为类PersonAction需要先创建出来。
创建可以获得people的PersonAction
在src/main/java/**/webapp/action中创建继承AppFuse的BaseAction的类PersonAction.java
package org.appfuse.tutorial.webapp.action;
import org.appfuse.webapp.action.BaseAction;
import org.appfuse.tutorial.model.Person;
import org.appfuse.service.GenericManager;
import java.util.List;
public class PersonAction extends BaseAction {
private GenericManager<Person, Long> personManager;
private List persons;
public void setPersonManager(GenericManager<Person, Long> personManager) {
this.personManager = personManager;
}
public List getPersons() {
return persons;
}
public String list() {
persons = personManager.getAll();
return SUCCESS;
}
}
典型的Struts 2 action既是控制器又是模型。在本例中,list()方法担当控制器的角色,getPersons()方法为模型处理数据。这种对MVC的简化处理使得这个web框架在编程过程中非常容易使用。
用你的IDE运行PersonActionTest或者运行命令mvn test -Dtest=PersonActionTest。
 | 零配置
如果你不习惯使用XML来配置你的Action,你可以考虑使用Struts的Zero Configuration特性。对于AppFuse 2.0 M4而言,zero configuration是默认启用的。 |
创建用来显示结果的personList.jsp
创建src/main/webapp/WEB-INF/pages/personList.jsp页面来显示people列表。
<%@ include file="/common/taglibs.jsp"%>
<head>
<title><fmt:message key="personList.title"/></title>
<meta name="heading" content="<fmt:message key='personList.heading'/>"/>
</head>
<c:set var="buttons">
<input type="button" style="margin-right: 5px"
onclick="location.href='<c:url value="/editPerson.html"/>'"
value="<fmt:message key="button.add"/>"/>
<input type="button" onclick="location.href='<c:url value="/mainMenu.html"/>'"
value="<fmt:message key="button.done"/>"/>
</c:set>
<c:out value="${buttons}" escapeXml="false" />
<s:set name="persons" value="persons" scope="request"/>
<display:table name="persons" class="table" requestURI="" id="personList" export="true" pagesize="25">
<display:column property="id" sortable="true" href="editPerson.html"
paramId="id" paramProperty="id" titleKey="person.id"/>
<display:column property="firstName" sortable="true" titleKey="person.firstName"/>
<display:column property="lastName" sortable="true" titleKey="person.lastName"/>
<display:setProperty name="paging.banner.item_name" value="person"/>
<display:setProperty name="paging.banner.items_name" value="people"/>
<display:setProperty name="export.excel.filename" value="Person List.xls"/>
<display:setProperty name="export.csv.filename" value="Person List.csv"/>
<display:setProperty name="export.pdf.filename" value="Person List.pdf"/>
</display:table>
<c:out value="${buttons}" escapeXml="false" />
<script type="text/javascript">
highlightTableRows("personList");
</script>
这个文件中最重要的一行是<display:table>标签上方的一行。也就是<s:set>标签。这个标签调用PersonAction.getPersons()并将resulting List中的值传送到request范围中,也就是<display:table>标签可以抓取的范围。这是必须的,因为用于显示的标签对于Struts 2中的数值栈ValueStack一无所知。
打开src/main/resources目录中的struts.xml文件。在文件末尾部分定义一个<action>并设定它的class属性和personAction中的bean相匹配。
<action name="persons" class="org.appfuse.tutorial.webapp.action.PersonAction" method="list">
<result>/WEB-INF/pages/personList.jsp</result>
</action>
默认的结果类型为dispatcher,其名字为success。这种设定的结果类型只是当有success从PersonAction.list()中返回时,简单地跳转到personList.jsp文件。其他的结果类型包括redirect类型和chain类型。Redirect完成一个客户端的重定向,而chain则跳转到另一个action。若要了解所有的结果类型,请参看Struts 2的结果类型文档。
这个actioin的mothod属性有一个list属性。当URL "persons.html"被调用时list属性将调用list()方法。如果你不包含method属性,它将调用execute()方法。
打开src/main/resources/ApplicationResources.properties并对各个person属性添加i18n keys/values
# -- person form --
person.id=Id
person.firstName=First Name
person.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
在你的浏览器中运行mvn jetty:run-war 并访问http://localhost:8080/persons.html
。用admin/admin登录,然后你将看到类似下图的界面。
又一张图
AppFuse的安全设定指定了所有*.html样式的url模式应当被保护(除了/signup.html 和 /passwordHint.html之外)。这就确保了客户端必须通过一个Action来获得一个JSP(或者至少是在WEB-INF/pages中的一个)。
 | CSS定制
如果你想要对某个特殊的页面定制CSS,你可以在文件的开头添加<body id="pageName"/>。它将被SiteMesh所处理,并放入最中生成的文件中。你可以用类似以下这种方式一页一页来定制你的CSS:
body#pageName element.class { background-color: blue } |
修改PersonActionTest和PersonAction 的edit(), save() and delete() 方法
若要创建detail 界面,需对类PersonAction添加edit(), save(), and delete()方法。在你这样做之前,要记得对这些方法创建测试用例。
打开src/test/java/**/webapp/action/PersonActionTest.java并对修改,保存,删除操作进行测试。
public void testEdit() throws Exception {
log.debug("testing edit...");
action.setId(1L);
assertNull(action.getPerson());
assertEquals("success", action.edit());
assertNotNull(action.getPerson());
assertFalse(action.hasActionErrors());
}
public void testSave() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
ServletActionContext.setRequest(request);
action.setId(1L);
assertEquals("success", action.edit());
assertNotNull(action.getPerson());
action.getPerson().setLastName("Updated Last Name");
assertEquals("input", action.save());
assertEquals("Updated Last Name", action.getPerson().getLastName());
assertFalse(action.hasActionErrors());
assertFalse(action.hasFieldErrors());
assertNotNull(request.getSession().getAttribute("messages"));
}
public void testRemove() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
ServletActionContext.setRequest(request);
action.setDelete("");
Person person = new Person();
person.setId(2L);
action.setPerson(person);
assertEquals("success", action.delete());
assertNotNull(request.getSession().getAttribute("messages"));
}
这个类还不能够被编译直到你需要更新你的src/main/java/**/action/PersonAction.java类。cancel和delete方法将捕获cancel和delete按钮的点击事件。execute()方法将form中的各个action分发到相应的方法中。
private Person person;
private Long id;
public void setId(Long id) {
this.id = id;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public String delete() {
personManager.remove(person.getId());
saveMessage(getText("person.deleted"));
return SUCCESS;
}
public String edit() {
if (id != null) {
person = personManager.get(id);
} else {
person = new Person();
}
return SUCCESS;
}
public String save() throws Exception {
if (cancel != null) {
return "cancel";
}
if (delete != null) {
return delete();
}
boolean isNew = (person.getId() == null);
person = personManager.save(person);
String key = (isNew) ? "person.added" : "person.updated";
saveMessage(getText(key));
if (!isNew) {
return INPUT;
} else {
return SUCCESS;
}
}
仔细研究你的PersonActionTest,你会发现所有的test依赖于在数据库中拥有一个id=1的记录 (但对于testRemove,id=2),因此我们应当在样例sample数据文件中添加这些记录(src/test/resources/sample-data.xml)。从文件结尾处添加即可----记录的顺序并不重要因为当前它和其他表之间并不存在关系。如果你已经有这个表,要保证第二条记录存在,否则testRemove()将不能正常工作。
<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>Bob</value>
<value>Johnson</value>
</row>
</table>
在任何test运行之前,DbUnit会将这个文件载入,这样你的Action test就可以访问这些记录。
保存所有的文件,然后利用命令 mvn test -Dtest=PersonActionTest 运行PersonActionTest中的test
BUILD SUCCESSFUL
Total time: 31 seconds
创建personForm.jsp来修改个人信息
创建页面src/main/webapp/WEB-INF/pages/personForm.jsp来显示表单:
<%@ include file="/common/taglibs.jsp"%>
<head>
<title><fmt:message key="personDetail.title"/></title>
<meta name="heading" content="<fmt:message key='personDetail.heading'/>"/>
</head>
<s:form id="personForm" action="savePerson" method="post" validate="true">
<s:hidden name="person.id" value="%{person.id}"/>
<s:textfield key="person.firstName" required="true" cssClass="text medium"/>
<s:textfield key="person.lastName" required="true" cssClass="text medium"/>
<li class="buttonBar bottom">
<s:submit cssClass="button" method="save" key="button.save" theme="simple"/>
<c:if test="${not empty person.id}">
<s:submit cssClass="button" method="delete" key="button.delete" onclick="return confirmDelete('person')" theme="simple"/>
</c:if>
<s:submit cssClass="button" method="cancel" key="button.cancel" theme="simple"/>
</li>
</s:form>
<script type="text/javascript">
Form.focusFirstElement($("personForm"));
</script>
Struts减少了你原先不得不在form中写入的HTML代码的总量。<s:form>标签替你创建了 <form> 以及 结构标签。<s:textfield>标签创建整行,包括用来容纳input field 的<ul> 和 <li> 标签。
然后,更新文件src/main/resources/struts.xml,包含"editPerson" 和 "savePerson"这两个action。
<action name="editPerson" class="org.appfuse.tutorial.webapp.action.PersonAction" method="edit">
<result>/WEB-INF/pages/personForm.jsp</result>
<result name="error">/WEB-INF/pages/personList.jsp</result>
</action>
<action name="savePerson" class="org.appfuse.tutorial.webapp.action.PersonAction" method="save">
<result name="input">/WEB-INF/pages/personForm.jsp</result>
<result name="cancel" type="redirect">persons.html</result>
<result name="delete" type="redirect">persons.html</result>
<result name="success" type="redirect">persons.html</result>
</action>
运行mvn jetty:run-war,打开你的浏览器,访问http://localhost:8080/persons.html
,并点击Add按钮。
图片阿图片。。。
填写first name和last name 域并点击Save按钮。这将跳跳转道list界面,界面中会显示一条成功信息,新录入的人员信息则会显示在list中。
 | 显示成功信息
|