显示标签为“Java”的博文。显示所有博文
显示标签为“Java”的博文。显示所有博文

2009年11月24日星期二

旅途中仓促引入 的bug

需求:当 endTime 比当前时间超过 20 年时,将 expiration date 置为空。

错误地将 date 要么减 20 年,要么为null:

Calendar endTime = response.getConsumerService().getEndTime();
if (endTime != null) {
// roll endTime back 20 years
CalendarUtil.truncateDay(endTime).roll(Calendar.YEAR, -20);

if (endTime.after(CalendarUtil.truncateDay(Calendar.getInstance()))) {
// If the expiration date of the current subscription received from ECWSis greater than 20 years, make this null
endTime = null;
}
}
currentSubscriptionDto.setExpirationDate(endTime);

正确的逻辑应该为:
Calendar endTime = response.getConsumerService().getEndTime();
if (endTime != null) {
Calendar clonedEndTime = (Calendar)endTime.clone();

// roll clonedEndTime back 20 years
CalendarUtil.truncateDay(clonedEndTime ).roll(Calendar.YEAR, -20);

if (clonedEndTime .after(CalendarUtil.truncateDay(Calendar.getInstance()))) {
// If the expiration date of the current subscription received from ECWSis greater than 20 years, make this null
endTime = null;
}
}
currentSubscriptionDto.setExpirationDate(endTime);


原本高兴地在机场第一次 bug fixing, 可是时间仓促,也是因为 Date 和 BigInteger, BigDecimal 一样,思考起来一点也不直观。

慎加注意!

2008年11月19日星期三

NetBeans 6.5 出来了!

Download NetBeans!
尽管这一版 6.5 一直在“鼓吹”给 PHP 的朋友听,
但是NetBeans team 还是没有忘本,有关JAVA IDE 的特征也不少:
NetBeans IDE 6.5 Release Client - New and Noteworthy
6.5 Release Information


下面这些是我个人比较喜欢的:
  • Automatic Compile on Save
  • Improved Eclipse project import and synchronization
  • Java Call Hierarchy
  • Analyze Javadoc
  • CamelCase code completion
  • Customize formatting settings per project
  • Enhanced support for Spring, Hibernate
  • New multi-threaded debugging with improved UI and work-flow

Have fun!

2008年11月16日星期日

JAVA 1.6.0_10 -Xmx

依次执行如下命令

C:\Documents and Settings\pprun>ver

Microsoft Windows XP [版本 5.1.2600]

C:\Documents and Settings\pprun>java -version
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)


C:\Documents and Settings\pprun>java -Xmx2048m -version
Error occurred during initialization of VM
Could not reserve enough space for object heap
Could not create the Java virtual machine.

2 G 内存,很新鲜吗?
我刚加了一条,才百多块。
然而,直到我试到 1446 这个可爱的幸运数字时,才成功:
C:\Documents and Settings\pprun>java -Xmx1447m -version
Error occurred during initialization of VM
Could not reserve enough space for object heap
Could not create the Java virtual machine.

C:\Documents and Settings\pprun>java -Xmx1446m -version
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode)


我立即切换到 Ubunut8.04,进行了一下测试:

pprun@pprun-t61:~$ uname -r
2.6.24-21-generic
pprun@pprun-t61:~$ sudo lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 8.04.1
Release: 8.04
Codename: hardy

pprun@pprun-t61:~$ java -Xmx3072m -version
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Server VM (build 11.0-b15, mixed mode)

pprun@pprun-t61:~$ java -Xmx3722m -version
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Server VM (build 11.0-b15, mixed mode)

pprun@pprun-t61:~$ java -Xmx3723m -version
Error occurred during initialization of VM
Could not reserve enough space for object heap
Could not create the Java virtual machine.

是不是 JAVA 也赞成 Windows XP 黑屏?
竟然不能够分配超过 1446m (1.446g) 内存堆!

2008年6月1日星期日

列出 JVM/JRE 当前的所有系统属性

使用一行代码即可得知:


public static void main(String[] args) {
System.getProperties().list(System.out);
}


以下是我的输出:


-- listing properties --
java.runtime.name=Java(TM) SE Runtime Environment
sun.boot.library.path=/usr/lib/jvm/java-6-sun-1.6.0.06/jre/...
java.vm.version=10.0-b22
java.vm.vendor=Sun Microsystems Inc.
java.vendor.url=http://java.sun.com/
path.separator=:
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
file.encoding.pkg=sun.io
user.country=CN
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=unknown
java.vm.specification.name=Java Virtual Machine Specification
user.dir=/home/****/code/NetBeansProjects/Main
java.runtime.version=1.6.0_06-b02
java.awt.graphicsenv=sun.awt.X11GraphicsEnvironment
java.endorsed.dirs=/usr/lib/jvm/java-6-sun-1.6.0.06/jre/...
os.arch=amd64
java.io.tmpdir=/tmp
line.separator=

