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. 判断输入数据的合法性

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

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

2006年10月1日星期日

Hibernate JPetstore 系列之三: 控制层技术

文档内容


  • 概览
  • Spring 应用上下文
  • 依赖注入
  • 拦截机
    • Spring 内置支持的事务处理拦截机
    • Spring 自定拦截机

  • 声明性事务控制
    • 事务隔离级别
    • 事务传播行为
    • 只读提示
    • 事务超时周期
  • Actions 及 struts-config.xml
    • BaseAction
    • DoNothingAction
    • ActionForm <-- struts-config.xml --> Action
    • SecureBaseAction

  • DAO接口设计及Hibernate DAO 实现
  • 总结


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


其实在发出上篇文章之后,我发现我遗漏了一个很大的主题没讲,就是在包
org.springframework.samples.jpetstore.dao.hibernate
的实现内容。但是因为这些类的实现严格依赖 Spring 的 HibernateDaoSupport
类,再者由于上篇文章实在太长了,所以决定放在这里来讲。但是请别误会,这个包是属于数据层的内容,并不是控制层。


概览


在传统的基于 Struts 应用中,所谓的控制层组件,自己需要写的都无非是一些 Action,对于
ActionForm,严格地讲,它更接近于表示层,主要用来将表示层的表单数据传递到控制层的 Action。

但是由于我们引入了Spring,所以引入了依赖注入、拦截机(AOP的范畴)及声明性事务控制。

所以本系列的内容除了将上一系列遗漏的 Dao 的 Hibernate 实现补上之外,就是:

依赖注入、拦截机、声明性事务控制及Struts 的 Action.


Spring 应用上下文


Spring 之所以又叫 Bean 包容器(container), 就是因为它存在一个特殊的配置文件
applicationContext.xml 用来注册所有 bean, 这些 bean
会在应用加载或应用部署完成后一刹那完成初始化,除非你将某个 bean
配置成“懒初始化”(Lazily-instantiating),默认的是提前初始化 (eagerly pre-instantiate).详情见
Spring reference: 3.3.5. Lazily-instantiating beans


我们将 applicationContext.xml 全部内容列出:

<?xml version="1.0" encoding="UTF-8"?>

