2006年11月11日星期六

Hibernate Jpetstore 之四 表示层技术

文档内容

  • 概览
  • Struts 表示层组件 FormBean
    • FormBean 配置
    • FormBean 类层次
    • BaseActionForm 子类实例 AccountActionForm
  • 避免重复提交
    • Struts 的事务 Token
  • 我们还缺什么?
    • 客户端校验
    • 漂亮的页面
  • 总结



在阅读本篇文章之前,请先仔细阅读前面系列的相关内容。

概览


在各种框架欣欣向荣的今天,你能想象最初 Java WEB 开发者的日子吗?要知道,就算是JSP,当时都被寄予厚望,因为当时,开发者不得不在Servlet 中书写之如:out.println("<html><head><title>My God</title></head><body>");

你能想象,以这种方式做一个象样的页面是怎样的一种情形。这种情况下,是将“表示层”的内容(HTML标记)渗透进Java代码中了,你哪怕是修改页面上的一个文字,你都不得不在上述的 println 中修改 -> 编译 -> 测试-> ...

于是,JSP应运而生,可是很快,开发者发现,情况反过来了:在JSP页面代码中到处散布有之如:


<%

String amount =
request.getParameter("amount");

if
( amount != null &&
amount.length() > 0 ) {


...
%>


也就是说,此时表示逻辑的代码渗透进页面代码中了。

于是才有后来的 JavaBeans, <jsp:useBean>, Taglibs 等,以及术语 WEB MVC, MVC2 等。

毫无疑问,对于在JAVA Web 领域工作多年的老手,看到我这篇关于 Struts 的文章肯定会觉得好土,或者甚至老掉牙了! 的确,这也是我这段时间一直在考虑是否需要写这样一个系列的原因。

不管现在 JSF. WebWorks/Struts2, SpringMVC , JBoss Seam 被如何鼓吹,Struts 作为 Web
框架的先行者,还是有它的位置。尽管此例子中所采用的方法比起最新的 Struts (Struts 1.3.x 系列)也同样显得有些陈旧,但是正如 JAVA 领域中的一惯作法,“在引入新功能前先考虑向后兼容”,因此,新的功能尽管加入吧,你可以欣喜若狂,但我也同样可以一直运行已经稳定运行好几年的产品。

随便提一下,本人并不认为上述新的WEB框架使开发工作简化了多少,相反,倒是增加了不少复杂性。作为新手,很难保证在研究这些框架一周后能开发出一个稳
定可靠的方案。相反象几个简单的框架反而在引入面向 Page 的设计方法的同时,简化了开发的难度:Wicket Click ,而且更加符合当今的 Web2 的需求。

再有,由于 JSTL 的流行,几乎所有的 Web 框架都依靠它来排除JSP脚本。但是我们不会在这里介绍每个 JSTL 标记的用法,具体的用法见工程的JSP 源代码。

好了,一来就说了这么多,无非是为了引入主角 Struts,但是请原谅,关于整个Struts
的介绍是需要一整书才能介绍完的。所以我们还是以代码为依托,一步步来吧。
我们的主题是表示层的相关技术。

Struts 表示层组件 FormBean

FormBean 配置


FormBean 即是我们熟悉的 JSP + JavaBean 设置方式中的 JavaBean,只不过它作为 Struts 框架的组件担任起页面表单与 Struts Action的信息传递的使者。

为了弄清 FormBean 的工作原理,我们现在给出我们整个的 struts-config.xml 文件的内容

<?xml version="1.0"
encoding="GBK"?>

<!DOCTYPE struts-config PUBLIC
"-//Apache Software
Foundation//DTD Struts Configuration 1.1//EN"

"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">

<struts-config>
<!-- 配置此应用中的所有 FormBean
-->

<form-beans>

<!-- 这种复用
FormBean 的方式值得讨论,见紧随其后的说明 1 -->


<!--
与注册帐户和帐户信息相关的页面使用的 FormBean -->


<form-bean name="accountForm"
type="org.springframework.samples.jpetstore.web.struts.AccountActionForm"/>

<!--
与购物车相关的页面使用的 FormBean -->


<form-bean name="cartForm"
type="org.springframework.samples.jpetstore.web.struts.CartActionForm"/>

<!-- 没有对应的页面元素的
FormBean, 例如点击一个链接或按下搜索按钮等等功能,被设计成不需要 FormBean 来收集用户输入 -->

<form-bean
name="emptyForm"
type="org.springframework.samples.jpetstore.web.struts.BaseActionForm"/>

<!-- 与帐户修改相关的页面使用的
FormBean,因为此时也许在当前的 session 已经存在了一个 accountForm -->

<form-bean
name="workingAccountForm"
type="org.springframework.samples.jpetstore.web.struts.AccountActionForm"/>

<!-- 与所有定单操作相关的页面使用的 FormBean -->
<form-bean
name="workingOrderForm"
type="org.springframework.samples.jpetstore.web.struts.OrderActionForm"/>

</form-beans>


<!-- 全局跳转声明,这此跳转可以被所有的
Action 中共享 -->


<global-forwards>

<forward
name="failure" path="/WEB-INF/jsp/struts/Error.jsp"
redirect="false"/>

<forward
name="unknown-error" path="/WEB-INF/jsp/struts/Error.jsp"/>

<forward
name="global-signon" path="/WEB-INF/jsp/struts/SignonForm.jsp"/>

</global-forwards>

<!-- 以下为所有的 Action 映射
-->


<action-mappings>

<!--
点击链接将一只宠物加入购物车 -->


<action path="/shop/addItemToCart"
type="org.springframework.samples.jpetstore.web.struts.AddItemToCartAction"


name="cartForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/Cart.jsp"/>

</action>

<!-- 结算购物车
-->


<action path="/shop/checkout"
type="org.springframework.samples.jpetstore.web.struts.ViewCartAction"


name="cartForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/Checkout.jsp"/>
</action>

<!--
修改帐号信息 -->


<action path="/shop/editAccount"
type="org.springframework.samples.jpetstore.web.struts.EditAccountAction"

name="workingAccountForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/EditAccountForm.jsp">
<forward name="success" path="/shop/index.do"/>
</action>
<action
path="/shop/editAccountForm"
type="org.springframework.samples.jpetstore.web.struts.EditAccountFormAction"
name="workingAccountForm" scope="session"
validate="false">

<forward name="success"
path="/WEB-INF/jsp/struts/EditAccountForm.jsp"/>

</action>
<action
path="/shop/index"
type="org.springframework.samples.jpetstore.web.struts.DoNothingAction"


validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/index.jsp"/>

</action>
<action
path="/shop/help"
type="org.springframework.samples.jpetstore.web.struts.DoNothingAction"


validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/help.jsp"/>

</action>
<action
path="/shop/listOrders"
type="org.springframework.samples.jpetstore.web.struts.ListOrdersAction"


name="accountForm" scope="session" validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/ListOrders.jsp"/>

</action>
<action
path="/shop/newAccount"
type="org.springframework.samples.jpetstore.web.struts.NewAccountAction"


name="workingAccountForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/NewAccountForm.jsp">


<forward name="success" path="/shop/index.do"/>

</action>
<action
path="/shop/newAccountForm"
type="org.springframework.samples.jpetstore.web.struts.NewAccountFormAction"


name="workingAccountForm" scope="session" validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/NewAccountForm.jsp"/>

</action>

<!--
进入结算中心页面后,点击继续进入此 -->



<action path="/shop/newOrderForm"
type="org.springframework.samples.jpetstore.web.struts.NewOrderFormAction"


name="workingOrderForm" scope="session" validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/NewOrderForm.jsp"/>

</action>

<!--

fixed by pprun: 将原先混在一起的逻辑打破成几个小部分,否则在多步向导式提交


页面中的任何一步出错都无理地返回到 NewOrderForm.jsp 页面,而不是真正的出错的页面


-->

<!--
<action
path="/shop/newOrder"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/NewOrderForm.jsp">


<forward name="confirm"
path="/WEB-INF/jsp/struts/ConfirmOrder.jsp"/>


<forward name="shipping"
path="/WEB-INF/jsp/struts/ShippingForm.jsp"/>


<forward name="success" path="/WEB-INF/jsp/struts/ViewOrder.jsp"/>

</action>
-->

<!--
填写定单信息的多页向导式页面 -->


<!--
当第一页校验失败时,需要跳回填写购物单的第一页 -->




<action path="/shop/newOrderStep1"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/shop/newOrderForm.do">


<forward name="confirm"
path="/WEB-INF/jsp/struts/ConfirmOrder.jsp"/>


<forward name="shipping"
path="/WEB-INF/jsp/struts/ShippingForm.jsp"/>

</action>

<!--
(只有页面上填写了将宠物送到不同的地址时,默认为送到当前用户的地址),
才会出现此面。此页校验失败,毫无疑问应该回到这个新地址填写页,
而不是整个流程的第一页。这就是原版中的BUG所在处,因为它将这个向导性的流程
处理放到了一个映射中,所以没法处理这种情况 -->

<action
path="/shop/newOrderStep2"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/ShippingForm.jsp">


<forward name="confirm"
path="/WEB-INF/jsp/struts/ConfirmOrder.jsp"/>

</action>

<!--
当在最后一步确认时出错,需要跳回填写购物单的第一页 -->

<action
path="/shop/newOrderStep3"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/NewOrderForm.jsp">


<forward name="success" path="/WEB-INF/jsp/struts/ViewOrder.jsp"/>

</action>
<!-- fixed
end -->



<action
path="/shop/removeItemFromCart"
type="org.springframework.samples.jpetstore.web.struts.RemoveItemFromCartAction"


name="cartForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/Cart.jsp"/>

</action>
<action
path="/shop/searchProducts"
type="org.springframework.samples.jpetstore.web.struts.SearchProductsAction"


name="emptyForm" scope="session" validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/SearchProducts.jsp"/>

</action>
<action
path="/shop/signon"
type="org.springframework.samples.jpetstore.web.struts.SignonAction"


name="accountForm" scope="session" validate="false">


<forward name="success" path="/shop/index.do"/>

</action>
<action
path="/shop/signonForm"
type="org.springframework.samples.jpetstore.web.struts.DoNothingAction"


validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/SignonForm.jsp"/>

</action>
<action
path="/shop/updateCartQuantities"
type="org.springframework.samples.jpetstore.web.struts.UpdateCartQuantitiesAction"


name="cartForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/Cart.jsp"/>

</action>
<action
path="/shop/viewCart"
type="org.springframework.samples.jpetstore.web.struts.ViewCartAction"


name="cartForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/Cart.jsp"/>

</action>
<action
path="/shop/viewCategory"
type="org.springframework.samples.jpetstore.web.struts.ViewCategoryAction"


name="emptyForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/index.jsp">


<forward name="success" path="/WEB-INF/jsp/struts/Category.jsp"/>

</action>
<action
path="/shop/viewItem"
type="org.springframework.samples.jpetstore.web.struts.ViewItemAction"


name="emptyForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/Product.jsp">


<forward name="success" path="/WEB-INF/jsp/struts/Item.jsp"/>

</action>
<action
path="/shop/viewOrder"
type="org.springframework.samples.jpetstore.web.struts.ViewOrderAction"


name="accountForm" scope="session" validate="false">


<forward name="success" path="/WEB-INF/jsp/struts/ViewOrder.jsp"/>

</action>
<action
path="/shop/viewProduct"
type="org.springframework.samples.jpetstore.web.struts.ViewProductAction"


name="emptyForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/index.jsp">


<forward name="success" path="/WEB-INF/jsp/struts/Product.jsp"/>

</action>
</action-mappings>

</struts-config>



说明:

1. 从FormBean 的数量和每个使用了FormBean的 Action 映射可知,这里存在 FormBean
复用问题,即用一个来服务多个 Action, 这种方式可以大大减少 FormBean 的数量,
但是,在 Action中的逻辑变得复杂了。因为在 FormBean 中包括了所有 Action 的需求,
而在Action中不得不排除它所不需要的元素的干扰。这样使代码看起来很混乱。

2. 有了全局声明,在 Action 的代码中就可以随时发出 mapping.findForward("failure");
之类的代码,而不需要在对应的 Action 映射中配置该 <forward> 子元素.


3. 注释掉的 Action 映射是为了修复一个BUG:

因为在涉及到定单提交时,采用的是多页提交(也就向导页面)方式,即收集的信息是从连续多个
页面中获得的,而不是普通的从一个页面中得到的。这样就涉及到,当其中的一个页面出现校验
失败时,将要将流控跳转到出错的页面,通过将原先混在一起的逻辑打破成几个小部分,
否则在多步向导式提交页面中的任何一步出错都无理地返回到 NewOrderForm.jsp 页面,
而不是真正的出错的页面,请看相应的映射元素的注释说明。


4. 为了保持连贯性,关于每个映射元素的每个属性,我们重复 Hibernate
JPetstore 系列之三: 控制层技术
中的 ActionForm <-- struts-config.xml -->Action 部分的内容:

1. path=/shop/editAccount

所有要去住 /shop/editAccount (严格地讲,如果按照Struts 的方言是 /shop/editAccount.do)
的请求,都要遵循这里的配置

2. name="workingAccountForm"

都将绑定 workingAccountForm ,注意 form-bean 在 Action 中引用是通过 name
属性来引用的,它是在一开始定义的: <form-bean name="workingAccountForm"
type="org.springframework.samples.jpetstore.web.struts.AccountActionForm"/>

3. scope="session" 该 bean

将在整个会话其间始终有效 ,但注意这个配置是多余的,默认就是 session 上下文的,反而如果 bean 只在
request上下文时,才需要明确地声明。


4. validate="true" 将对表单的输入调用 form-bean 的 validate 方法。但我们发现在
AccountActionForm.java 中,只有 doValidate(...) 方法,并没有 validate()方法,
但细心的话,应该发现了,和所有的Action 都是从BaseAction 中派生而来一样,
所有的ActionForm 中都是从一个基类 BaseActionForm.java 中派生下来。

其中定义了所有 formbean都需要的东西,对于校验错误的处理。

其中就是 validate(...) 方法,并在其中调用了 doValidate() 方法,

而每个BaseActionForm 的子类只要override 这个doValidate() 方法,

如果 validate="true"声明了的话,那么子类中的 doValidate() 方法将会被调用。这是多态性的表现。


5. input="..." 一般来讲,一个表单在校验失败后都需要回去重纠正错误的输出项,
所以我们通过这个值来告诉 Struts该回到哪去纠错.

FormBean 类层次


与 Action 类一样,FormBean 也基于类的继承关系设计的,这样子类 FormBean
只需实现父类 FormBean指定的约束,所有的子类都复用父类中的功能并按照这种设计约束工作。

基类 BaseActionForm

该类本身又是从 抽象类 org.apache.struts.action.ActionForm 派生而来,
所以应用中的所有 FormBean只要从 BaseActionForm 派生即可:


public class BaseActionForm
extends ActionForm {

/**
*此乃最原始的错误处理方法,将所有错误信息加入到一个列表后,然后存入到
*Servlet 请求属性中供页面使用.
*更现代的方法是使用 Struts1.1 之后的 commons-validator,关于这个功能在
* 各种关于Struts 的参考或书籍中都有介绍。
*
*是否为调用此方法是通过属性 validate 来控制的,如:
*<action path="/shop/signon"type="org.springframework.samples.jpetstore.web.struts.SignonAction"
*name="accountForm" scope="session" validate="false">
*是不会调用的,因为 validate="false".
   
* 这是“模板方法”(Template method) 设计模式中的“方法”
*/

public ActionErrors validate(ActionMapping mapping,
HttpServletRequest request) {
ActionErrors errorMessages =
null;



// 整个系统的错误信息列表,通过调用doValidate(mapping, request, errorList);

// addErrorIfStringEmpty 会将错误信息加入到列表当中,并且它被存入了请求属性当中.


ArrayList errorList = new
ArrayList();

doValidate(mapping, request,
errorList);


request.setAttribute("errors", errorList);

if (!errorList.isEmpty()) {


errorMessages = new org.apache.struts.action.ActionErrors();


errorMessages.add(ActionErrors.GLOBAL_MESSAGE, new
ActionMessage("global.error"));

}

return errorMessages;

}



/**
*此方法被设计为供子类覆盖的(overriding).
*任何子类实现了这个方法,将自动被上面的 validate 方法调用。
*
* 这是“模板方法"模式中的默认钓子方法,由子类的实现方法来替换。
*
* @param mapping
* @param request
* @param
errors

*/

public void doValidate(ActionMapping mapping,
HttpServletRequest request, List errors) {

}


/**
*此辅助方法被用来给定的页面输入内容是否为空,如果是空的话,将显示给定的出错信息。
*
* @param errors 错误信息列表
* @param message 当 value 为空时,将显示这个错误信息
* @param value 页面元素对应的值
*/

protected void addErrorIfStringEmpty(List errors,
String message, String value) {

if (value == null ||
value.trim().length() < 1) {

errors.add(message);

}

}

}



BaseActionForm 子类实例 AccountActionForm


我仅介绍一个子类 AccountActionForm :


public class AccountActionForm
extends BaseActionForm {


/** 用于检验的常量定义,因为在新建帐户与修改帐户时检验逻辑是不一样的。
* 至少在修改帐户时,帐户名是已经存在了 */

public static final String VALIDATE_EDIT_ACCOUNT = "editAccount";
public static final String VALIDATE_NEW_ACCOUNT = "newAccount";

/** 用于存贮用户的首先语言的列表 */
private static final ArrayList LANGUAGE_LIST = new ArrayList();

/* Private Fields */

// 看起来好象与 Account 中的成员重复了,这是因为此 Form 被多个页面重复使用的

// 结果,因为在登录页面时,那时根本不存在 Account, 所以不可能通过

// account.getUsername() 和 account.getPassword() 来得到用户的输入值的,



// 下面两项即是在登录当时收集输入 信息,

// 其它情况(比如修改,新建帐户时)都是间接使用了 Account 中的成员,因当时都

// 已经在 session 中存放了一个 Account 的实例

// 所以重用是有代价的(使代码不那么直观了,如果是一个页面表单 Form 对应一个

// FormBean 的话,以下成员与页面中的输入元素是一一对应的)



// 供登录页面使用的 元素

private String username;
private String password;

// 登录后,与帐户相关的元素
private String repeatedPassword;
private List languages;
private List categories;

/**

* 这个成员的值是通过页面隐藏元素传入的:

* NewAccountForm.jsp 中: <html:hidden name="workingAccountForm" property="validate"
value="newAccount"/>

* EditAccountForm.jsp 中:<html:hidden name="workingAccountForm" property="validate" value="editAccount" />
*/
private String validate;

/**
* 用来记住用户是从哪里跳转过来的,因为准备对购物车进行结算时,如果没有登录

* 的话,首先将结算中心页面的地址存入此成员中,登录成功后再跳转过去。

* 如果没有这样一步操作的话,那么就会出现讨厌的将你送回首页面(也就是程序

* 的逻辑流程打扰了用户的进程,这是最应当避免的。)

*/

private String forwardAction;

/**

* 所有的帐号信息放在这个 POJO 中

*/

private Account account;

/**
* 用于显示标语,当你在用户信息页面选择显示标语时

*/

private String bannerName;

/**
* 用于显示根据用户的喜好被推荐的宠物列表,当你选择了显示该列表时。

*/

private PagedListHolder myList;

/**
* 用户最喜欢的宠物类别

*/

private String favCategoryName;

/* Static Initializer */

static {

LANGUAGE_LIST.add("english");


LANGUAGE_LIST.add("japanese");

}

public AccountActionForm() {

languages = LANGUAGE_LIST;

}

public PagedListHolder getMyList() {

return myList;

}
public void setMyList(PagedListHolder myList) {

this.myList = myList;

}

public String getForwardAction() {

return forwardAction;

}
public void setForwardAction(String forwardAction) {

this.forwardAction = forwardAction;

}

public String getUsername() {

return username;

}
public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getRepeatedPassword() {

return repeatedPassword;

}

public void setRepeatedPassword(String repeatedPassword) {

this.repeatedPassword = repeatedPassword;

}

public Account getAccount() {

return account;

}

public void setAccount(Account account) {

this.account = account;

}

public List getLanguages() {

return languages;

}
public void setLanguages(List languages) {

this.languages = languages;

}

public List getCategories() {

return categories;

}
public void setCategories(List categories) {

this.categories = categories;

}

public String getBannerName() {

return bannerName;

}

public void setBannerName(String bannerName) {

this.bannerName = bannerName;

}

public String getFavCategoryName() {

return favCategoryName;

}

public void setFavCategoryName(String favCategoryName) {

this.favCategoryName = favCategoryName;

}

public String getValidate() {

return validate;

}

public void setValidate(String validate) {

this.validate = validate;

}

/**
* 覆盖父类中的方法”默认钓子“方法,用于特定于此子类的输入校验

*/

public void doValidate(ActionMapping mapping,

HttpServletRequest request, List errors) {


if (validate != null) {


if (VALIDATE_EDIT_ACCOUNT.equals(validate) ||


VALIDATE_NEW_ACCOUNT.equals(validate)) {


if (VALIDATE_NEW_ACCOUNT.equals(validate)) {


// 是新建帐户时,需要额外的校验


account.setStatus("OK");


addErrorIfStringEmpty(errors, "User ID is required.",


account.getUsername());




if (account.getPassword() == null ||


account.getPassword().length() < 1 ||


!account.getPassword().equals(repeatedPassword)) {


errors.add("Passwords did not match or were not provided. " +


"Matching passwords are required.");


}


}




if (account.getPassword() != null &&


account.getPassword().length() > 0) {


if (!account.getPassword().equals(repeatedPassword)) {


errors.add("Passwords did not match.");


}


}




addErrorIfStringEmpty(errors, "First name is required.",


this.account.getFirstname());


addErrorIfStringEmpty(errors, "Last name is required.",


this.account.getLastname());


addErrorIfStringEmpty(errors, "Email address is required.",


this.account.getEmail());


addErrorIfStringEmpty(errors, "Phone number is required.",


this.account.getPhone());


addErrorIfStringEmpty(errors, "Address (1) is required.",


this.account.getUserAddr().getAddr1());


addErrorIfStringEmpty(errors, "City is required.",


this.account.getUserAddr().getCity());


addErrorIfStringEmpty(errors, "State is required.",


this.account.getUserAddr().getState());


addErrorIfStringEmpty(errors, "ZIP is required.",


this.account.getUserAddr().getZipcode());


addErrorIfStringEmpty(errors, "Country is required.",


this.account.getUserAddr().getCountry());


}


}



}

/**

* 此方法是一个很重要的方法,我们看看基类中对该方法的描述:


*


* Reset bean properties to their default state, as needed.


* This method is called before the properties are repopulated by the
controller.


* 在需要时,复位 Bean 的属性值,此方法是在控制器重新组装Bean的属性值之前调用的。


*


* The default implementation does nothing. In practice, the only
properties


* that need to be reset are those which represent checkboxes on a


* session-scoped form. Otherwise, properties can be given initial values


* where the field is declared.


* 默认的实现,并没有做任何事。实际上,唯一需要重置的属性是那些基于


* session 作用域的复选框页面元素。否则这些元素将使用页面上声明的默认值。


* 是勾选还是未勾选。


*


* If the form is stored in session-scope so that values can be collected


* over multiple requests (a "wizard"), you must be very careful of
which properties,


* if any, are reset. As mentioned, session-scope checkboxes must be
reset to


* false for any page where this property is set. This is because the
client


* does not submit a checkbox value when it is clear (false).


* If a session-scoped checkbox is not proactively reset, it can never
be set to false.


* 假如表单是存贮在 Session 作用域中(如:


* <action
path="/shop/signon"
type="org.springframework.samples.jpetstore.web.struts.SignonAction"


* name="accountForm" scope="session"
validate="false">


* 即声明为 session 范围的 formBean)的话,表单元素的值可以在多个请求(即多页向导性页面)


* 中被收集,此时必须小心对等哪些输入域必须重置。象我们前面所述,session


* 作用域范围内的 checkbox(复选按钮),在为它们设置值之前必须重置为 false,


* 因为客户端(即浏览器)在复选按钮未被勾选时并不会发送任何值到服务器端。(否则,


* 就出现这样的问题:如果之前该复选按钮是勾选状态,并且用户请求这一页面


* 该按钮显示为勾选状态,在后续的操作中,用户取消选中状态。但是因为 checkbox


* 在取消选中状态后,浏览器并不发送任何关于这个控件的信息,但 ActionForm 中


* 要改变控制的状态,必须比较浏览器传上来的状态和当前状态,但因为浏览器并未


* 告知它,所以 ActionForm 认为这个控件的状态并未改变。因为从这时开始,无论


* 用户怎么做,这个控件将永远保持为选中状态)


*/

public void
reset(ActionMapping mapping, HttpServletRequest request) {


super.reset(mapping, request);


setUsername(null);


setPassword(null);


setRepeatedPassword(null);




// BUG here: by pprun


// 按照此方法的 api 文档说明,说 checkbox 的值必须在此复位,


//可是 NewAccountForm.jsp 中 Enable MyList 和 Enable MyBanner 却没有


// 所以当用户第一次选中后,以后想改为未选中是没门了,(除了象程序控制那样:


// 比如:
acctForm.getAccount().setDisplayMylist(


//
request.getParameter("account.displayMylist") != null);


// acctForm.getAccount().setDisplayBanner(


//
request.getParameter("account.displayBanner") != null);)


//


// 但是当输入错误时重新显示当前页面时,上次选为未选中状态被丢失了!


//


// 因为按照 api 的说明


// 当 checkbox 为未选中状态时,浏览器是不会发信息到服务器端的,所以


// struts 无法设置其值


// 解决办法:


if (getAccount() != null) {


getAccount().setDisplayMylist(false);


getAccount().setDisplayBanner(false);


}

}
}