java.vm.specification.vendor=Sun Microsystems Inc.
os.name=Linux
sun.jnu.encoding=UTF-8
java.library.path=/usr/lib/jvm/java-6-sun-1.6.0.06/jre/...
java.specification.name=Java Platform API Specification
java.class.version=50.0
sun.management.compiler=HotSpot 64-Bit Server Compiler
os.version=2.6.24-17-generic
user.home=/home/****
user.timezone=
java.awt.printerjob=sun.print.PSPrinterJob
file.encoding=UTF-8
java.specification.version=1.6
user.name=****
java.class.path=/home/****/code/NetBeansProjects/Mai...
java.vm.specification.version=1.0
sun.arch.data.model=64
java.home=/usr/lib/jvm/java-6-sun-1.6.0.06/jre
java.specification.vendor=Sun Microsystems Inc.
user.language=zh
java.vm.info=mixed mode
java.version=1.6.0_06
java.ext.dirs=/usr/lib/jvm/java-6-sun-1.6.0.06/jre/...
sun.boot.class.path=/usr/lib/jvm/java-6-sun-1.6.0.06/jre/...
java.vendor=Sun Microsystems Inc.
file.separator=/
java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport...
sun.cpu.endian=little
sun.io.unicode.encoding=UnicodeLittle
sun.desktop=gnome
sun.cpu.isalist=



请注意,在此配置下,如果想读写日期格式:dd-MMM-YYYY (01-Jun-2008),
是会出错的,如果在不修改源码的情况下,需要修改系统属性:


java -Duser.language=en ...

2008年2月2日星期六

Closures in Java 7: YES vs. NO

Java 前辈(James Gosling, Joshua Bloch, Neal Gafter, ...)们现在正在热论“闭包”这一主题:

http://blogs.sun.com/jag/entry/closures
http://java.dzone.com/news/james-brings-closure-debate

我个人的观点是 "NO", 因为这将使 JSL (Java 语言规范)逼近1000页, 并且,
到现在我仍然在这一恐惧中:我用来学习 Java 5 Generic 的时间比曾经用来学习 Java 语言时间还长,可是让我现在设计出一个使用 “泛型”的框架,我心中没底。

2007年9月18日星期二

NetBeans 6.0 beta1 终于出来了!

经过“漫长”的等待,beta1 终于出来了!其实我一直在用它的最近的 daily build, 因为 M10 实在是太不稳定了。
beta1 给人的第一印象是,更换了主题(桌面 icon, welcome 页,向导图案),使用的是接近于 Vista 类似的蓝绿色调,给人以清新的感觉!
加紧试用吧!



2007年8月25日星期六

SUN 将股票代码 SUNW 换为 JAVA

作为SUN公司战略计划的一部分,其将老的股票代码SUNW退修,而取而代之 JAVA,
作为JAVA开发者,我们在高兴之余,你还想详细了解其 "java everywhere" 战略计划吗?
请看SUN总裁的BLOG(对,为数不多的CEO会写博客,Jonathan Schwartz 就是其中的一位,
而且他还留着马尾辫子。个性与成功并不矛盾!)

http://blogs.sun.com/jonathan/entry/java_is_everywhere

2007年8月15日星期三

Hibernate Jpetstore 之五 部署

文档内容
  • 概览
  • 获得工程代码
  • 配置
    • 数据库初始化
      • MySQL 5.x
      • Oracle 9i, 10g, HsqlDB, Postgres 及其它
    • 应用服务器初始化
      • 本地数据源(非JNDI 数据源)
        • Tomcat 5.x,
        • Sun Application Server8.x, 9.x / GlassFish 1.x, 2.x
        • Jetty 6.1.3
      • JNDI 数据源
        • Sun Application Server8.x, 9.x / GlassFish 1.x, 2.x
        • JBoss 4.0.4 +
        • Jetty 6.1.3
  • 部署并运行
  • 总结
PS: 为什么这个系列的最后一篇这么长时间才出来?原因是,我跟大家说过,工程的所有源代码最终将发布。所以我要找一个合适的地方上载。
这个过程还是比较烦的,况且老外们的工作效率普通不如我们,每次交流至少等上一周左右才有回应。试想申请、审批,确认,上载等过程,其实令我这个急性子试 图想把自己家里的电脑搬到主机托管中心,申请一个域名了事!

还好,它终于出来了 (hjpetstore)!但愿没让你失望。

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

概览

对于传统的J2EE 项目,当项目开发完后,其工作并未结束,紧接着的部署过程其实是很令开发者头痛的,特别是象重量级的应用服务 WebLogic, WebSphere 等。还好 NetBeans 现在抬简化了这一过程。
我们这个例子是基于无存在数据库的方案,所以在我们演示结果前,我们将要导入一些数据。但首先我们得创建相应的数据库用户和数据库方案(Schema).

获得工程代码

1.从 hjpetstore 得到工程源代码,具体的步骤网站上有说明,在 NetBeans 中就很简单了:

CVS | checkout:
cvs root: :pserver:username@cvs.dev.java.net:/cvs (这个 username 是你必须到 java.net 上注册的用户名称,目前 anonymous 好象不能工作了)
password:

下一页中, module: hjpetstore 下载完后,NetBeans 会问你是否打开该工程,选择是。

2. 你可能需要调整一下lib 的位置,这是 NetBeans 的一个缺陷,保存的路径不是相对路径。
右击工程 | properties
点 Libraries,在Compile 页中将所有 .jar 文件 选中后 'remove',
再加入下载下来的WEB-INF/lib 目录下的所有 jar 文件

3. 确保 Clean And Build project 成功


配置

数据库初始化