<!--
- Application context definition for JPetStore's business layer.
- Contains bean references to the transaction manager and to the DAOs in
- dataAccessContext-local/jta.xml (see web.xml's "contextConfigLocation").

Jpetstore 的应用上下文定义,包含事务管理和引用了
在 dataAccessContext-local/jta.xml
(具体使用了哪个要看 web.xml 中的 'contextConfigLocation' 的配置)
中注册的DAO

-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">


<!-- ========================= GENERAL DEFINITIONS ========================= -->

<!-- Configurer that replaces ${...} placeholders with values from properties files
占位符的值将从列出的属性文件中抽取出来
-->
<!-- (in this case, mail and JDBC related properties) -->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>WEB-INF/mail.properties</value>
<value>WEB-INF/jdbc.properties</value>
</list>
</property>
</bean>

<!-- MailSender used by EmailAdvice
指定用于发送邮件的 javamail 实现者,这里使用了 spring 自带的实现。
此 bean 将被 emailAdvice 使用

-->
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="${mail.host}"/>
</bean>


<!-- ========================= BUSINESS OBJECT DEFINITIONS ======================== -->

<!--
主要的商业逻辑对象,即我们所说的门面对象
注入了所有的DAO,这些DAO是引用了 dataAccessContext-xxx.xml 中
定义的DAO
门面对象中的所有方法的事务控制将通过下面的 aop:config 来加以控制

- JPetStore primary business object (default implementation).
- Transaction advice gets applied through the AOP configuration below.
-->
<bean id="petStore" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
<property name="accountDao" ref="accountDao"/>
<property name="categoryDao" ref="categoryDao"/>
<property name="productDao" ref="productDao"/>
<property name="itemDao" ref="itemDao"/>
<property name="orderDao" ref="orderDao"/>
</bean>


<!-- ========================= ASPECT CONFIGURATION ======================== -->
<!-- AOP配置,用来控制哪些方法将需要进行事务处理,采用了AspectJ 的语法 -->
<aop:config>
<!--
This definition creates auto-proxy infrastructure based on the given pointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"txAdvice" to all methods on classes named PetStoreImpl.
-->
<!-- 指出在 PetStoreFacade 的所有方法都将采用 txAdvice(在紧接着的元素中定义了)事务方针,
注意,我们这里虽然指定的是接口 PetStoreFacace, 但其暗示着其所有的实现类也将
        
同样具有这种性质,因为本身就是实现类的方法在执行的,接口是没有方法体的。
-->
<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/>

<!--
This definition creates auto-proxy infrastructure based on the given pointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"emailAdvice" to insertOrder(Order) method of PetStoreImpl
-->
<!-- 当执行 PetStoreFacade.insertOrder方法,该方法最后一个参数为Order类型时
(其实我们的例子中只有一个 insertOrder 方法,但这告诉了我们,当我们的接口或类中有重载了的方法,
        
并且各个重载的方法可能使用不同的拦截机机制时,我们可以通过方法的参数加以指定),
将执行emailAdvice(在最后定义的那个元素)
-->
<aop:advisor pointcut="execution(* *..PetStoreFacade.insertOrder(*..Order))"
advice-ref="emailAdvice"/>

</aop:config>

<!--
     
事务方针声明,用于控制采用什么样的事务策略<> Transaction advice definition,
based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name starts with
"insert" or "update", and to PROPAGATION_REQUIRED with read-only hint
for all other methods.
-->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="insert*"/>
<tx:method name="update*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>


<!-- 拦截机,用于在适当的时机(通过AOP配置,如上面)在方法执行成功后发送邮件
AOP advice used to send confirmation email after order has been submitted -->
<!-- -->
<bean id="emailAdvice"
class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean>


<!-- ========================= 忽略 REMOTE EXPORTER DEFINITIONS ======================== -->


</beans>;

先粗略地看看红色的注释和相关的配置,下面将一一介绍。



依赖注入


依赖注入 DI (Dependency Injection ),又做反转控制 IoC (Inversion of Control)。
不管这些概念如何,我们用最简单的文字和例子加以描述,省得费心去理解一大堆陌生的概念。


由于我们的门面实现类中要汇集所有的DAO,要调用DAO的方法,当然首先需要获得DAO的实例对象。既然我们知道一定会用到DAO的实例对象,那么,传
统的方式肯定不外乎

PetStoreImpl

AccountDao accountDao = new HibernateAccountDao(...);

这是传统的依赖方式,即 PetStoreImpl 依赖于 AccountDao,这种传统的依赖方式有什么不好?

因为为了初始化一个类,虽然 对类型的声明可以是接口或抽象类,如我们的 AccountDao 正好是个接口,但 new 后面永远只能是
具体的实现类 (concrete class),
不可能是抽象类或接口。这说明了什么?这说明了当从一种实现切换到另一种实现时,你仍然不得不修改这段代码。如现在想提高性能,重新用JDBC实现了一套
DAO,JdbcAccountDao, 那么从 HibernateAccountDao 换到 JdbcAccountDao, 我们需要这样做:


AccountDao accountDao = new JdbcAccountDao(...);

虽然工厂方法可以减轻这种影响,将改变集中到工厂方法之中,但是一个类要想被构造出来,在普通的 Java 代码中离不开 new
关键字。



所以依赖注入的倡导者认为,既然我们知道 PetStoreImpl 一定会用到 AccountDao,我们不如让 AccountDao 注入到
PetStoreImpl
中,何必要等到要用时,才将其初始出来呢?这就原行的顺序依赖倒过来了:被依赖的对象自己初始化好了并且注入到依赖于它的对象中来。这就是依赖注入或反转
控制的由来。

但是,我知道任何东西都有利必有弊:依赖注入有时会显得浪费,如果整个应用的生命周期内根本没有用到这个类,那个它的初始化及浪费在加载时的时间就显示多
余了。但是,这些损失对于服务器端的程序来讲还是可以忍受,只是对于客户端的程序有些不适合。比如我们的IDE,启动要那么长时间,就是因为每次都加载了
所有的东西,但其实我们只想用它打开一个源文件,看完就关了罢了。

但是我们前面提到了,“懒加载”就是为了解决这一问题的。


下面我们看看项目的依赖注入的例子:

PetStoreImpl.java:


public class PetStoreImpl implements PetStoreFacade, OrderService {

// 以下是所依赖的DAO
private AccountDao accountDao;

private CategoryDao categoryDao;

private ProductDao productDao;

private ItemDao itemDao;

private OrderDao orderDao;

//-------------------------------------------------------------------------
// Setter methods for dependency injection
// 我们采用的是基于 Setter 方法的注入方式
//-------------------------------------------------------------------------

public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}

public void setCategoryDao(CategoryDao categoryDao) {
this.categoryDao = categoryDao;
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}

public void setItemDao(ItemDao itemDao) {
this.itemDao = itemDao;
}

public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}



代码应该好简单,声明一个 private 的依赖的对象,提供一个对应的 setter 方法,剩下的事情就是配置了:



applicationContext.xml 文件中:对每个私有的成员对应有一个 <property name="成员的名称">,
ref= 告诉 Spring 这个成员的实例是引用其它地方配置的 bean, 如果不是在其它地方配置的,这里可以直接提供一个
value="org.springframework.samples.jpetstore.dao.hibernate.HibernateAccountDao",
对于其它类型的属性,如集合类型的属性的值的设置,请参见 Spring reference: 3.3.3. Bean properties
and constructor arguments detailed


 <bean id="petStore" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
<property name="accountDao" ref="accountDao"/>
<property name="categoryDao" ref="categoryDao"/>
<property name="productDao" ref="productDao"/>
<property name="itemDao" ref="itemDao"/>
<property name="orderDao" ref="orderDao"/>
</bean>



dataAccessContext-hibernate.xml 文件中:该文件中配置了所有的 DAO的实现类.

值得注意的是,每个DAO的实现类又需要一个 sessionFactory Bean, 这个重量级的bean 同样是在此文件中定义了。


 <bean id="accountDao" class="org.springframework.samples.jpetstore.dao.hibernate.HibernateAccountDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="categoryDao" class="org.springframework.samples.jpetstore.dao.hibernate.HibernateCategoryDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="productDao" class="org.springframework.samples.jpetstore.dao.hibernate.HibernateProductDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.hibernate.HibernateItemDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="orderDao" class="org.springframework.samples.jpetstore.dao.hibernate.HibernateOrderDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