基于 JSTL 和 Struts HTML Tag 的 JSP


我们主要介绍一下JSP文件的总体结构。



由于此应用在表示层来讲,大体上还是属于 Demo 级别的,所以并未采用 Struts Tile
技术来对页面布局进行管理。而是使用传统的JSP表态包含指令,来包含进公共部分,如页眉,页脚及导航区域等。

所有以 Include 前缀命名的JSP都用来被其它JSP页面包含的页面块。例如:



<%@ include
file="IncludeTop.jsp" %>


页面的具体内容

<%@ include
file="IncludeBottom.jsp" %>






在 IncludeTop.jsp 中声明:


<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>


<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>


<%@ taglib prefix="html" uri="http://jakarta.apache.org/struts/tags-html" %>


这样我们就不需要在所有用到JSTL的页面中重复声明。
同时,我们并没有使用 Struts 的 Bean 和 Logic 等标记库,因为在 Struts 网站上声明有:



Note: - Many of the features in this taglib are also
available in the
JavaServer Pages Standard Tag Library (JSTL).
The Apache Struts
group encourages the use of the standard tags over the Struts specific
tags when possible.


避免重复提交


避免重复提交是一项挑战性的工作,如果你曾经真正参与过一个基于 B/S 结构的项目的话,
甚至基于 C/S结构的界面也同样有这样的工作,正不过在那个领域叫做控制状态管理,
比如,当你按下一个登录按钮后,而按钮并没有变为disable/不可用状态,
你可能在不经意间又点了一次该按钮,那么在一瞬间你肯定登录了两次,这种情况还好,
因为登录并不伤害系统的其它情况,只不过统计系统或许会感觉到纳闷,
为什么在不到两秒钟内,你登录了两次?