我这里只介绍 MySql 的 配置,其它的数据库配置列作 TBD. (待做,其实大部分脚本已经在工程中了,等待你的加入吧!因为我不是一个数据库专家,也没有太多时间去研究这个。)我成功配置过oracle 和 hsqldb.

注 意:因为下面的脚本会删除 'hjpetstore'数据库用户及其所有资源,请确保用户 'hjpetstore' (oracle) 或数据库 hjpetstore (mysql) 目前没被使用,如果使用了,请修改数据库脚本。所以最好的办法是使用你的个人数据库来作演示。

MySQL 5.x


1. 创建用户hjpetstore 和 数据库 hjpetstore

# 在命令行下以 root 身份运行创建脚本
# $hjpetstore 是用真实的工程路径代替
# 其它值根据你的设置作相应的改变,比如你如果连非本机的数据库,那 'localhost' 就是那个机器在 ip 了
> mysql -h localhost -u root -p < $hjpetstore\conf\jpetstore_mysql.sql Enter password: ******** 如果程序的输出显示了 hjpetstore, 则表明成功了: Database information_schema
hjpetstore

mysql
...

或者,如果有mysql query browser 的话,用它直接运行如下命令也可:
-- frist drop database hjpetstore and user hjpetstore
drop database if exists hjpetstore;

create database hjpetstore;

-- create user hjpetstore and give the password hjpetstore
grant all privileges on hjpetstore.* to hjpetstore identified by 'hjpetstore';

show databases;

2. 得用 hibernate.hbm2ddl.auto 自动生成数据库方案
确保 web/WEB-INF/dataAccessContext-hibernate.xml 中 设置了 update

这个属性的具体含义,我在前面的系列中已经讲过了,在产品初始化,你就可以安全地把它注释掉。

3. 在 NetBeans 右击工程 Run project
这一步将所有的数据库表创建出来, 只是没有数据。

4. 加裁数据
使用 NetBeans Sql Editor
4.1 注册mysql 驱动
Runtime | DataBases 右击 -> new Driver
Add ... -> 导航到工程WEB-INF/lib/下的 mysql-connector-java-3.1.12-bin 点 OK

4.2 创建连接
右击刚注册的驱动 MySql (Connector/J driver) -> Connect using ....
Database URL: jdbc:mysql://localhost:3306/hjpetstore?useUnicode=true&characterEncoding=UTF-8
user name: hjpetstore
password: hjpetstore

点 Ok 后,在Databases 下应该会出现一个新的连接。

4.3 执行 SQL 脚本
1. 在 Files 窗口中导航到 db/mhsql/jpetstore-mysql-dataload.sql 并双击打开它
2. 在编辑器的工具条中 Connection: 选择 刚创建的数据库连接:jdbc:mysql://localhost:3306/hjpetstore?useUnicode=true&amp;amp;amp;amp;amp;amp;amp;amp;ampamp;characterEncoding=UTF-8
3. 点击编辑器工具条上,紧挨着下拉框的 run sql

确保没有显示错误信息。


Oracle 9i, 10g, HsqlDB, Postgres 及其它


数据库脚本都已经在工程中了,你所要做的就是利用这些数据库提供的工具创建一个用户 'hjpetstore',
之后的步骤与上述相同。


应用服务器初始化

本地数据源(非JNDI 数据源)


Tomcat 5.x,
事实上,工程默认是使用 Tomcat 服务器的,所以现在你根本不需要改动什么就可以运行工程了。
有关数据源的配置是在 web/META-INF/ context.xml 文件中

Sun Application Server8.x, 9.x / GlassFish 1.x, 2.x
同样的配置,只不过要生成一个 sun-web.xml 文件,
很好,NetBeans 会帮你自动产生,如下:
右击工程 -> Run | Server: 选择注册的Sun App Server (如果你还没注册 Sun App Server 的话,你需要先注册一下,具体步骤见相关文档)

此时,文件已经产生,右击工程 -> Run Project

Jetty 6.1.3
所有的配置文件已经在 WEB-INF 下了: jetty-web.xml, jetty-env.xml, 所以要做的只剩下将dist 上下生成的 hibernateJpetstore.war
放到 Jetty 的部署目录,还好这个目录跟 Tomcat 的目录同名叫 webapps

在 Jetty 目录下运行:
java -jar start.jar
然后在浏览器中请求: http://localhost:8080/hjpetstore/

JNDI 数据源


使用JNDI数据源当然是为了使用其 JTA(包容器管理的事务及其数据库连接池的实现),
只需要按正确的名称 jdbc/hjpetstore 在管理界面配好数据库连接池和相应的数据源,运行起来还是挺方便的,

Sun Application Server8.x, 9.x / GlassFish 1.x, 2.x
1. 首先按照这篇文章介绍的步骤正确配置 mysql 数据源连接池
(中文) http://pprun.blogspot.com/2007/05/glassfishsun-app-server.html
(English) http://enpprun.blogspot.com/2007/05/problem-in-setting-mysql-xa-datasource.html
注意,我文章中介绍的是使用root/root 作为用户名/密码,此时可以设置成hjpetstore/hjpetstore

2. 配置数据源
在应用服务器的 管理 界面 导航Resources | JDBC | JDBC Resources
点击右边主页面中的 new 后进入配置页面,填入:
JNDI Name: jdbc/hjpetstore
Pool Name: 选择前面配置的数据源:连接池:mysql
完成后点击 Ok