如果你细心的话,你会发现,在所有的Hibernate 实现的DAO中,根本不存在:


private SessionFactory sessionFactory;

public final void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

但是,我们看看,所有的这些类都是从 org.springframework.orm.hibernate3.support.HibernateDaoSupport
派生过来的,跳到它的源码,我们可以看到:




 private HibernateTemplate hibernateTemplate;


/**
* Set the Hibernate SessionFactory to be used by this DAO.
* Will automatically create a HibernateTemplate for the given SessionFactory.
* @see #createHibernateTemplate
* @see #setHibernateTemplate
*/
public final void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = createHibernateTemplate(sessionFactory);
}



在 Spring 中 HibernateTemplate
扮演着与 Hibernate 的 SessionFactory.getCurrentSession()同样的角色,即获得一次数据库会话,与
JDBC 的 DriverManager.getConnection(...) 有异曲同工之效。

所以上面的代码是真正把 sessionFactory 注入到了 HibernateDaoSupport 类中了,也即是所有 Hibernate
DAO 的超类中了。



因此,我们在所有的Hibernate DAO 总是看到如下代码:


getHibernateTemplate().xxx

如这样,获得了数据库连接后,你想干什么就干什么,但剩下的内容正是上系列遗漏的,在HibernateDAO 实现一节中介绍


拦截机


拦截机 (Interceptor), 是 AOP (Aspect-Oriented Programming)
的另一种叫法,我们的应用在两个地方使用了这种机制。AOP本身是一门语言,只不过我们使用的是基于JAVA的集成到Spring 中的
SpringAOP。同样,我们将通过我们的例子来理解陌生的概念。



先看一下最常用的事务控制器拦截机。如果不采用拦截机的机制时,在使用JDBC进行数据库访问时,存在两种情况:


  • 自动提交 & nbsp;
    这是JDBC驱动默认的模式,每次数据库操作(CRUD)成功完成后,都作为一个单独的事务自动提交,如果未成功完成,即抛出了
    SQLException 的话,仅最近的一个操作将回滚。


  • 非自动提交
    这是想更好的控制事务时需要程序地方式进行控制:


    • 在进行该事务单元的任何操作之前 setAutoCommit(false)

    • 在成功完成事务单元后 commit()

    • 在异常发生后 rollback()


自动提交模式是不被推荐的,因为每个操作都将产生一个事务点,这对于大的应用来说性能将受到影响;再有,对于常见的业务逻辑,这种模式显得无能为力。比
如:

转帐,从A帐户取出100元,将其存入B帐户;如果在这两个操作之间发生了错误,那么用户A将损失了100元,而本来应该给帐户B的,却因为失败给了银
行。

所以,建议在所有的应用中,如果使用 JDBC 都将不得不采用非自动提交模式(你们要能发现了在我们的 JDBC
那个例子中,我们采用的就是自动提交模式,我们是为了把精力放在JDBC上,而不是事务处理上),即我们不得不在每个方法中:


try {
// 在获得连接后,立即通过调用 setAutoCommit(false) 将事务处理置为非自动提交模式

// Prepare Query to fetch the user Information
pst = conn.prepareStatement(findByName);

// ...

conn.commit();

} catch(Exception ex) {
conn.rollback();

throw ex;
} finally {
try {
// Close Result Set and Statement
if (rset != null) rset.close();
if (pst != null) pst.close();

} catch (Exception ex) {
ex.printStackTrace();
throw new Exception("SQL Error while closing objects = " +
ex.toString());
}
 }



这样代码在AOP的倡导者看来是“肮脏”的代码。他们认为,所有的与事务有关的方法都应当可以集中配置(见声明性事务控制),
并自动拦截,程序应当关心他们的主要任务,即商业逻辑,而不应和事务处理的代码搅和在一起。


我先看看 Spring 是怎么做到拦截的:


Spring 内置支持的事务处理拦截机



这个配置比想象的要简单的多:


<aop:config>
<!--
This definition creates auto-proxy infrastructure based on the given pointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"txAdvice" to all methods on classes named PetStoreImpl.
指出在 PetStoreFacade 的所有方法都将采用 txAdvice(在紧接着的元素中定义了)事务方针,
注意,我们这里虽然指定的是接口 PetStoreFacace,

但其暗示着其所有的实现类也将同样具有这种性质,因为本身就是实现类的方法在执行的,接口是没有方法体的。
<> -->
<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/>


<!-- 其它拦截机-->

</aop:config>

1. 所有的拦截机配置都放在 <aop:config> 配置元素中.

2. 下面还是需要理解一下几个有关AOP的专用名词,不过,是挺抽象的,最好能会意出其的用意


  • pointcut 切入点,比如:updateAccount
    方法需要进行事务管理,则这个切入点就是“执行方法体”(execution)。Spring 所有支持的切入点类型在都在 Spring
    reference: 6.2.3.1. Supported Pointcut Designators 中列出了。


  • advice 要对这个切入点进行什么操作,比如事务控制

  • advisor Spring 特有的概念,将上两个概念合到一个概念中来,即一个 advisor
    包含了一个切入点及对这个切入点所实施的操作。


因为 方法执行切入点 execution 为最常见的切入点类型,我们着重介绍一下,execution 的完全形式为:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?)

这是一个正则表达式,其中由 '?' 结尾的部分是可选的。翻译过来就是:

执行(方法访问修饰符? 方法返回类型 声明类型? 方法名(方法参数类型) 抛出异常?)

所有的这些都是用来定义执行切入点,即那些方法应该被侯选为切入点:


  • 方法访问修饰符 即 public, private 等等


  • 方法返回类型 即方法返回的类型,如 void,
    String 等等


  • 声明类型
    1.5的语法,现在可以先忽略它


  • 方法名
    方法的名字


  • 方法参数类型 方法的参数类型


  • 抛出异常
    方法声明的抛出的异常



例如,所有dao代码被定义在包 com.xyz.dao 及子包 com.xyz.dao.hibernate, 或者其它,如果还有的话,子包中,
里面定义的是提供DAO功能的接口或类,那么表达式:

execution(* com.xyz.dao..*.*(..))

表示切入点为:执行定义在包 com.xyz.dao 及其子包(因为 ..
所致) 中的任何方法



详细情况可以参见 Spring refernce: 6.2.3.4.
Examples


<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/>

因此这个表达式为执行定义在类 PetStoreFacade 及其实现类中的所有方法,采取的动作定义在 txAdvice 中.
关于该 advice 的定义,(见声
明性事务控制)一节





Spring 自定拦截机



看来为了进行事务控制,我们只需简单地配置几下,所有的工作都由 Spring
来做。这样固然很好,但有时我们需要有我们特有的控制逻辑。因为Spring
不可能包含所有人需要的所有拦截机。所以它提供了通过程序的方式加以定制的方式。我们的项目中就有这么一个拦截机,在用户确认付款后,将定单信息通过
email 的方式发送给注册用户的邮箱中。



<aop:config>
...

<!--
当执行 PetStoreFacade.insertOrder方法,
该方法最后一个参数为Order类型时(其实我们的例子中只有一个
insertOrder 方法,但这告诉了我们,当我们的接口或类中有重载了的方法,
       
 并且各个重载的方法可能使用不同的拦截机机制时,我们可以通过方法的参数加以指定),
将执行emailAdvice(在最后定义的那个元素)
-->
<aop:advisor pointcut="execution(* *..PetStoreFacade.insertOrder(*..Order))"
advice-ref="emailAdvice"/>

</aop:config>

红色的注释已经说的很清楚这个 Advisor 了,它的切入点(pointcut) 为 PetStoreFacade 的 void
insertOrder(Order order) 方法,采取的动作为引用的 emailAdvice, 下面我们就来看看 emailAdvice:



 <bean id="emailAdvice"
class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean>

它给了这个 advice 的实现类为 logic 包中 SendOrderConfirmationEmailAdvice, 该Bean
引用了我们前面定义的邮件发送器(一个 Spring 内置的邮件发送器).



下面看看这个实现类:

public class SendOrderConfirmationEmailAdvice implements AfterReturningAdvice, InitializingBean {
// user jes on localhost
private static final String DEFAULT_MAIL_FROM = "test@pprun.org";

private static final String DEFAULT_SUBJECT = "Thank you for your order!";

private final Log logger = LogFactory.getLog(getClass());

private MailSender mailSender;

private String mailFrom = DEFAULT_MAIL_FROM;

private String subject = DEFAULT_SUBJECT;

public void setMailSender(MailSender mailSender) { this.mailSender = mailSender; }

public void setMailFrom(String mailFrom) {
this.mailFrom = mailFrom;
}

public void setSubject(String subject) {
this.subject = subject;
}

public void throws Exception {
if (this.mailSender == null) {
throw new IllegalStateException("mailSender is required");
}
}

/**
*

* @param returnValue 被拦截的方法的返回值

* @param m 被拦截的方法的所有信息(Method类封装了这些信息)

* @param args 被拦截的方法的所有参数组成的数组

* @param target 目标对象,对于方法执行来说,即是方法所在的类的实例(与 this 同,批当前对象)

* @throws java.lang.Throwable

*/

public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable {
// 我们被拦截的方法为 void insertOrder(Order order),方法只有一个参数,所以可知数据的第1个元素即是被传进的 order 对象
// 得到了order 对象,就可以将 order 对应的帐户名及帐单号发送到邮件中,以便确认无误。
Order order = (Order) args[0];
Account account = ((PetStoreFacade) target).getAccount(order.getUser().getUsername());

// don't do anything if email address is not set
if (account.getEmail() == null || account.getEmail().length() == 0) {
return;
}

StringBuffer text = new StringBuffer();
text.append("Dear ").append(account.getFirstname()).
append(' ').append(account.getLastname());
text.append(", thank your for your order from JPetStore. " +
"Please note that your order number is ");
text.append(order.getId());

SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(account.getEmail());
mailMessage.setFrom(this.mailFrom);
mailMessage.setSubject(this.subject);
mailMessage.setText(text.toString());
try {
this.mailSender.send(mailMessage);
} catch (MailException ex) {
// just log it and go on
logger.warn("An exception occured when trying to send email", ex);
}
}

}



1. 色的内容即为反向注入的 mailSender 属性



2. 色的内容为 Spring Bean
的一个通用的接口 InitializingBean
实现类需要实现该接口定义的方法 afterPropertiesSet()
,该方法中一般是在Bean 被初始化后并设置了所有的 setter
注入后调用的。所以这里是保证邮件发送器配置正确。因为如果没有配置正确,下面的工作是无法进行的,所以与其等那时抛出异常,还不如早早地在部署时就告知
(通过抛出 IllegalStateException 来提示)