但是如果这个操作是插入一条数据或者是删除一条数据呢?

对于插入一条数据,如果系统没设唯一性检查,则两条相同的数据生成了;

对于删除数据,则第二次删除必然会失败。



知道问题的重要性了,可是对B/S 开发人员来说,问题还不止这些:

1.典型的,网络状况不是很好时,为完成一个插入操作可有会等上好几十秒的时间,
用户此时会“再点”一次,还是会“回退”,甚至是忍无可忍关掉浏览器呢?

2. 对于回退,如果前一操作是删除操作,是否需要再次进行一次删除操作?

3. 如果用户收藏起了这一个进行删除或插入操作的URL,在他/她重新激活这一链接后,
该做何处理,如果这一操作需要授权呢?

我们要介绍的机制并不是完美的机制,事实上这些现实的问题并没有列入大多数的WEB
框架的设计议程中,所以做WEB应用开发是乏味的,甚至有时会让人冒火!



Struts 的事务 Token


通过使用Struts 的事务Token 来防止重复提交是可行的,
仔细阅读org.apache.struts.action.Action中的如下方法的 javadoc

  1. generateToken

  2. saveToken

  3. isTokenValid

  4. resetToken

  5. <html:link
    transaction="true">
    If set to true, any current transaction
    control token will be included in the generated hyperlink, so that it
    will pass an isTokenValid() test in the receiving Action.