JBoss 4.0.4 +

1. 使用 JBoss 也许是冲着所谓的 #1 应用服务器而来的吧,但其配置有一些变化:
第一它实现了自己的一套日志方式,所以需要把 web.xml 中的

org.springframework.web.util.Log4jConfigListener

注释掉。

2. 它的数据源的配法也不相同,只需要将相应的数据库的配置文件(如:mysql-ds.xml ,内容见随后)放到
jboss-4.0.4.GA\server\default\deploy 目录下,
再在 jboss-4.0.4.GA\server\default\conf\login-config.xml 中加入:
    <application-policy name = "MySqlDbRealm">
<authentication>
<login-module code
= "org.jboss.resource.security.ConfiguredIdentityLoginModule" flag =
"required">
<module-option name ="principal">hjpetstore</module-option>
<module-option name ="userName">hjpetstore</module-option>
<module-option name ="password">hjpetstore</module-option>

<module-option name
="managedConnectionFactoryName">jboss.jca:service=LocalTxCM,name=hjpetstore-mysql</module-option>
</login-module>
</authentication>
</application-policy>
mysql-ds.xml 相应的内容如下:
    <?xml version="1.0" encoding="UTF-8"?>
<datasources>
<local-tx-datasource>

<jndi-name>hjpetstore-mysql</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/hjpetstore</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>hjpetstore</user-name>
<password>hjpetstore</password>
<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>

<!-- should only be used on drivers after 3.22.1 with "ping" support

<valid-connection-checker-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLValidConnectionChecker</valid-connection-checker-class-name>
-->
<!-- sql to call when
connection is created

<new-connection-sql>some arbitrary sql</new-connection-sql>
-->
<!-- sql to call on an
existing pooled connection when it is obtained from pool -
MySQLValidConnectionChecker is preferred for newer drivers

<check-valid-connection-sql>some arbitrary
sql</check-valid-connection-sql>

-->

<!-- corresponding
type-mapping in the standardjbosscmp-jdbc.xml (optional for ejb) -->
<metadata>

<type-mapping>mySQL</type-mapping>
</metadata>
</local-tx-datasource>
</datasources>


3. 还有,就是JBoss 的 JNDI 的名称有些怪:
    <bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">

<!-- JBoss
-->

<property name="jndiName" value="java:/hjpetstore-mysql">

<!-- other standard Java EE server

<property name="jndiName"
value="java:comp/env/jdbc/hjpetstore">
-->
</bean>


看到区别了吗?它只需要java:/hjpetstore-mysql


Jetty 6.1.3
目前,还未测试成功!


部署并运行


在 NetBeans 中,只需要在 工程属性中选定所要运行的 服务器后,点 Run Project 即可运行在本地数据源配置上。
此外,还可以按照服务指定的自动部署目录,将生成的 dist/hibernateJpetstore.war 文件放到该目录,
如果服务器已经运行,一切就 ok 了,如果没有,启动服务器即可。

如果要运行在 JNDI 配置上,则需要改一下 web.xml :

<!--
- Location of the XML file that defines the root
application context.
- Applied by ContextLoaderServlet.
-->
<context-param>

<param-name>contextConfigLocation</param-name>

<!-- local datasource -->
<param-value>
/WEB-INF/dataAccessContext-hibernate.xml
/WEB-INF/applicationContext.xml
</param-value>
<!-- jndi datasource and JTA (for a transactional
JNDI DataSource)

<param-value>

/WEB-INF/dataAccessContext-hibernate-jndi.xml
/WEB-INF/applicationContext.xml
</param-value>
-->
</context-param>

这几行配置说的应该很明白了,上面的是默认情况下的本地数据源,如果使用JNDI数据源,是这样了:

<context-param>


<param-name>contextConfigLocation</param-name>



<!-- jndi datasource and JTA (for a
transactional JNDI DataSource) -->

<param-value>


/WEB-INF/dataAccessContext-hibernate-jndi.xml
/WEB-INF/applicationContext.xml

</param-value>



</context-param>


只要按照上述的步骤配置好了应用服务器的数据源,现在运行的效果应该跟本地数据源是一样的。


总结


NetBeans 对于 Java EE 的开发是全面的,除了几个服务器还未集成进来之外,其它的功能已经走在了所有IDE的最前列,
但这也不防碍开发者使用这些未集成的服务器,因为大部分服务器都支持热部署,当NetBeans 给你的工程生成了 WAR 文件后,
剩下的就是“将它放入热部署目录”了。


看运行在 Jetty6.1.3 上的效果 (注意脚注部分显示,当前运行在什么服务器上):

2007年8月10日星期五

Java 7 语言级的改动

到目前为止,Java7 已经有如下议案:

  • Language-level XML support (语言级的XML支持)

  • Closures (闭包,目前 Java 匿名类担任部分这一角色)

  • Block constructs (块结构)

  • Strings in switch statements (允许 String 作为 switch 语句的开关值)

  • Language support for BigDecimal (语言级支持 BigDecimal, 目的是减小 double 不够用的压力)

  • Java property support (语言级属性支持,目前我们熟悉了 getter/setter)

  • Lightweight method references (轻量级方法引用,目前如果不通过反射是无法完成对方法引用对象的传递)

  • Extensions to the annotation mechanisms (进一步扩充 annotation - 元数据机制)

  • Java Module System (模块系统,解决头疼的 CLASSPATH 问题)