3. 绿色的内容为这个 Advise
的核心,即在切入点被切入后将采用的动作。因为 Advise 也同样有多种类型,比如我们这里的“方法正常返回”,“方法执行前”,“方法执行后”,“环绕在方法执行前后”,“方法抛出异常时”等等(详情参见 Spring Reference: 6.2.4. Declaring advice)。但是我们的逻辑为在用户
确认定单并且执行成功(所谓的成功是指将这一定单插入到了表 Order 中了)后,将发送一确认信。所以”方法正常返回“完全符合我们的要求。

接口AfterReturningAdvice
即是Spring中表示”方法正常返回“
这一语义的 Advice, 所以我们实现这个接口及其必须的方法 afterReturning.

方法代码的工作其实并不重要,只要我们理解这些“魔法”一样的技术后,实现代码是很简单的。值得提及的是这个方法的参数,这
些参数是封装了切入点的所有信息,请见上面的注释。在
我们的实现中只使用了被拦截方法的参数,在复杂的 Advice 实现中可能会用到切入点所有信息。


声明性事务控制



由于你们都不熟悉EJB,其实EJB有一个专用名词叫做 CMT (Container
Management Transaction),它也是在EJB的部署文件中对每个方法声明执行这个
方法是否需要进行事务控制,以及如何控制。



理解事务处理的概念是掌握任何事务处理框架的的关键,所以我们必须强迫自己尽量理解。Spring 的事务处理框架也不例外,详细情况可以参见
(Spring In Action: 5.3.1
Understanding transaction attributes):


  • Propagation behavior 传播行为


  • Isolation level 隔离级别


  • Read-only hints 只读提示


  • The transaction timeout period 事务超时周期

事务隔离级别


由于事务隔离级别的概念相对简单些,所以我们首先看看事务隔离级别



打开 java.sql.Connection类的 doc
(jdk-1_5_0-doc/docs/api/java/sql/Connection.html),我们可以看到其声明了五个事务隔离极别常量成
员:


  1. TRANSACTION_NONE
    A constant indicating that transactions are not
    supported.


  2. TRANSACTION_READ_UNCOMMITTED
    A constant indicating that dirty reads, non-repeatable reads and
    phantom reads can occur. This level allows a row changed by one
    transaction to be read by another transaction before any changes in
    that row have been committed (a "dirty read"). If any of the changes
    are rolled back, the second transaction will have retrieved an invalid
    row.()

  3. TRANSACTION_READ_COMMITTED
    A constant indicating that dirty reads are prevented; non-repeatable
    reads and phantom reads can occur. This level only prohibits a
    transaction from reading a row with uncommitted changes in it.

  4. TRANSACTION_REPEATABLE_READ
    A constant indicating that dirty reads and non-repeatable reads are
    prevented; phantom reads can occur. This level prohibits a transaction
    from reading a row with uncommitted changes in it, and it also
    prohibits the situation where one transaction reads a row, a second
    transaction alters the row, and the first transaction rereads the row,
    getting different values the second time (a "non-repeatable read").

  5. TRANSACTION_SERIALIZABLE
    A constant indicating that dirty reads, non-repeatable reads and
    phantom reads are prevented. This level includes the prohibitions in
    TRANSACTION_REPEATABLE_READ and further prohibits the situation where
    one transaction reads all rows that satisfy a WHERE condition, a second
    transaction inserts a row that satisfies that WHERE condition, and the
    first transaction rereads for the same condition, retrieving the
    additional "phantom" row in the second read.




以下是我对这几个隔离极别的翻译:(我想如果是四年制本科的软件专业,在数据库课程中应该会接触到这些内容)


  1. TRANSACTION_NONE
    没有事务支持,不可能用在真正的应用中


  2. TRANSACTION_READ_UNCOMMITTED
    允许“脏”读,不可再现重读和影子读:具体表现为,允许数据库的一行由一个事务单元A进行改变的同时,允许由另一个事务单元B读取这个被事务A改变后的同
    一行,而此时事务A还没提交它的改变,因为数据正处在改变状态还未将改变提交到数据库中(术语“脏”的来历),如果事务后来又回滚了它的改变,那么事务B
    读取的信息将是无效的,而这无效只有等到事务B提交时才能察觉到(以失败告终)。此极别提高了事务的并发性,但同时导致了无效的读增多。此方案在只读事务
    单元时有用

  3. TRANSACTION_READ_COMMITTED
    此级别不允许“脏读”,但未可再现重读和影子读还是允许的,此级别阻止了读取未提交的改变。

  4. TRANSACTION_REPEATABLE_READ
    此级别不允许“脏读”,也不允许不可再现的重读,但影子读还是允许的。所谓的“不可再现的重读”为:一个事务单元A读取一行,紧接着第二个事务单元B改变
    了这行,但未提交更改,事务单元A再接接着读取这行,如果允许事务单元A两次读取的内容不同,则叫做不可再现的重读,如果数据库保证了了两次读取的内容必
    须相同,则为可再现重读。

  5. TRANSACTION_SERIALIZABLE
    此级别指示“脏读”,不可再现重读及影子读都被禁止。本级别除了 TRANSACTION_REPEATABLE_READ
    中的限制之外,还得满足未影子读。所谓影子读为:如果一个事务单元A通过 Where
    条件限制了取回的结果集,而第二事务单元B插入了一行且这行满足Where
    条件,但未提交更改,当事务单元A再次通过同样的条件进行查询时,如果可以获得刚刚由事务单元B插入的记录,则表示影子读,如果不可能获得则叫禁止影子读