我们通过提交定单的例子来看这个事务 Token 的工作流:

我们的例子中,是要在显示确认页面中,如果点 'Continue' 按钮,会将一个定单插入到数据库中,
显然,我们需避免重复点击该按钮。


解决方案

我们得看看这个过程的映射配置:


<!-- 进入结算中心页面后,点击继续进入此 -->


<action path="/shop/newOrderForm"
type="org.springframework.samples.jpetstore.web.struts.NewOrderFormAction"


name="workingOrderForm" scope="session" validate="false">


<forward name="success"
path="/WEB-INF/jsp/struts/NewOrderForm.jsp"/>

</action>


<!--
填写定单信息的多页向导式页面 -->


<!--
当第一页校验失败时,需要跳回填写购物单的第一页 -->


<action path="/shop/newOrderStep1"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/shop/newOrderForm.do">


<forward name="confirm"
path="/WEB-INF/jsp/struts/ConfirmOrder.jsp"/>


<forward name="shipping"
path="/WEB-INF/jsp/struts/ShippingForm.jsp"/>

</action>


<!--
(只有页面上填写了将宠物送到不同的地址时,默认为送到当前用户的地址),才会出现此面。
此页校验失败,毫无疑问应该回到这个新地址填写页,而不是整个流程的第一页。
这就是原版中的BUG所在处,因为它将这个向导性的流程处理放到了一个映射中,
所以没法处理这种情况 -->