其实我个人关心的是: reified generics (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5098163)
如果这个未实现的话,设计generics (泛型)类(注意,我没说使用泛型)仍然会是一种痛苦。

相信随着时间的推移,各种介绍会陆续出现,这些概念也慢慢会一目了然。

详细信息见 javac 作者的 blog:
Neal Gafter: http://gafter.blogspot.com/
Peter Ahé: http://blogs.sun.com/ahe/
因为 JDK 已经成为了 OpenJDK 了,所以以上两位作者事实上都已经离开了SUN,
开发 javac 的责任留给了 java 社团,也许你就是其中一位了!

2007年7月6日星期五

小心 Java SE 6 U2

Java SE 6 U2 修复了不少关键的BUG,正因为此,我也迫不急待地更新了。
但是,我发现文件选择对话框(JFilechooser)简直慢如蜗牛了。
请参见这里的讨论:javalobby !

由于个人习惯,在新版本到来后,就会把老版本删除掉。可这次,我简直太无奈了,
java.sun.com 竟然也找不到 U1 的下载链接了,而笔记本又没带回来!
我不得 Google 一番才在国内的下载网站找到了。

我使用的操作系统 WindowsXp.

小心,各位!

2007年7月2日星期一

Hibernate 处理大数据量的方案

问题:OutOfMemoryException


大家知道,Hibernate 有 一级 cache (Session 级) 和二级 cache (需另外配置,如 ehcache),
以下代码,Hibernate 在处理到大约50000条记录时,就会抛出 OutOfMemoryException, 这是因为,Hibernate 把所有新建的 MiniMessage 对象都放在了 Session 级的缓存中了。

Session session = null;
Transaction tx = null;
try {
session = HibernateUtil.getSessionFactory().openSession();
tx = session.beginTransaction();
for(int i=0; i<300000; i++ ) {
System.out.println(i + ".................");
MiniMessage message = new MiniMessage("Hello World" + i);
session.save(message);

}

tx.commit();

} catch (HibernateException he) {
tx.rollback();
throw he;
} finally {
session.close();
}

解决办法:


使用"批处理”(Batch process)
Session session = null;

try {
session = HibernateUtil.getSessionFactory().getCurrentSession();
Transaction tx = session.beginTransaction();

for(int i=0; i<200000; i++ ) {
log.debug(i + ".................");

MiniMessage message = new MiniMessage("Hello World" + (i+1));
session.save(message);
if ( i % 100 == 0 ) {
//100, same as the JDBC batch size set in xml file:
// <property name="hibernate.jdbc.batch_size">100</property>
//flush a batch of inserts and release memory:
log.debug("fulsh at : " + i + ".................");

session.flush();
session.clear();
}
}


session.getTransaction().commit();
} catch (HibernateException he) {

session.getTransaction().rollback();

throw he;
}


在这种情况下,需要在hibernate.cfg.xml 配置几个参数来达到更好的效果:

1. 配置批处理的大小


 <property name="hibernate.jdbc.batch_size">100</property> 

2. 放弃二级缓存:



<!-- Disable the second-level cache because the
batch process is one-off process. -->

<property
name="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</property>


<property
name="hibernate.cache.use_second_level_cache">false</property>

<property
name="hibernate.cache.use_query_cache">false</property>

<property
name="hibernate.cache.use_minimal_puts">false</property>

这样,Hibernate 会在每 100 个插入后,与数据库同步一次,并将一级缓存中的实体对象清除。

2007年6月7日星期四

Java 安全拷贝协议 (JSCP: Java Secure Copy Protocol) NetBeans 插件

JSCP NetBeans 插件的作用


大家知道,SCP 广泛使用于SSH出现之前的 Unix 之类的平台上,它允许在 Client <-> Server 间进行双向的文件传输(ScpTo, ScpFrom)

JSCP NetBeans plugin 作为一个 TopComponet 插入到 NetBeans 的 Navigator 方位,通过 Tools | Java SCP 调用。

SCP 可进行文件双向传输的
  • 向支持 SCP 的 Unix/Linux 服务器上传文件(ScpTo)
  • 从支持SCP的 Unix/Linux 服务器获得文件(ScpFrom)

这两种工作模式是分别作为 JTabbedPane 的 两个 Tab 出现在 JScp 这个 TopComponent

JSCP NetBeans 插件的使用方法

从NetBeans PluginPortal 网站上获得一个压缩包,然后解压到一个目录供下面的步骤使用。

安装 .NBM 文件

  1. Tools | Update Center
  2. 选择 Install Manually Downloaded Modules (.nbm Files) 后,点击 Next
  3. 点击 Add... , 在 Select Directory or .nbm Files 对话框中,导航到此插件的两个 .nbm 文件(com-jcraft-jsch.nbm 和 org.pprun-jscp.nbm),同时选中它们后点击 Ok
  4. 点击 Next
  5. 点击 Next
  6. 点击 Next, 在View Certificates and Install Modules 界面点击 Include 列下面的多选框中打上勾。界面将出现版权及插件签名信息。(如果你希望使用计算机的所有用户都使用这个插件,可以将在 Global 列下打勾)
  7. Finish, 不出意外,将显示插件更新界面。
  8. 等到NetBeans 的状态条中显示 Turing on modules... done. 后,点击 Tools 菜单,此时将在菜单最底端看到 Java SCP 菜单项,如下:


使用说明

前提条件:

  • 保证网络可以访问到一台支持 SCP/ SSH1 的 Unix/Linux 服务器
  • 保证具有以上服务器上的一个帐户并且对其中的一个目录具有“写”权限(如果你只使用 ScpFrom 的话,此项可选)

ScpTo (文件上传)


(如果还没打开 JScp Window 的话)通过 Tools | Java SCP 打开,它会出现在左下角并停靠在 Navigator 所在的窗口中,如下图所示:

  1. LocalFile 上传的文件,通过右边的按钮来选择
  2. User@Host 用户名和主机名(或IP地址) 的组合
  3. Password 上述用户的密码
  4. RemoteDir 上传的文件在服务器上放置的目录


请注意在输入的过程中,会动态对输入域的值进行校验,如下,桔色的字显示没有指定服务器主机名(或IP地址):



如果所有的输入都合法的话,按钮 Scp 将可用,点击它将进行网络传输,进度条指示这一过程:



如果一切正常,最终进度条将停止指示。反之,如果后台操作出现错误的话,错误将显示:




ScpFrom (文件下载)


(如果还没打开 JScp Window 的话)通过 Tools | Java SCP 打开,它会出现在左下角并停靠在 Navigator 所在的窗口中,如下图所示:

  1. User@Host 用户名和主机名(或IP地址) 的组合
  2. Password 上述用户的密码
  3. RemoteFile 要下载的服务器上的文件
  4. LocalDir 下载的文件放置的目录,通过右边的按钮来选择




如果所有的输入都合法的话,按钮 Scp 将可用,点击它将进行网络传输。


总结


自从 NetBeans 5.0 开始,编写基于 NetBeans 的插件或平台应用已经变得非常简单。对于新来者,最大的障碍无非是一些NetBeans专用的术语及早期遗留下来的几个不大好理解的概念。不过还好, NetBeans 自己在快速前进的同时并没有忘记为开发者提供便利。
NetBeans wiki 是各种信息的大轮盘
planetnetbeans 则是全世界NetBeans开发者的乐园。大家为了 NetBeans 开怀畅谈。
Geertjan's Weblog 不得不看

2007年6月1日星期五

试用GlassFish V2,赢取 52 吋液晶电视机



只要下载并试用 GlassFishSun Java System Application Server 9.1 Beta 2 ( Java EE 5 SDK Update 3 Preview 2 或 Java Application Platform SDK Update 3 Preview 2) 并将使用效果反馈给SUN(通过BLOG或直接提交表单),就有机会羸取上面的52英吋液晶电视机。 

详细情况见,规则见.

2007年5月25日星期五

软件的脆弱:从二分法查找的BUG说开去

大概是在去年的七月份左右,我首先在 javalobby 上看到这篇。当时也无比震惊,因为JDK (1.5及之前的版本) 中 Arrays.binarySearch(int[] a, int key) 及 Collections.binarySearch(int[] a, int key) (其调用indexedBinarySearch) 存在一个在业界隐藏了几十年的BUG,而这两个方法的作者恰恰是两位高人实现:

* @author Josh Bloch
* @author Neal Gafter

我想在Java 领域呆的比较长的开发者应该会有所耳闻吧,Josh Bloch 被称之为 "Java 之母"(虽然他是一位男性),因为 java collection 框架,java.math, 泛型 及《Effective Java Programming Language Guide》都出自他之手;而 Neal Gafter 则是 我们每天都用的JAVA编译器 Javac 的实现者。


我们先看有问题的代码:

public static int binarySearch(int[] a, int key) {
int low = 0;
int high = a.length-1;

while (low <= high) {
int mid = (low + high) >> 1;
int midVal = a[mid];

if (midVal < low =" mid"> key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}

有问题的代码是这一行:
int mid = (low + high) >> 1; 

大家知道,其等价于:
int mid =(low + high) / 2;

问题是在 low 和 high 都很大时,比如数组的元素达到 2^30 时,low + high 将超过整数的最大值 2^31 -1,此时将造成溢流,溢流后得到的 mid 为负值。

正确的实现应该为:
             int mid = low + ((high - low) / 2);

或者更加清楚地使用Java的无符号右移操作符:
            int mid = (low + high) >>> 1;

虽然这个问题目前得到解决,我们就能断定这十几行程序就准确无误吗?
但连以上两位作者目前也还表示怀疑。
从行内得知,第一个二分法算法是1946年出现的,而当时被认为“无误”的实现到1962年才出现(也就是说以上十几行代码是经过十几年才得到的)。因为当时的数据量不可能逼近 2 ^ 30的数量级,所以直到去年这个BUG被提交到 Java 的 Bug 库中。可想人类的思维是有缺陷的。

目前,对于搜索引擎,基因工程领域,这一数量级应该是少见多怪了,所以如果你工作的领域需要处理大量的数据时,请使用 JDK 6.0

顺便提一句,对于C的实现,可以采用如下实现:
mid = ((unsigned) (low + high)) >> 1;
看到这样的情况,作为程序员,我们应该时刻警惕、保持低调!

2007年5月23日星期三

SwingWorker for you

想必大家已经知道 SwingWorker 已经加入到了 javax.swing 包中了。它的前身经过好几个阶段改进的,如果你阅读网上的例子就会发现你阅读的例子跟你下载的包不兼容。
SwingWorker 在被正式加入到JDK6中之前叫做:org.jdesktop.swingworker.SwingWorker.java, 除了这个类之外,还包括 org.jdesktop.swingworker.AccumulativeRunnable.java 和 org.jdesktop.swingworker.SwingPropertyChangeSupport.java. 我并不打算深入介绍这几个类的源码,而是利用实际的例子来描述 SwingWorker 给 GUI 程序带来的便利。

如果写过GUI程序的开发者肯定对界面的响应度及界面冻结会有所了解。首先我得说明的是,这个问题并不是SWING特有的,所有的GUI框架如果没有处理 好都会存在这种问题,举例来说,
1. 在网络不好的环境下,在 windows 的文件浏览器中请求一个FTP地址或任何服务器的地址时,我们可以看到“灰块”(所谓灰块,是由于界面元素刷新队列被某个长时间的任务给阻塞,造成本该立 即刷新的界面得不到处理,显示出来的效果就是一块被扯得扭曲了的区域。)
2. 用过 PLSQL Developer 的开发者肯定也体会过屏幕冻结的感受吧。


费话少说,进入我们的 SwingWorker 之旅。

SwingWorker有几个重要的概念:
  1. 初始线程(Initial threads) 一般来讲是应用的主线程 (运行main 方法的那个线程) 
  2. 工作者线程(Worker Thread) 在主线程中生成的,用于执行那些长时间操作的线程
  3. 事件调度线程(Event Dispath Thread) 所有界面相关的行为都应该在这个线程进行,并且一定要保持快速的响应。
如下图是利用 NetBeans Profile 监控到线程:
  • main - 初始线程
  • Our Swingworker #1 - 工作者线程
  • AWT-Envent-Queue-0 事件调度线程
  • 其它的线程不在我们感兴趣之列




我们要记住的是:
  • 所有长时间的操作都不应该放在事件调度线程(EDT-Event Dispatch Thread,专门用来处理与界面响应相关的操作)中,否则界面在这段时间内将变得无法响应。
  • 所有对SWING组件的更新(在其已经被显示出去后)都应当通过EDT来访问,否则有可能造成线程死锁或界面根本没有反映作出的更改。
不要被前面这几段弄晕了,只要弄清楚了 Swing 的线程体系,其实大部分工作JDK 已经为你做好了。我们所要做的就是按照范例实现我们的代码。这样就肯定安全可靠。


我们的例子是一个登录界面,这个界面要求输入用户名与密码,然后点击登录,正常情况下将与中心数据库进行通信。但考虑到文章的长度,我们将使用 Thread.sleep 来演示和长时间的网络操作。

最终的界面将如下所示,我们这里也不介绍怎样制作界面(是的,我的确是使用 NetBeans 的 Gui Builder 来制作的,但使用了一个自定义的 JImagePanel)




如果阅读过别人的代码或自己认真写过SWING方面的代码,在没有使用 SwingWorker 的 GUI 工程中,一定会有如下代码:

java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
// 长时间的操作
}
});


这样做的目的就是为了使长时间的操作在另外的线程中运行。

为了使用面象对象的方式来处理这种行为,我们现在使用 SwingWorker。下面是我们的例子的代码:


import org.jdesktop.swingworker.SwingWorker;
import org.pprun.interviewofprologic.db.domain.UserBusinessDelegate;

/**
* Worker thread for login operation.
* @author pprun
*/
public class LoginSwingWorker extends SwingWorker {
private String username;
private char[] password;

private Exception exception;

/** Creates a new instance of LoginSwingWorker */
public LoginSwingWorker(String aUserName, char[] aPassword) {
this.username = aUserName;
this.password = aPassword;
}

/**
* 所有的后台操作都在这里,如果要动态将处理的数据发出去的话(比如数据库查询的应用),
* 可以在这里调用 publish 方法.
*/
@Override
protected Boolean doInBackground() throws Exception {
try {
// 我们注释掉了这段代码,取而代之以假想的 Thread.sleep
// UserBusinessDelegate cbd = UserBusinessDelegate.getInstance();
// final boolean result = cbd.login(username, password);

//return result;

try {
Thread.sleep(5000);

} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}

return true; // 如果想看看登录失败时的效果,return false;

} catch( Exception anyException ) {
exception = anyException;
throw exception;
}
}


/**
* Returns the exception thrown by the method doInBackground, if any, or
* null if no exception was generated.
* @return The exception generated by the call to doInBackground, or null
* if no exception was generated.
*/
public Exception getDoInBackgroundException() {
return exception;
}
}



在触发端,点击 Ok 按钮时:


private void okJButtonActionPerformed(java.awt.event.ActionEvent evt) {
// ...

loginSwingworker = new LoginSwingWorker(username, password);

loginSwingworker.addPropertyChangeListener(this);
loginSwingworker.execute();

// ...
}



属性改变监听器的实现方法,监听在 SwingWorker 中的改变:


public void propertyChange(PropertyChangeEvent pce) {
if (pce.getSource() == loginSwingworker) {
// property change event coming from the loginSwingworker
if (pce.getPropertyName().equals("state") &&
loginSwingworker.getState() == SwingWorker.StateValue.DONE ) {
// loginSwingWorker 完成,但有可能抛出异常
loginSwingworker.removePropertyChangeListener( this );
if (loginSwingworker.getDoInBackgroundException() != null ) {
// 抛出异常
if (true) {
infoJLabel.setText("Error in login!");
} else {

infoJLabel.setText("Error in login!");
}
}

} else if (loginSwingworker.isCancelled()) {
// loginSwingowrker 被取消时的代码
}
}



关于在 Swing 中使用并发的详细资料,参见 Concurrency in Swing

2007年5月22日星期二

体验更多 NetBeans 的新功能

不喜欢 NetBeans 的开发者往往指出 NetBeans 没有这个功能,没有那个功能。无可厚非,当时他们是对的,但随着时间的推移,现在也许错了:

1. Last Edit (是近更改按钮,带星号的那个),将你带到最近更改的地方




2. Diff SideBar (差异侧条), 根据所在行代码是增加、更改还是删除,在侧条中显示不同的小条,右击可以使用进一步的功能



3. JUnit4 支持(也就是现在可以使用基于 JDK 5 Annotation 的单元测试了)




4. Find/Replace in Project (全工程范围内搜索),是的,这个功能我真的也非常需要:
看到左下方的"Replace" 按钮了吗?
值得注意的是,这个功能在 M9 中被屏蔽了,但在每日构建的版本中可以使用。

[nb-find-replace.png]



另外,大部分初次使用NetBeans 的开发者,不知道更改“自动完成弹出窗口”的键绑定,因为最常用的"Ctrl + 空格" 是不可工作的,因为在中文操作系统中被绑定到输入法的切换了。所以我一般把它改成 'Ctrl + Enter' , 如下进行:
  1. Tools | Options -> Keymap -> Other
  2. 找到 Show Code Completion Popup, 选中它,点击Add...
  3. 按下任何所你希望的键序列,但是如果直接按 Ctrl + Enter 的话,系统提示这个组合键已经被绑定到 Split Line,所以如果我们要使用这个组合键的话,要先把它与 Split Line 解除绑定
  4. 在 Show Code completion Popup 下方第六个即是 Split Line, 选中它,点击移除。你可以为这个功能提供另外的组合键,如果经常使用这一功能的话。
  5. 然后,按照上述把"Ctrl + Enter " 加到 Show Code Completion Popup 中去。

(期待下一篇)

2007年5月18日星期五

GlassFish/Sun App Server 配置 MySqlXADataSource 的问题

如果你打算使用 GlassFish/Sun App Server,并且打算使用 MySql 的 XA 数据源的话。在目前的配置过程中,会遇到如下问题:

重现步骤:
1. (如果之前没做这一步的话)将 Mysql 的 Connector/J 包 (如我的:mysql-connector-java-3.1.12-bin.jar)放入 GlassFish/Sun App Server 安装目录下子目录 \AppServer\lib\ 中

2. 启动 GlassFish/Sun App Server. 可通过 右击 Runtime | Servers | Sun App Server 选择 start

3. 启动后,右击 Sun App Server 选择 View Admin console

4. 登录WEB 管理后台

5. 在左侧导航器中 点击 Resources | JDBC | Connection Pool s,在表格的头部点击 New... 按钮

6. 在右侧中填入:
Name: MySql
Resource Type: javax.sql.XADataSource
Database Vendor: mysql

然后点击 next

7. 注意在 Datasource class name 中自动填入了: com.mysql.jdbc.jdbc2.optional.MysqlXaConnectionPoolDataSource,这个值是不对的。

你如果想试试的话,在最下面的 Properties 窗格中填入 :


点击 Finish

8. 在结果窗口中点击 Mysql

9. 在打开的页面中可以看到一个ping 按钮,点击是用来测试配置成功与否,点击一下,将出现如下错误:


解决的办法:
如果解开mysql-connector-java-3.1.12-bin.jar 文件,在包com.mysql.jdbc.jdbc2.optional 中可 看到:

com/mysql/jdbc/jdbc2/optional/
com/mysql/jdbc/jdbc2/optional/CallableStatementWrapper.class
com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.class
com/mysql/jdbc/jdbc2/optional/MysqlConnectionPoolDataSource.class
com/mysql/jdbc/jdbc2/optional/MysqlDataSource.class
com/mysql/jdbc/jdbc2/optional/MysqlDataSourceFactory.class
com/mysql/jdbc/jdbc2/optional/MysqlPooledConnection.class
com/mysql/jdbc/jdbc2/optional/MysqlXAConnection.class
com/mysql/jdbc/jdbc2/optional/MysqlXADataSource.class
com/mysql/jdbc/jdbc2/optional/MysqlXAException.class
com/mysql/jdbc/jdbc2/optional/MysqlXid.class
com/mysql/jdbc/jdbc2/optional/PreparedStatementWrapper.class
com/mysql/jdbc/jdbc2/optional/StatementWrapper.class
com/mysql/jdbc/jdbc2/optional/SuspendableXAConnection.class

...
其中并没有默认填入的 com.mysql.jdbc.jdbc2.optional.MysqlXaConnectionPoolDataSource, 但是有 com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource

在页面Application Server > Resources > JDBC > Connection Pools > MySql 中:
1.将 Datasource class name 的值改为: com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource

2.点击 Save 按钮

3. 点击 Ping 按钮,成功显示:

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

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

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