事务传播行为


除了上面我们已经介绍了的隔离级别,另一个重要的概念则是事务传播行为(Propagation behavior):



Spring In Action:
Table 5.2 Spring’s
transactional propagation rules


  • PROPAGATION_MANDATORY
    Indicates that the method must run within a transaction. If no existing
    transaction is in progress, an exception will be thrown.

  • PROPAGATION_NESTED
    Indicates that the method should be run within a nested
    transaction if an existing transaction is in progress. The nested
    transaction can be committed and rolled back individually from the
    enclosing transaction. If no enclosing transaction exists, behaves like
    PROPAGATION_REQUIRED. Beware that vendor support for this propagation
    behavior is spotty at best. Consult the documentation for your resource
    manager to determine if nested transactions are supported.

  • PROPAGATION_NEVER
    Indicates that the current method should not run within a transactional
    context. If there is an existing transaction in progress, an exception
    will be thrown.

  • PROPAGATION_NOT_SUPPORTED
    Indicates that the method should not run within a transaction. If an
    existing transaction is in progress, it will be suspended for the
    duration of the method. If using JTATransactionManager, access to
    TransactionManager is required.

  • PROPAGATION_REQUIRED
    Indicates that the current method must run within a
    transaction. If an existing transaction is in progress, the method will
    run within that transaction. Otherwise, a new transaction will be
    started.

  • PROPAGATION_REQUIRES_NEW
    Indicates that the current method must run within its own transaction.
    A new transaction is started and if an existing transaction is in
    progress, it will be suspended for the duration of the method. If using
    JTATransactionManager, access to Transaction-Manager is required.

  • PROPAGATION_SUPPORTS
    Indicates that the current method does not require a transactional
    context, but may run within a transaction if one is already in progress.

  • PROPAGATION_MANDATORY
    指示方法执行必须具有事务支持,否则异常将会抛出。

  • PROPAGATION_NESTED

    指示如果当前已经存在一个事务,该方法将在自己的事务单元中执行,但该事务单元将嵌套在已存在的事务单元之中,而且这个事务单元可单独于外包的事务单元进
    行提交、回滚。如果没有事务单元存在,它的行为与PROPAGATION_REQUIRED一样,一个新的事务单元将被初始化出来。但是每个数据库厂间对
    这个传播行为的支持不大一样。请留意。

  • PROPAGATION_NEVER
    指示方法不应运行在一个事务单元中,如果已经有一个事务单元存在了,将抛出异常。

  • PROPAGATION_NOT_SUPPORTED
    指示方法不应运行在一个事务单元中,但如果已经存了一个事务单元,在方法的执行过程中将会将事务功能挂起。

  • PROPAGATION_REQUIRED
    指示方法必须运行在事务单元中,如果已经存了一个事务单元,则方法将合并到该事务单元中去,如果没有存在的事务单元,则将创建出一
    个新的事务单元。

  • PROPAGATION_REQUIRES_NEW
    指示此方法必须运行在自己的事务单元中,如果一个事务单元已经存在的话,一个新的事务单元将创建且原先存在的事务单元在这个方法的执行其间将被挂起。

  • PROPAGATION_SUPPORTS
    指示方法不必一定要运行在事务单元中,但是如果有已经存在的事务单元的话,它也乐意在事务单元中运行。

只读提示



此属性只对 PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and
PROPAGATION_NESTED 传播行为有意义,因为对于其它没有事务单元的传播行为是没有必要的。

只读提示给予数据库有机会作出更大胆的优化。

如果是使用 Hinbernate 作为 O/R maping 的话,Hibernate 可以使用 FLUSH_NEVER
模式避免不必要的数据同步消耗,因为是吹读,数据同步是根本没有必要的。


事务超时周期



对于长时间运行的事务操作,我们必需估计操作的最坏情形(也就可能的最大运行时间),由于事务会大量使用数据库锁,将相关的数据库列,或数据库行或整个表
锁住。长时间的锁将会使数据库长时间不可用。因此我们必须估计最坏的情形。通过设置超时时间,超过了这个时间,整个事务会回滚。


ORACLE Tip:
Read-only connections are supported by the Oracle server, but not by the Oracle JDBC drivers. For transactions, the Oracle server supports only the TRANSACTION_READ_COMMITTED and TRANSACTION_SERIALIZABLE transaction isolation levels.
The default is TRANSACTION_READ_COMMITTED.

只读模式只针对服务器端,JDBC 驱动不支持。
ORACLE只支持两种事务隔离级别:TRANSACTION_READ_COMMITTED 和 TRANSACTION_SERIALIZABLE, 默认的是 TRANSACTION_READ_COMMITTED



弄懂了上面这些重要的概念后,看 Spring 对事务的配置是相当的简单了:


 <!--
事务方针声明,用于控制采用什么样的事务策略<> Transaction advice definition, based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name starts with
"insert" or "update", and to PROPAGATION_REQUIRED with read-only hint
for all other methods.
-->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="insert*"/>
<tx:method name="update*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>