<action
path="/shop/newOrderStep2"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/ShippingForm.jsp">


<forward name="confirm"
path="/WEB-INF/jsp/struts/ConfirmOrder.jsp"/>

</action>


<!--
当在最后一步确认时出错,需要跳回填写购物单的第一页 -->

<action
path="/shop/newOrderStep3"
type="org.springframework.samples.jpetstore.web.struts.NewOrderAction"


name="workingOrderForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/NewOrderForm.jsp">


<forward name="success" path="/WEB-INF/jsp/struts/ViewOrder.jsp"/>

</action>



1. 因为最后三个映射使用的是同一个 NewOrderAction,再有作重复提交检查就是在这个 Action 中,因此,不可能在这个Action 的 exeute 方法中调用 saveToken(request);,
一般来说,总是在进行重复提交检查的前一个Action 中放一个 Token, 即调用 saveToken(request);方法,因此根据这个流程,我们只能在
NewOrderFormAction
中生成:正如你可以在源码中看到一样:


//避免重复提交

saveToken(request);

return mapping.findForward("success");





2. 因为我们是要避免重复按 ConfirmOrder.jsp
中的 'Continue' 按钮,因此我们需要这样写:



<%-- prevent duplication submit --%>

<center><html:link
page="/shop/newOrderStep3.do?step=3&newOrder=true" transaction="true">

<img border="0"
src="../images/button_continue.gif" />

</html:link>

</center>



3. 最后,在处理的 Action 中(即 NewOrderAction) 进行有效性检查:


protected ActionForward doExecute(ActionMapping mapping, ActionForm form,


HttpServletRequest request, HttpServletResponse response) throws Exception {

if (!isTokenValid(request, false)
{

// 如果结果不是同一个令牌,为多重提交

//resetToken(request); // 判断完不自动销毁,留待下面的逻辑处理

request.setAttribute("message", "多重提交!");
request.getSession().removeAttribute("workingOrderForm");
request.getSession().removeAttribute("cartForm");

// Fixed by pprun for duplicate-submitand bug in the next time submit:
// 竟然不再需要确认了!

request.getSession().removeAttribute("orderForm");
return mapping.findForward("failure");

} else {

// 多页表单

OrderActionForm orderForm = (OrderActionForm) form;

// 是否要进入可选的 shipingAddress 页面

if
(orderForm.isShippingAddressRequired() && orderForm.getStep().equals("1")) {

// 需要将物品寄给别人,而不是自己

return mapping.findForward("shipping");

// 两种情况:
// 1.
从页面1直接进入确认页面(不需要寄到不同的地址时)

// 2. 从 shipingAddress
进入到确认页面

} else if
((orderForm.getStep().equals("1") && orderForm.isShippingAddressRequired() == false)
|| orderForm.getStep().equals("2")) {

// 进入确认页面

return mapping.findForward("confirm");

} else if (orderForm.getOrder() != null) {

// 最终处理

// 销毁事务标记(放在此处,最开始处很重要,

// 以保证不管再快的多重提交都会得到无效的判断的)


resetToken(request);


Order order = orderForm.getOrder();
// todo 这段逻辑应该放在 DAO 层?

getPetStore().insertOrder(order);

// 成功进行后,移除会话状态,
// 以便 NewOrderFormAction 中检查出是否用户后退操作

request.getSession().removeAttribute("workingOrderForm");
request.getSession().removeAttribute("cartForm");

// Fixed by pprun for duplicate-submit and bug in the next time submit:

// 竟然不再需要确认了!所以必须移除它
request.getSession().removeAttribute("orderForm");
request.setAttribute("order", order);
request.setAttribute("message", "Thank you, your order has been
submitted.");

// 选择 ViewOrder.jsp 中的显示方式

request.setAttribute("newOrder", true);
return mapping.findForward("success");

} else {

request.setAttribute("message",
"An error occurred processing your order (order was null).");

return mapping.findForward("failure");

}

}

}



调用
isTokenValid(request,false) 判断我们上述的 1, 2, 3 三处步骤是否是按顺序成功处理完,如果中途哪个步骤重新执行,比如在执行到第三步的 doExecute()的代码resetToken(request)之前,又来了一个请求,由于此时 Token 还在,未被 resetToken, 此时比较已经存在的 Token 和 link 带进的
Token,发现它们俩不同,因此
isTokenValid(request,false)将返回 false,告之多重提交,并跳到错误页面。



我们之所以调用
isTokenValid(request, false) 这个方法并传一个 false是因为我们使用的向导页面,在这个判断之后到最终的确认页面还有一个或两个页面要处理,因此我们不能在判断完后,立即销毁 Token,而是要等到真正处理完时才这样做。但是对简单逻辑的页面,可以直接调用isTokenValid(request) 或isTokenValid(request,true) 在判断完后,直接销毁 Token.


我们还缺什么?


客户端校验

基于 JavaScript 的检验方式。Struts支持这种处理方式,只不过我们没有把这一功能加入进来而已。因为客户端检验可以在第一时间发现输入数据的问题,而不至于浪费一个数据传输来回(提交错误数
据 -> 在 FormBean 中判断为无效 -> 以错误信息的形式显示给用户)。

但是,请记住!
服务端校验是一定要做的,因为有人总喜欢在中途拦截、篡改客户发来的数据而骗过客户端的校验器。而服务端是发生在服务器上,只要服务器没被攻破,黑客是不
可能篡改这段 FormBean 代码的。


漂亮的页面

现在的页面只是个原型,离最终的漂亮还有段距离。但是这是需要美工设计人员介入的,因为一个人总不可能样样在行的。


总结


对于新手而言,看基于 Struts 的实现代码,有时的确会失去方向。此时,最好将 Strut-config.xml
文件打印一份在手边,然后对应页面上的每一个动作(提交,链接点击等)得到其要去往的URL,然后在Strut-config.xml 中找到对应的Action 映射。例如:

在 SignonForm.jsp 页面中有:


<a href="<c:url
value="/shop/newAccountForm.do"/>">

<img
border="0" src="../images/button_register_now.gif" />

</a>



于是我们在struts-config.xml 文件中搜索“/shop/newAccountForm” 找到:



<!-- 修改帐号信息 -->

<action path="/shop/newAccountForm"
type="org.springframework.samples.jpetstore.web.struts.NewAccountFormAction"


name="workingAccountForm"
scope="session" validate="false">

<forward name="success"
path="/WEB-INF/jsp/struts/NewAccountForm.jsp"/>

</action>




这样我们得知:

1. 在页面 SignonForm.jsp 中,如果点击了 注册 按钮的话,Struts 将使用 workingAccountForm
(即,类AccountActionForm) 来收集页面的即将的输入值,



2. validate="false" 所以这时不需要做任何校验,因为此时,用户还没输入数据,只是在
注册页面上点了“注册”按钮被带到了注册信息填写页面。



3. 执行 NewAccountFormAction#execute()方法,在成功处理后,将前进到页面 NewAccountForm.jsp


4. 用户输入用户信息数据


我们看到在 NewAccountForm.jsp 页面中有:

<html:form action="/shop/newAccount.do"
styleId="workingAccountForm" method="post" >



5. 我们再次在 struts-config.xml 文件中找 "/shop/newAccount", 得到


<action path="/shop/newAccount"
type="org.springframework.samples.jpetstore.web.struts.NewAccountAction"

name="workingAccountForm" scope="session" validate="true"
input="/WEB-INF/jsp/struts/NewAccountForm.jsp">

<forward name="success" path="/shop/index.do"/>
</action>


6. 这一次还是利用同一个 FormBean(已经在前面收集了用户的输入数据),
因为这一次 在NewAccountAction#execute()方法中要用到 前面的输入值.


7. 判断输入数据的合法性

如果不合法,将跳转到同一页面,但此时将显示错误信息

如果合法,则继续向前,这一次是回到首页,即这一流程宣告结束。