1. 首先定义了 Advice ,该 advice 被 AOP事务 拦截机引用了

2. 事务的属性,依次列出了每个方法的前缀,如 insert* 则会匹配PetStoreFacade中的 void
insertAccount(Account account); void insertOrder(Order order);,而
update* 则会匹配 void updateAccount(Account account); ,其它的("*") 都只是
get/search/select, 即是只读查询了,所以了指定了 read-only="true" 属性.



你可能会问,上面谈了半天的 isolation level 及 propagation behavior, 怎么这里根本没有出现。其实是
Spring 使用了默认的值:



如果没有指定隔离级别,ISOLATION_DEFAULT 将被默认指定

如果没有指定事务传播行为,PROPAGATION_REQUIRED 将被指定



所以上面的声明与下面的的一样的:


 <tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="insert*" PROPAGATION_REQUIRED ISOLATION_DEFAULT />
<tx:method name="update*" PROPAGATION_REQUIRED ISOLATION_DEFAULT />
<tx:method name="*" PROPAGATION_REQUIRED ISOLATION_DEFAULT read-only="true" />
</tx:attributes>
</tx:advice>



即:

1. 所有以 insert 开头的方法都需要事务处理 (PROPAGATION_REQUIRED)而隔离级别采用底层数据为默认的级别

2. 所有以 update 开头的方法与 insert 相同

3. 其它方法除与上面的事务属性相同外,且为 read-only



Actions 及 struts-config.xml

Action 是Struts 的核心,是控制层组件的关键,是通往业务层/ DAO的层的途径。在页面上,每一个按钮,超链接都有相应的
Action 来表示,因为每个这种页面元素都是用来做一点事情的。比如:提交一张用户信息表单;执行一段服务器的代码,比如,按下搜索按钮,将从数据库
中查出满足条件的产品记录。

每个Action 都应当从 Struts 的 org.apache.struts.action.Action 类派生下来,并
overriding 其中的一个 execute() 方法,所有的动作(按下按钮或点击链接后的动作)都应当在这个方法中完成。



我们这里不可能把所有的 Action 都列出来,所以只挑几个有代表的来作介绍,最主要是理解 Struts
框架的机理,怎么使各个部分有机的组织在一起,达到简化开发步骤的目的。



BaseAction



我们已经在前面的系列中详细讲过这个类,如果不记得它做了什么,回头看看源码,如果还不十分清楚,可以再回头看看前面的系列。这个 Action
是我们所有真正工作的(带有 execute()方法的) Action的超类,所以这里不打算重复了。不过,值得借鉴的是,如果所有的 Action
中都有的东西,都可以提到这个超类当中来。



DoNothingAction


对于有些 Action,
它其实并没涉及到商业逻辑,只是从一个页面跳到另一个页面,因为甚至连校验用户这个功能都不需要(这是可能的,当你进入china-pub
时,在没有注册的情况下是完全可以浏览图书的)。所以存在一个与商业逻辑无关的 Action, 即 DoNothingAction,
我们看看它的代码:




public class DoNothingAction extends BaseAction {

/* Public Methods */

public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 是否是从 Spring web 跳过来的
if (request.getParameter("invalidate") != null
&& request.getParameter("invalidate").toString().equals("true")) {
// 如果是,无效 session
request.getSession().invalidate();
}


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



其中,真正的代码就这一行:return mapping.findForward("success");

其它的是因为我们工程是有两套WEB框架来演示的(一个是 Struts , 一个是 SpringMVC),
所以这里判断是否是从另一种实现中跳过来的,如果是的话,先将目前 session
中的对象无效,比如:已经登录的用户,已经放入到购物车的东西。因为从一种框架跳到另一种框架(在页面的最下面),后台的实现是一样的,只是WEB层东西
不一样,但为了演示的框架的整个生命周期,所以从一个实现跳到一种实现都会“从头再来”。



所以这个Action 所要进行的动作就是,找到你该去的地方(success),如果不出意外的话,跳过去吧。在
struts-config.xml 文件中搜索(Ctrl-f) "DoNothingAction",我们发现三个地方引用了该类




<!-- 无论何时,要去首面时 -->
<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/signonForm" type="org.springframework.samples.jpetstore.web.struts.DoNothingAction"
validate="false">
<forward name="success" path="/WEB-INF/jsp/struts/SignonForm.jsp"/>
</action>



ActionForm <-- struts-config.xml --> Action


struts-config.xml 文件是Struts 框架将各部分有机地组织在一起的关键,可以打开 struts-config.xml
文件看看:

1. 它声明了所有的 form-bean (即每个输入表单对就的 Java 对象,用来收集用户提交的输入信息),这些 bean 将被
<action-mappings> 中的 <action> 的 name 属性引用

2. 它指定了全局跳转 (global forward), 可以在Action 的 execute()
方法中引用,如:mapping.findForward("failure");

3. 它指定了所有的 action-mapping, 从这个 mapping 集合就可知整个应用的来龙去脉,如:


 <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>

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
该回到哪去纠错

SecureBaseAction

SecureBaseAction 是一个抽象类,但它也是从BaseAction 派生过来的,它的目的在执行 execute 方法之前首先要经过是否登录检查,如果没有则跳到登录页面,并将当前要去往的页面记下来,等登录完
后,再跳转过去。(而不象有些菜鸟做的,我本来选择了去下载页面,但它首先要我登录,而登录后却把我带到了首页,而不是我要去的下载页面。注意在
execute() 方法中调用用的是抽象方法 doExecute(...) ,这个抽象方法是等待此类的子类来实现的。这也是多态性的表现。



DAO接口设计及Hibernate DAO 实现


设计方式有好多种,有些是从自底向上设计,有些叫做自顶向下设计。对于 J2EE 的项目自底向上设计会好些。

对于DAO的设计,一般来说,每个DAO都会包含所有的 CRUD(Create, Retrieve, Update,
Delete),而不管顶层,即表示层目前是否会全用到这些功能。因为这些基本功能是组成更复杂数据操作的基础,况且目前虽然没有用到,但说不定随着产品
的演化,下一版本就会用到。况且实现这些 CRUD并不那么困难。但是我们的项目好象偷了懒,只是定义出上层需要的所有方法。这对于演示项目也无可厚非。



除了基本的CRUD,一般为了适应上层的需求,都需要提取出相应的方法来满足需求的动词。如:列出满足条件的所有产品,那么在 ProductDao
中则有:




List searchProductList(String keywords) throws DataAccessException;



我们不把主要精力放在DAO接口的设计上,因为这主要是设计师的责任(或许对于小公司而言,也是程序员的责任,如果是这样的话,一定要抓住机会!)。我们
主要看看基于 Spring 的 HibernateSupport 的 Dao 的实现。



如果了解SQL语言,其实是很简单的,但如果不了解的话,别无选择,至少得读一本SQL方面的书,如果想成为一名开发者的话,当然在第一份工作时可以不必
担心,如果你敢于冒险的话!是不是,良子?



摘自HibernateCategoryDao.java


public class HibernateCategoryDao extends HibernateDaoSupport implements CategoryDao {


public List getCategoryList() throws DataAccessException {
// todo: now not retrive id
return getHibernateTemplate().find(
"from Category");
}

/**
* todo: pp, renamed categoryId to categoryName
*/
public Category getCategory(String categoryName) throws DataAccessException {
Category category = null;

List ls = getHibernateTemplate().find(
"from Category cat where cat.categoryName = ?", categoryName);

if (ls != null && ls.size() > 0) {
category = (Category) ls.get(0);
}

return category;
}
}



色的是调用的 Spring 的
HibernateDaoSupport 的api


色的则是 HQL (Hibernate Query Language) , SQL 的变体



1. 要从表 Category 选出所有记录,form
Category
就这么简单,其相当于 select
* from category
;



2. 如果要设置查询条件,就象第二个方法中那样做。



3. 如果 api 返回的是一个结果集,那么它即是 List, 有了这个结果集,我们就可以用我们已熟悉的方式处理它了。JSTL
的for-each,普通的iterator 随你怎么用。值得一提的是,Spring 内置支持简单的分页功能。在我们的好几个 Action
都用到了。如:



分页显示产品项列表,每页显示4项:


 PagedListHolder itemList = new PagedListHolder(getPetStore().
getItemListByProduct(productNumber));
itemList.setPageSize(4);





4. 如果我不想返回表的所有列,那么就明确地写 select 子句(前我们都省去了它)



摘自 HibernateProductDao.java


getHibernateTemplate().find(
"select p , c.categoryName " +
"from Product p, Category c " +
"where c.id = p.category.id and p.productNumber = ?", productNumber);

值得提及的是,我们看到(绿色的字)我们可以级联导航(多层访问,通过点"."), p.category.id
其实是从Product表导航到了Category 表,再访问 id 列的。



上面我们看到的都是查询,我们看看其它操作:



5. 插入

摘自HibernateOrderDao.java


getHibernateTemplate().persist(order);

如果映射一切正常,在这个购物单上的所有项(LineItem)都将被持久化。还记得 cascade="save-update"
吗?我们花那么大力气,为得就是此时的这么简单。





6. 更新

摘自 HibernateAccountDao.java


getHibernateTemplate().update(account, LockMode.UPGRADE);



7. 删除

虽然我们的代码中没有删除功能,但正如你猜到的,如下:


getHibernateTemplate().delete(abcObject);



8. 在IDE中,将鼠标指针放在 getHibernateTemplate().
激活自动完成,看到的方法应该可以完成大部分普通的数据操作,如果你能用SQL轻松实现的话,应该就会在这里找到对应的方法。





最后,你可能会问,我们映射了那么多 domain 对象,为什么这时只有五个DAO呢?

我们没有必要对每个 domain
对象都提供一个DAO的,特别是在关系建立的比较好的数据库时。因关系的存在,再加映射正确地建立起来了,从一个表导航到另一个表,此时只在 java
代码或 HQL 中表现出来:


getHibernateTemplate().find(
"select inv.quantity from Inventory inv where " +
"inv.item.itemName = ?", itemName);

虽然此语句在 HibernateItemDAO 中,但是它用到了 Item 与 Inventory 之间的关系。



一般按照这个法则,domain 是细粒度的对象 < Dao 是中等粒度的对象 < Facade
门面是粗粒度的对象


总结


控制层技术或框架本身就是为了简化开发而开发的,所以它不可能太复杂的。

只要弄懂了框架的来龙去脉,写应用就不会偏离主心。