Česky   |  Deutsch   |  English   |  Español   |  Français   |  Indonesia   |  日本語   |  한글   |  Polski   |  Português (BR)   |  Türkçe   |  中文   |  正體中文   |  Your Language  
PlanetNetbeans
NetBeans星球是来自所有博客空间的NetBeans相关思索的集合。
APIDesign - Blogs - June 02, 2014 07:49 AM
Podcast related to Japanese Translation

--JaroslavTulach 07:49, 2 June 2014 (UTC)

BlogJava-Java桌面技术-随笔分类-NetBeans - March 19, 2014 05:55 AM
JDK 8和NetBeans 8.0正式版发布了

经过2年半的努力、屡次的延期和9个里程碑版本,甲骨文的Java开发团队终于发布了Java 8正式版本。
同时,作为Java首选IDE,NetBeans同时也发布了最新的8.0版本。
JDK下载:http://www.oracle.com/technetwork/java/javase/downloads/index.html
NetBeans下载:https://netbeans.org/downloads/

BlogJava-何以解忧?唯有Java - February 18, 2014 03:59 PM
JBPM 工作流设计插件 for NetBeans

今天和大家分享NetBeans 最新的插件 jBPMN :http://plugins.netbeans.org/plugin/50735/jbpmn  ,也可以在NetBeans 的插件中心中下载
工具->插件->可用插件   在列表中查找jBPMN这插件。





BlogJava-Java桌面技术-随笔分类-NetBeans - February 22, 2013 05:25 AM
NetBeans 7.3正式版发布了

摘要: 甲骨文公司正式推出NetBeans IDE 7.3。
对开发者来说,新版本除了改善了对于Java平台的支持,还引入了一系列HTML5、JavaScript和CSS开发功能,开发者可以快速、轻松地构建、调试富互联网应用和移动应用。   阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - July 24, 2012 03:05 PM
NetBeans 7.2正式版发布

摘要: 这次的发布有些低调,但是仍然值得一用  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - January 06, 2012 09:30 AM
重返Swing

摘要: 我又干起了老本行,Swing。这次回归没有以往对Swing的着迷,取而代之的是对Swing的反感... ...  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - January 05, 2012 02:22 PM
NetBeans7.1正式版发布

摘要: 本来去年发布,可惜延期到了今年1月。看NetBeans路线图,NetBeans7.1.1也不远了,7.2版本也将于6月底发布。期待吧  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - October 06, 2011 03:00 PM
缅怀乔布斯

摘要: 有三个苹果改变了世界。夏娃的苹果让人有了道德,牛顿的苹果让人有了科学,乔布斯的苹果让人有了生活  阅读全文

BlogJava-何以解忧?唯有Java - June 02, 2011 03:33 AM
KDE 环境下的BlogJava客户端

今天找到一个在KDE 环境下写博客的软件 Blogilo ,现在准备测试一下是否支持blogjava 发布博客(经测试可以发布)

好久没有写博客了,今天先发一篇如何使用Blogilo 发布BlogJava博客的文章,接下来工作没那么忙了,会开始写一些NetBeans RCP开发的入门文章。废话不多说了,接下来开始讲解如何使用Blogilo发布博客。

第一步: 先检查你的Linux发行版是否安装了kdepim,因为blogilo是和kdepim绑定发布的,目前主流的包含KDE桌面的发行版都默认安装kdepim,也就是说默认安装了Blogilo,不排除一些发行版精简kdepim,把blogilo移除了。我目前使用的是Fedora15 KDE 发行版。Blogilo 目前支持 Blogger 1.0 API,MetaWeblog API,MovableType API,Wordpress,Blogspot.com 这五种博客API,如下图,你可以选择适合自己的博客API,当然今天写的是适合BlogJava的MetaWeblog API。

第二步:配置Blogilo,打开Blogilo,然后开始配置,如下图:

接着在弹出的界面中,选择Blogs,如下图:

点击add 按钮,开始添加BlogJava 博客API 配置 ,在Blog/Homepage URL 填写的博客服务地址,例如我的博客:http://www.blogjava.net/gml520 然后在这个地址的后边再

添加 /services/metaweblog.aspx 。 完整的地址:http://www.blogjava.net/gml520/services/metaweblog.aspx。 你可以根据自己的实际情况将gml520 替换成自己的用户名。然后添加用户名和密码,接着点击Advanced 选项卡,在API 列表中选择 MetaWeblog API ,Blog ID 填写自己的用户名,最后点击OK 完成配置。

这里有几个地方需要注意: (1)在Basic 选项卡里不要点击 Auto-Configure,除非你的博客的API 不是 MetaWeblog 类型。(2)在Advanced 选项卡下输入完 Bolg ID 后不要点击 Fetch ID。否则会出错的,原因同(1)一样。

配置过程如下图:

第三步: 配置完成后,你就可以开始写博客啦,这篇博客就是用 Blogilo 客户端写的。:)

最终界面:

=-=-=-=-=
Powered by Blogilo



BlogJava-何以解忧?唯有Java - May 31, 2011 08:51 AM
我的NetBeans 黑色主题

发图不解释


BlogJava-Java桌面技术-随笔分类-NetBeans - April 21, 2011 01:54 AM
NetBeans7.0正式发布

摘要: 经过漫长的等待,无数次延期,NetBeans7.0终于发布了。这次带来了众多改进,如JDK7的支持、GlassFish3.1的支持、Maven3的支持等,都是开发者们盼望已久的功能。  阅读全文

BlogJava-何以解忧?唯有Java - March 30, 2011 01:42 AM
NetBeans 7.0 RC1 发布

NetBeans 7.0 RC1 终于发布了,这个版本的发布时间因为Oracle 对Sun的收购而一直推迟,不过推迟的好处是:通过了更多的质量测试,使得NetBeans7.0 变得更加的可靠,有兴趣尝试NetBeans 7.0 的朋友可以到官方网站上下载:http://dlc.sun.com.edgesuite.net/netbeans/7.0/rc1/

  

  下面是 NetBeans Dzone的一段介绍:
  
   NetBeans 7.0 RC1 is out! But the real news is all the effort made to make this a great quality release. The NetCat (NetBeans Community Acceptance Testing) team revamped its approach and the results are outstanding! The biggest improvement, IMO, is the introduction of Testing Tribes:


更多关于NetBeans 的新特性介绍可以看看NetBeans 的wiki http://wiki.netbeans.org/NewAndNoteworthyNB70



BlogJava-何以解忧?唯有Java - December 31, 2010 12:25 PM
NetBeans 任务提醒插件(Linux 平台)

今天在Dzone 上看到一篇文章介绍,NetBeans 使用 Growl 来实现 NetBeans 执行任务时的本地化方式提醒,感觉很不错。文章链接地址:
http://netbeans.dzone.com/nbnotify-netbeans-growl   
          

         联想到Linux上也有类似的提醒,而且操作很简单,于是就想是不是自己也写个类似的插件来供自己使用呢? 说干就干,按照流程 新建一个NetBeans 插件项目,然后再新建一个 Installer ,这个Installer 是在模块被载入的时候执行的。所以在这个Installer中 注册一个监听器,用来监听 NetBeans 的任务操作。监听到任务后,接下来的事情就好办了,只要判断任务状态就可以了,然后根据任务的状态发送提醒信息。
           在Linux的各个发行版中使用提醒命令最多的是 notify-send 这个命令,在Fedora 和Ubuntu 的Gnome 环境中都是使用这个命令。我的机器上同时安装 Fedora 14 (KDE桌面)和 Ubuntu10.10(Gnome桌面),所以我就在这两个发行版上测试。在测试的过程中,发现在Ubuntu 10.10 中notify-send 这个命令的 -t 参数竟然不起作用,-t 参数是用来控制提醒保留的时间的,但是在Ubuntu10.10中,无论你设不设置 -t 参数,他都是保持10秒后隐藏提醒,于是在Ubuntu10.10 下使用这个插件就很悲剧的不能及时的展现通知,只能10秒后再显示下一条通知,相反在Fedora14 KDE 中却工作良好。下面四张截图就是分别在Fedora  14 KDE 和Ubuntu10.10 Gnome桌面环境下的效果图:

           在Ubuntu10.10 Gnome 中当 NetBeans 对项目执行“ 生成 ”任务的开始 和 结束 时的提醒状态。
     
              

            在Fedora14 KDE 中当 NetBenas 对项目执行 “清理并生成” 任务的 开始 和 结束 时的提醒状态。


        


  怎么样,效果还不错吧,其实在KDE中还有一个更加强大的提醒工具,那就是 kdialog ,kdialog 不仅拥有 notify-send 的所有功能,同时还拥有更加强大的提示功能,比如对话框形式,密码输入等等,如果想要了解更多的kdialog 提醒功能,请使用 kdiglog --help 这个命令获取更多的参数了解。
目前这个插件的功能还比较弱,只能对执行任务时 的开始和结束状态 做出提醒,对于执行项目出错等状态还不能提醒,不过目前的这个提醒还凑和,至少你可以在编译 一个耗时比较长的项目的时候去做其他的事情,编译完成后,NetBeans 会自动的提醒你编译任务完成了(无论是否成功)。

BTW: 明天就是 新年--2011 年了,祝大家 新年快乐!万事如意!财源滚滚! 同时也祝福自己!

插件下载:NbLocalNotify   (Linux only,源码将在下一篇博客中公布)。



BlogJava-何以解忧?唯有Java - November 21, 2010 08:48 AM
NetBeans 7.0 Beta 发布

摘要: NetBeans 最新版本NetBeans 7.0 Beta 发布了,想要体验新版本的童鞋们可以去官方网站上下载了。
http://dlc.sun.com.edgesuite.net/netbeans/7.0/beta/  阅读全文

BlogJava-何以解忧?唯有Java - October 14, 2010 02:00 AM
JavaFX Script 的替代品 Visage

    在今年的JavaOne 上Oracle 宣布停止JavaFX 脚本语言的开发,转而用Java API实现JavaFX 的功能,就我个人而言,我更倾向于JavaFX Script, JavaFX Script 当初使用自己的语言而不是使用Java 语言,就是为了减少 程序员的负担,以更简洁更人性化的语法公诸于世。然而Oracle 却放弃了JavaFX Script。 于是就有JavaFX Script 的爱好者开发了 Visage语言来延续JavaFX ,虽然名字不一样,但是语法却是一样的。 他的 项目主页 :http://code.google.com/p/visage/  

    原先用JavaFX 语言开发的应用可以继续开发了,不用再因为Oracle 放弃JavaFX而苦恼了,因为有开源的Visage,呵呵!有兴趣的朋友可以去 Visage 的项目主页上看看。


BlogJava-Java桌面技术-随笔分类-NetBeans - August 19, 2010 02:05 AM
NetBeans6.10发布计划

NetBeans下一个版本是6.10,NetBeans小组正在紧锣密鼓地进行了。http://wiki.netbeans.org/NetBeans_610?intcmp=925655

按照计划,6.10将于明年1月发布。



BlogJava-Java桌面技术-随笔分类-NetBeans - June 17, 2010 01:50 AM
NetBeans6.9正式发布

摘要: 今天一早来到公司,习惯性地打开www.netbeans.org发现期待已久的6.9版本正式发布。这是一个里程碑的版本,首次正式加入对OSGI的支持。其他编辑特性也明显增强  阅读全文

BlogJava-何以解忧?唯有Java - May 13, 2010 04:43 AM
发布一个 NetBeans Java ByteCode Viewer 插件

摘要: 昨天在JavaEye论坛上又一位仁兄用Jclasslib 查看 Java class byte code,http://www.javaeye.com/topic/663117#1489190
于是去下载 Jclasslib ,发现官方网站上在2005年以后就没有更新了,他的NetBeans 插件也没有更新,对于新版的NetBeans 支持不了,于是就萌生了重新开发 Jclasslib for NetBeans 插件,于是就有了今天的这篇博文,呵呵!
话不多说,先上图,正所谓一图胜千言,可能有些朋友会觉得使用Java 默认的界面比较丑陋,但是我觉得还行,哈哈!主要是因为这个外观的的Java运行速度比较快,Linux 下用这个外观很实用。
  阅读全文

BlogJava-何以解忧?唯有Java - April 16, 2010 05:22 AM
NetBeans6.9 开发版的几个新功能


这里只列举几个新特性:

1、内置 shell 控制台


本地,和远程的shell 视图:



2、 拼写检查


注释中的拼写检查



版本控制中的拼写检查


3、 链接地址显示



4、 Line wrap



效果图:



更多的功能请看http://wiki.netbeans.org/NewAndNoteWorthy





BlogJava-何以解忧?唯有Java - March 30, 2010 02:00 AM
解决ubuntu10.04 Firefox3.6 Java浏览器插件不工作的问题

这几天忍不住尝试了一下ubuntu10.04,感觉还不错。默认的我的显卡驱动就可以开3D 了,我的是A卡。
但是今天在安装java 浏览器插件的时候死活的都装不上,firefox 的插件列表里就是没有jre 插件,于是开始
google+baidu  试了很多种的方法,走了许多的弯路都没有解决,最后在
https://bugs.launchpad.net/ubuntu/+source/sun-java6/+bug/532174   这里找到的解决办法:

sudo update-alternatives --install /usr/lib/mozilla/plugins/mozilla-javaplugin.so mozilla-javaplugin.so /usr/lib/jvm/java-6-sun/jre/lib/i386/libnpjp2.so 1

只要在命令行下输入这条命令就可以了。前提是你已经安装好jre 的浏览器插件。



BlogJava-Java桌面技术-随笔分类-NetBeans - January 14, 2010 01:31 AM
JDK6 update18发布

摘要: JDK6 update18发布了,这次更新包含了许多值得关注的新特性以及性能的提升  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - December 11, 2009 01:55 AM
NetBeans6.8发布了

摘要: NetBeans6.8正式发布,同时符合Java EE6规范的GlassFishV3也放出了正式版本。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - July 17, 2009 05:55 AM
期待NetBeans6.8的发布

摘要: 性能问题一向是历代NetBeans发布时的关注焦点。和发布6.7之前的情形一样,6.8版本包含很多值得自豪的改进。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - May 29, 2009 10:54 AM
JDK 1.6.0_14 发布了

期待已久的JDK 1.6.0_14 发布了。http://java.sun.com/javase/6/webnotes/6u14.html
本次更新是继Update 10以来最大的一次更新,包含了众多新特性和bug的修复,喜欢尝鲜的开发人员可以试试最新版本的JDK带来的益处。

需要关注的是JRE的安装界面,Sun标志的背景改为了红色,但愿Oracle能使Java更加辉煌吧。



Practical API Design - May 18, 2009 05:43 PM
API Podcast #2: Reentrancy

Listen to podcast #2: to learn about our take on Swing and its poor reentrancy. Find out what it may mean for your own API design and especially Runtime_Aspects_of_APIs that you create. Learn to fight with that problem by maximizing the declarative nature of your API. --JaroslavTulach 17:43, 18 May 2009 (UTC)

Practical API Design - May 12, 2009 07:50 PM
API PodCast #1

Listen to this: ! It is almost a year since we (me and Geertjan) started our regular API Design Tips podcasts. They used to be part of larger NetBeans podcasts, however recently I needed some promotion material for TheAPIBook and I decided to extract the API Tip parts. I am glad I can offer these sketches to you. Enjoy podcast #1. --JaroslavTulach 19:50, 12 May 2009 (UTC)

Practical API Design - December 12, 2008 09:06 AM
2009: The Year of Annotations

As I noted recently, I see the year 2009 as the year of annotations. The NetBeans project is about to rely on them more heavily. Finally! We've been waiting for that for ages, but finally we can compile with JDK 1.6 JavaC and we can use compile time annotation processors. As a result we can replace our layer based registrations with annotations and benefit from compile type checking, code completion, from having the registrations in the same place as the code that is being registered, etc. Also we can offer our API users simple looking annotations and let associated annotation processors do more advanced and more effective processing. As a result the developers have simple API to deal with, while actual registration hidden behind can be as effective as possible, even at the cost of complexity, but without compromises to reliability (as the complexity is kept in the processing infrastructure, not exposed to API users).

The other project related to annotations that we are likely to incorporate during 2009 is our extended use of Annotations for Software Defect Detection. This is heavily based on the JSR 305, yet until it is stable we do not want to expose such unstable API to users of our stable APIs (more on that in Chapter 10, in section Beware of Using Other APIs). As such we are going to create our own annotations (still recognizable by FindBugs and co.). The hope is that our annotation will stay compatible even if the underlaying JSR 305 slightly changes. Please find our current patch and comment here or in the issue 137437.

Last project that deals with annotations is developed by our editor hints guru Jan Lahoda - its aim is to bring complex refactoring to masses! How? Why? We have observed that using @Deprecated annotation is good hint to help your API users recognize that some part of your API is obsolete and shall no longer be used, however that in no way helps users of your API with converting their code to new, non-deprecated style. We have a solution: Use Code Transformation Annotations! Dear [API] writers, let's adopt these annotations and use them in your API! They are completely standalone (read more), lightweight and we are ready to incorporate feedback of everyone interested in the project. Indeed, my plan is to bring these easy to use and flexible refactorings to NetBeans soon, hopefully for version 7.0.

So these are my three annotation related projects. I find them quite exciting and I cannot wait to see them being used. Annotations are here to simplify life of API users and developers. As soon as we have them, we will have full right to call the year 2009 the year of annotations!

Listen to our podcast or download it.

Name (required):

Comment:

--JaroslavTulach 09:06, 12 December 2008 (UTC)

平步星云 - December 10, 2008 05:08 PM
Mark Occurrences in NetBeans

Mark Occurrences (元素高亮)指的是当鼠标放在某个类元素上时,在当前文件高亮出该元素的 声明和引用。
如果你使用过 Find Usages 的话,你应该能知道这个功能的好处。但是 Mark Occurrences 比起 Find Usages 来属于轻量级的。因为:

  1. Mark Occurrences 只搜索当前文件
  2. Mark Occurrences 为动态的,不需要菜单来激活只需要将鼠标放在要查看的元素上即可。
  3. Mark Occurrences 懂得语义,比如将鼠标指向当前类的超类,它将显示所有被实现/覆盖的方法;放在方法的返回类型上,将显示方法的所有返回的语句上;...
如果你使用的是 NetBeans 6.0 的开发版的话,这个功能已经内置了,不过它被叫做 Highlights:

  • 成员作用域


  • 方法返回点

  • 特定异常抛出点




如果你使用的是 NetBeans 5.x 系列的话,你需要注册一个更新中心,然后下载插件,具体步骤如下:
  1. 选择 Tools | Options
  2. 单击 Advanced Options 按钮
  3. 选择 Options | IDE Configuration | System | Autoupdate Types
  4. 右击并选择 New | General Update Center
  5. Name 输入域中输入Sandip Chitale's Modules 然后单击 Finish
  6. (此步骤不必,如果你在完成上述步骤后立即进行下面的步骤时) 展开Autoupdate Types node, 选择 Sandip Chitale's Modules
  7. 在右边的窗口中的第一个属性值Server URL,输入: http://blogs.sun.com/roller/resources/scblog/update-center.xml
  8. 确保Enabled 属性勾上。然后关闭当前窗口

现在这个更新中心就可以用了,我们目标是得到 Mark Occurrences 插件,它就在这个更新中心中:
选择Tools | Update Center 菜单. 勾上 Sandip Chitale's Modules 项,如果它还没被勾上的话。依照更新向导在 Select Modules to Install 窗口中选择
Sandip Chitale's Modules 选择Make Occurrences

虽然作者声明:这个模块是个实验性的模块,但是到目前为止在我的工作中我一直在用它,而且没有出现任何问题(比起一些 Eclipse 的插件可强多了!)


注意: 安装完后(不记得要不要重启 NetBeans), 你应该能看到工具条上出现一个黄色的按钮(如下图),或者你可以从 View | Mark Occurrences 激活它:


平步星云 - December 10, 2008 05:08 PM
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. 判断输入数据的合法性

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

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

平步星云 - December 10, 2008 05:08 PM
NetBeans 6.0 抢先试用

NetBeans 6.0 M9 (Preview) 已经出来好些天了,还剩下最后一个里程碑就要进入测试阶段。

对我来说,自从从 Oracle JDeveloper 转向 NetBeans 以来,在工作中我一直坚持用它。也许是的我工作表现迫使远在美国那边的同事放弃了说服我使用 Eclipse 吧。你也许想知道,作为一名NetBeans 的老用户,我期待 6.0 的什么,虽然它已经很好了。

1.不要因为整个 Editor 的体系重组,而使强“稳定性”的口碑打折扣 (基本上大部分模块都重新Retouche了),因为我拒绝使用 Eclipse 的原因之一就是: JVM OutOfMemoryError.

2.Occurrences Highlight 我在之前的文章中写过关于这一功能


3.Javadoc and Declaration View 我已经等了好几年,我甚至曾经自己利用 NetBeans 的API 实现过一个类似的模块,但是因为NetBeans本身的问题(java meta data record),功能不完整。有了这两个窗口,就永远不用 Go to Source, Show JavaDoc 了,所有的信息都在手边,只需将鼠标放在想看的类元素上。



4.Code Generation dialog 我的确很厌倦写类的构造器的实现,特别是类的成员很多的情况下。还有标准的、功能完善的、性能良好的 Common Methods: equals, hashcode 的实现等。
有了它,在右图中的例子中我只需要输入
private long id;
private String name;
private int age;

其它的代码都是由它产生的。

5. Local History

或许有些功能被我忽视了,如是这样的话,请见此处的详细列表。还有整个重写的 Editor 的功能都陆续地出现在


是的,我也保留些目前还未完善的功能,我本人也没有加紧试用。如果我认为有用的话,我会进一步写出来的。

平步星云 - December 10, 2008 05:08 PM
NetBeans多语言版本,更换默认 Locale 设置时的问题

update: (这个BUG只会在中文操作系统下出现,在英文版的操作系统上一切正常。看来是一些模块在编写时没有正确地加载对应locale 的 properties 文件)

在我看来,在日常的工作平台,JDK/NetBeans 的i18n 字体问题已经成为历史了。但是我前些天又登记了一个 BUG 到 NetBeans 的 IssueZilla 中:

1. 下载多语言版本的安装包,安装
2. 因为我不喜欢那些翻译的不准确的词,所以我打算回退到英文的 locale:
增加一个启动参数到 $NetBeans/etc/netbeans.conf 文件:
netbeans_default_options="$默认的配置 --locale en"
语法为: --locale language[:country[:variant]]

3. 重启后,你将看到“著名的字体问题”重新又出现了,显示为小方框:

当前,只有两种办法可以解决这一问题:

1. 去掉加上的 locale 参数,但必须忍受翻译得不佳的词了。

2. 下载英文版,继续用吧。但这样的话,在 Welcome 中你将看不到中文的 Blog.

平步星云 - December 10, 2008 05:08 PM
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 按钮,成功显示:

平步星云 - December 10, 2008 05:08 PM
体验更多 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 中去。

(期待下一篇)

平步星云 - December 10, 2008 05:08 PM
NetBeans6 功能介绍: 布置 declaration View 和 Javadoc View

此篇文章介绍一个在 NetBeans 6 中同时查看鼠标指针处的源代码和
Javadoc (不再Go to Source .../ Show JavaDoc)

1. 首先打开它们:Window | Other | Declaration View 和 Window | Other | Javadoc View, 它们都被搁浅在 Output 窗口的位置,但此时只能看到一个窗口的内容,因为无论切换到其中的任何一个,它们都占据整个下端窗口。如下:



2. 我们要将它们分开,点击其中任一窗口的上方(类似标题栏区域),按住不放,将其拖向左侧(或右侧也可),当出现一个红色的方框后释放,如下:



释放后的效果如下:




这些位置会被记录下来,只要你在设置之后正常退出了。在下次启动 NetBeans 后,你可以看到同样的布局。

这个截图中显示的是 Integer.toHexString 方法的 Javadoc 和 实现源码。


(期待下一篇)

平步星云 - December 10, 2008 05:08 PM
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 不得不看

平步星云 - December 10, 2008 05:08 PM
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 上的效果 (注意脚注部分显示,当前运行在什么服务器上):

平步星云 - December 10, 2008 05:08 PM
NetBeans 6.0 beta1 终于出来了!

经过“漫长”的等待,beta1 终于出来了!其实我一直在用它的最近的 daily build, 因为 M10 实在是太不稳定了。

beta1 给人的第一印象是,更换了主题(桌面 icon, welcome 页,向导图案),使用的是接近于 Vista 类似的蓝绿色调,给人以清新的感觉!
加紧试用吧!



BlogJava-Java桌面技术-随笔分类-NetBeans - December 07, 2008 12:36 PM
《FilthyRichClients》读书笔记(二)-让Swing正确显示Gif

摘要: 显示GIF图片一直是swing的诟病,本文给出一个用swing处理gif类型图片的参考实现  阅读全文

平步星云 - November 19, 2008 03:03 PM
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!

平步星云 - November 16, 2008 04:29 PM
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) 内存堆!

BlogJava-Java桌面技术-随笔分类-NetBeans - June 23, 2008 02:49 PM
《FilthyRichClients》读书笔记(一)-SwingのEDT

摘要: 愿借助此帖能使读者彻底理解Swing事件队列及EDT机制,达到“此贴一发,余词尽废”的效果。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - May 29, 2008 01:09 PM
FilthyRichClients中文版

摘要: 没想到FilthyRichClients的中文版居然这么快!真是欣喜若狂。打算今后陆续开始写读书笔记,对自己、Swing开发人员都有益处。  阅读全文

平步星云 - February 02, 2008 04:33 AM
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 语言时间还长,可是让我现在设计出一个使用 “泛型”的框架,我心中没底。

平步星云 - January 07, 2008 08:24 AM
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
门面是粗粒度的对象


总结


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

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

BlogJava-Java桌面技术-随笔分类-NetBeans - December 30, 2007 04:49 PM
通过xml配置文件定义及布局组件

摘要: 无论Matisse发展得如何强大,但是其本质只是用来生成Java代码而已,当你修改这代码后,再逆向恢复成UI设计器时Matisse却出于自己的一套安全考虑不允许你这样做,所以最终不得迫使开发人员放弃拖曳方式设计UI,而统统采用面向代码的方式。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 29, 2007 08:53 AM
自定义布局管理器-CenterLayout

上文自定义布局管理器-FormLayout介绍了FormLayout的参考实现,利用FormLayout,通过指定left、top、

right(可选)、bottom(可选)布局约束可以对组件进行精确定位。然而有些组件在业务上是有固定尺寸的,例如自定义组件之Button介绍的一样,通过给按钮指定4种状态时的图片,那么组件的最佳尺寸就是图片的尺寸,因此组件的PreferredSize就可以确定,所以此时只需要组件中心的确定坐标就可以了,实际组件的Location只和其PreferredSize有关。如下图所示:

这就是CenterLayout的思想。

修改FormData,只需要添加两个变量即可。
public final class FormData {
    public FormAttachment left;

    public FormAttachment right;

    public FormAttachment top;

    public FormAttachment bottom;

    public FormAttachment centerX;

    public FormAttachment centerY;
}
CenterLayout与FormLayout不同只在于addLayoutComponent、layoutContainer这两个

方法实现,其他接口方法均相同,所以下面只介绍这两个方法实现,其他接口方法请

参阅上文自定义布局管理器-FormLayout
在addLayoutComponent方法的开头,同样是对布局约束参数constraints合法性进行检查,这点与FormLayout大致相同。

if (constraints == null) {
            throw new IllegalArgumentException("constraints can't be null");
        } else if (!(constraints instanceof FormData)) {
            throw new IllegalArgumentException("constraints must be a " + FormData.class.getName() + " instance");
        } else {
            synchronized (comp.getTreeLock()) {
                FormData formData = (FormData) constraints;
                if (formData.centerX == null || formData.centerY == null) {
                    throw new IllegalArgumentException("centerX FormAttachment and centerY FormAttachment can't be null");
                } else if (comp.getPreferredSize() == null) {
                    throw new RuntimeException("component must have preferred size before be add into parent with CenterLayout");
                }
                 componentConstraints.put(comp, (FormData) constraints);
            }
        }

对于CenterLayout来说,FormData对象的centerX、centerY必须给出,因为它代表,点的坐标,除此之外组件必须有PreferredSize属性来指定组件大小。

layoutContainer方法实现也大致相同
public synchronized void layoutContainer(Container target) {
        synchronized (target.getTreeLock()) {
            int w = target.getWidth();
            int h = target.getHeight();
            Component[] components = target.getComponents();
            for (Component comp : components) {
                FormData formData = componentConstraints.get(comp);
                if (formData != null) {
                    ...
                }
            }
        }
    }
上面这步与FormLayout一样。关键在if语句块内,代码如下:
  FormAttachment centerX = formData.centerX;
                    FormAttachment centerY = formData.centerY;
                    int width = component.getPreferredSize().width;
                    int height = component.getPreferredSize().height;
                    int x = (int) (centerX.percentage * w) + centerX.offset - width / 2;
                    int y = (int) (centerY.percentage * h) + centerY.offset - height / 2;
                    comp.setBounds(x, y, width, height);
获得centerX、centerY以及最佳尺寸,如上图所示,不难得出x、y的计算方法。

至此,自定义布局管理器就介绍到这里,这两个布局类可以解决很多静态布局需求,所谓静态布局是指容器内有什么组件是固定的。如果遇到动态界面,例如组件的内容依照用户级别、插件扩展点等因素决定,也并不是难事,因为了解了布局管理器运行机制以后可很容易地定义适合你需求的布局类。对于静态布局来说,你可能厌倦了hard coding来布局,你希望这一切由xml这样的配置搞定,好,下一部分则开始“压轴戏”——用配置文件解决布局。



BlogJava-Java桌面技术-随笔分类-NetBeans - November 24, 2007 10:26 AM
自定义布局管理器-FormLayout

摘要: 在java.awt包与javax.swing包下有许多现成的布局类,比如BorderLayout、FlowLayout,还有较为复杂的、用于精确定位的布局类GridBagLayout、SpringLayout等。起初我刚刚从事gooey时(06年中),企图依靠JDK自带的布局类进行布局,但是实际不可能或者说很难做到。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 18, 2007 07:15 AM
布局管理器面面观

摘要: 布局管理器是一个实现了LayoutManager接口或LayoutManager2接口并且能够确定一个容器内部所有组件大小和位置的对象。尽管组件能够提供大小和对齐的提示信息,但是一个容器的布局管理器将最终决定组件的尺寸和位置。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 07, 2007 08:17 AM
swing专业外观

摘要: 前3篇文章使用SWT组件实现了专业外观窗口,现在给出一个swing实现版本。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 06, 2007 04:49 PM
打造专业外观-三

摘要: 在《打造专业外观-二》中,留下了3个未实现的功能:窗口标题和图标,边缘圆角,功能按钮。在本篇中将实现这些功能来完结打造专业外观-窗口部分的讲解。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 04, 2007 12:12 PM
打造专业外观-二

摘要: 在上一篇《打造专业外观-九宫图》,介绍了九宫格的概念并留下了一个演示程序。那个程序只是一个渲染过的窗口,许多必要的功能尚未实现,比如拖拽移动、改变大小、标题栏双击等。好现在就来一一实现。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - November 03, 2007 09:56 AM
打造专业外观-九宫图

摘要: 起初依赖JButton、JScroll等基础组件,BorderLayout、FlowLayout等现成的布局管理器企图能实现类似MSN的外观效果,但是实际看来是完全不可能的,所以我当时就下了这样的结论,哪怕现在我依然认为是正确的:“抛弃所有的外观设计工具和一切现有的桌面组件及布局管理器,一切的一切必须自定义实现,设计工具最多能替你完成一半”。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - October 28, 2007 07:15 AM
NetBeans 6.0模块快速入门教程

摘要: 这篇文档展示如何创建一个模块来向IDE或其他基于NetBeans平台的应用程序添加一个Google查询工具栏。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - October 26, 2007 06:35 AM
Netbeans模块和富客户端应用学习教程翻译计划

摘要: 随着netBeans6.0 Beta2的发布,netBeans的优势已越来越明显。但是国内介绍netBeans的书籍相比Eclipse的臭街而言少之又少,而且没有一本专门介绍netBeans插件的书籍,有也是Sun中文技术社区零星的几篇翻译。而我又对netBeans插件的了解还只是入门级水平,看来看去最好去Sun的官方网站看NetBeans Modules学习教程。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - October 26, 2007 05:59 AM
netBeans6.0咸鱼翻身与Swing称霸桌面应用

摘要: 长期以来,Java被认为是服务器端的佼佼者而在桌面领域并无建树,早期的Swing在外观和性能上都差强人意,成功案例不多,基于Swing的netBeans无疑惨遭厄运,这样的局面下滋生了SWT和Eclipse这种违背Java原则的产物。  阅读全文

BlogJava-Java桌面技术-随笔分类-NetBeans - October 23, 2007 05:35 AM
SWT自定义组件之Slider

   曾经介绍过用SWT实现MSN风格的下拉框,SWT虽然没有Swing那么强大,尤其是在打造专业外观上,不支持L&F,但是通过自定义组件,同样可以达到用户要求。下面就向大家介绍本人实现的一个具备专业外观的Slider控件。
    首先来参考一下组件的实际运行效果,并和SWT原生组件进行一下对比。 

                                                       

可以看出,经过自定义的组件在外观上要比SWT直接调用本地组件显得更加专业。当用户托拽滑动块时,还会出现一个虚拟的滑动块用来标识将要移动到的位置。演示就到此为止,下面详细介绍这个很Cool的组件是如何通过SWT实现的。

    基本设计思想:与其他自定义组件一样,是通过继承org.eclipse.swt.widgets.Composite来实现,定义该类为Slider,另外滑动块(thumb)也是Composite,并放在Slider之上,当鼠标移动thumb时,调用setBounds方法定位在Slider在父组件(Slider)上的位置,从而达到拖拽thumb的目的。此外通过实现PaintListener接口进行自定义绘制,绘制的对象包括组件边框、被填充的格子、未被填充的格子、虚拟滑块。

    接触过GUI编程的程序员都应该知道像Scroll、Slider、ProgressBar这样的控件都有setMaxValue、setMinValue、setValue这样的方法,除了鼠标拖拽thumb来改变当前数值外,可直接调用setValue来设置当前值。此外这些控件还有水平(Horizontal)、垂直(Vertical)两种布局,对于事件处理一般都要有一个从java.util.EventObject继承而来的事件类,还要编写事件监听器(Listener)接口,因此在开始编写Slider控件之前先定义3个类,代码都不是很长,如果你熟悉AWT、Swing的事件处理机制,相信你能轻松跳过。


public enum SliderOrientation {
 HORIZONTAL, VERTICAL;
}

public class SliderEvent extends EventObject {

 private int value;

 public SliderEvent(Object source, int value) {
  super(source);
  this.value = value;
 }

 public int getValue() {
  return value;
 }
}

public interface SliderListener {

 public void valueChanged(SliderEvent event);
}

    接下来着重介绍Slider。首先是继承Composite并实现ControlListener、PaintListener、MouseListener,、MouseMoveListener,、MouseTrackListener,然后自动生成接口方法代码,通过Eclipse可以轻松实现,需要注意的是MouseListener,有java.awt.event.MouseListener和org.eclipse.swt.events.MouseListener两种,不要混淆,否则错误很难找到。然后是要采集一些数据信息,分别是:边框颜色、已有数据部分的填充颜色(上图中组件的绿色部分)、未达到数据部分的填充颜色(上图中组件的白色部分)、被禁用时的填充颜色、水平滑块的图标(正常、托拽中两种)、垂直滑块图标(正常、托拽中两种)、水平、垂直虚拟滑块图标。以上这些数据对应的常量声明如下:

 private final Color BORDER_COLOR = new Color(Display.getCurrent(), 180, 188, 203);

 private final Color FILL_COLOR = new Color(Display.getCurrent(), 147, 217, 72);

 private final Color BLANK_COLOR = new Color(Display.getCurrent(), 254, 254, 254);

 private final Color DISABLE_COLOR = new Color(Display.getCurrent(), 192, 192, 192);

 private final Image THUMB_ICON_V = new Image(Display.getDefault(), "slider_up_v.png");

 private final Image THUMB_OVER_ICON_V = new Image(Display.getDefault(), "slider_over_v.png");

 private final Image THUMB_ICON_H = new Image(Display.getDefault(), "slider_up_h.png");

 private final Image THUMB_OVER_ICON_H = new Image(Display.getDefault(), "slider_over_h.png");

 private final Image TEMP_H = new Image(Display.getDefault(), "temp_h.png");

 private final Image TEMP_V = new Image(Display.getDefault(), "temp_v.png");

 除了这些常量,还应该声明默认最大值的常量,private final int DEFAULT_MAX_VALUE = 100;
接下来定义当前数值和最大值,
private int value;
private int maxValue = DEFAULT_MAX_VALUE;
并生成以上两个成员属性的get方法
然后定义滑动块和布局
private SliderOrientation orientation;
private Composite thumb;
要处理数值变化,需要实现一组监听器,添加如下代码
private List<SliderListener> listeners = new ArrayList<SliderListener>();
public void addSliderListener(SliderListener sliderListener) {
 listeners.add(sliderListener);
}
public void removeSliderListener(SliderListener sliderListener) {
 listeners.remove(sliderListener);
}
接下来定义2个辅助方法,实现value<->pelsLength转换。其中value是当前的数值,由具体业务来决定,下文中称其业务值。例如一个音量控制器,音量范围在0~500,那么从业务上来讲可以将数值设置在0~500之间的任何数,而pelsLength则由控件的长/高度来决定,单位是像素。但是value与pelsLength之间存在着一个比例关系式:value/maxValue=pelsLength/控件长度或高度。这样不难得出两个函数的定义。
private int valueToPels(int value) {
  float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
    : getBounds().height;
  return (int) (widgetLength * (float) value / (float) maxValue);
 }

 private int pelsToValue(int pels) {
  float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
    : getBounds().height;
  return (int) ((float) pels * (float) maxValue / (float) widgetLength);
 }
最后定义构造器。代码如下
public Slider(Composite parent, SliderOrientation orientation) {
  super(parent, SWT.FLAT);
  this.orientation = orientation;
  thumb = new Composite(this, SWT.FLAT);
  thumb
    .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
      : THUMB_ICON_H);
  addControlListener(this);
  addPaintListener(this);
  thumb.addMouseListener(this);
  thumb.addMouseMoveListener(this);
  thumb.addMouseTrackListener(this);
 }
在构造器中,注入布局对象,然后在控件上创建滑动块组件thumb,并添加鼠标处理等。
到此为止,基本的成员和方法的定义完毕,下面循序渐进讨论如何实现这一Slider。

一、绘制边框
由于是绘制操作,所以一切绘制代码均在paintControl方法内实现,先将如下代码拷贝到paintControl内
int w = getBounds().width;
int h = getBounds().height;
int fillLength = valueToPels(getValue());
GC gc = e.gc;
switch (orientation) {
 case HORIZONTAL:
  break;
 case VERTICAL:
  break;
}
分析如下
首先获取控件的长度与高度,因为接下来的绘制要经常用到这两个变量。
“int fillLength = valueToPels(getValue());”这一行代码稍后作解释,然后是获得绘制上下文对象,下一步是根据布局不同采用不同的处理,除了paintControl函数,在其他很多地方都对布局进行判断,但是简单起见,只对水平布局进行介绍,垂直部分参考完整程序。
接下来的绘制操作均在case HORIZONTAL中进行,首先将颜色设置为边框的颜色
gc.setForeground(BORDER_COLOR);然后绘制一个矩形gc.drawRectangle(0, 2, w - 1, h - 5);
关于为什么要偏移2像素、长度为什么减1、高度为什么减5,请参考有关绘图的基本知识,上一篇也有简单的介绍。

现在,你就可以编写测试程序来验证结果了,看看边框是否与示例的效果一样。

二、托拽thumb的实现
桌面GUI编程领域技术深浅的度量衡通常有4项指标:皮肤(外观,swing组件体系称其L&F)、绘图、自定义组件布局(Layout)、自定义组件。而托拽是实现自定义组件和绘图不可或缺的技术,也是难点之一,因此掌握的深浅是衡量桌面编程水平的标志。
虽然作为难点,但是也有章可循,其基本实现简单到只监听鼠标事件这么简单,基本流程是:当鼠标在thumb上按下时,记住这个位置,然后按住鼠标左键托拽,最后松开鼠标计算两个位置之间的距离(位移),根据位移量移动thumb的位置并换算出等价的value增量(可能为负值)进行业务逻辑处理。下面通过代码循序渐进完成。
定义一个位置变量用来存储鼠标单击的位置,private Point controlPoint;然后实现public void mouseDown(MouseEvent e)和public void mouseUp(MouseEvent e)两个方法。
public void mouseDown(MouseEvent e) {
  controlPoint = new Point(e.x, e.y);
  thumb
    .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_OVER_ICON_V
      : THUMB_OVER_ICON_H);
 }

 public void mouseUp(MouseEvent e) {
  try {
   thumb
     .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
       : THUMB_ICON_H);
   countValue(e);
  } finally {
   controlPoint = null;
  }
 }
“controlPoint = new Point(e.x, e.y);”这一行实现记住鼠标点的位置,注意,这个位置是相对滑块thumb的,因为是thumb监听的鼠标事件。接下来是设置滑动块背景,不难理解当鼠标松开时,应该将背景恢复。然后进行非常重要的换算工作,通过countValue方法实现,最后务必要把鼠标位置清空,用try-finally是有必要这样的。之所以要在方法结束的时候清空controlPoint,是因为在鼠标移动的时候需要对controlPoint进行更加复杂的计算。稍后讲解mouseMove实现的时候再作解释,接下来着重分析countValue方法。
如前所述,countValue完成计算鼠标按下、松开的位移量,换算成与业务相关的数据(value)。
private void countValue(MouseEvent e) {
  switch (orientation) {
  case HORIZONTAL:
   int movedX = e.x - controlPoint.x;
   setValue(getValue() + pelsToValue(movedX));
   break;
  case VERTICAL:
   ......
  }
 }
“int movedX = e.x - controlPoint.x;”实现了位移量的计算,保存到movedX,调用pelsToValue方法将movedX转换成业务值的增量,然后调用setValue重新赋值,注意pelsToValue得到的是增量,需要与原值(getValue()得到)叠加。接下来分析setValue方法。
public void setValue(int value) {
  if (value < 0) {
   this.value = 0;
  } else if (value > getMaxValue()) {
   this.value = getMaxValue();
  } else {
   this.value = value;
  }
  try {
   moveThumb();
   redraw();
  } finally {
   for (SliderListener listener : listeners) {
    try {
     SliderEvent event = new SliderEvent(this, getValue());
     listener.valueChanged(event);
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  }
 }
方法开始处的一系列if语句对value进行验证后再赋值,然后是调用moveThumb实现滑块移动、调用redraw对组件重新绘制,最后是处理具体的业务,可以看出处理业务是setValue方法的关键,所以要用try-finally,谁也不敢确保moveThumb、redraw不出问题。对于实现业务是遍历监听器列表然后执行每个监听器的valueChanged方法,这种事件源-监听器模型也是Java2以后的GUI事件实现模型。

 private void moveThumb() {
  Image icon = thumb.getBackgroundImage();
  int iconw = (icon != null) ? icon.getBounds().width : 0;
  int iconh = (icon != null) ? icon.getBounds().height : 0;
  switch (orientation) {
  case HORIZONTAL:
   int x = valueToPels(getValue()) - iconw / 2;
   if (x < 0) {
    x = 0;
   } else if (x > getBounds().width - iconw) {
    x = getBounds().width - iconw;
   }
   thumb.setBounds(x, 0, iconw, iconh);
   break;
  case VERTICAL:
   ......  }
 }
不难理解,moveThumb的任务就是根据业务值value来将滑块移动到正确的位置。
以上代码声明滑块的位置是“x”,通过转换函数获得,但是还要减去滑块的一半,因为具体坐标应该落到滑动块的中间,仔细想想不难得出。if-else是对x进行验证,最后通过setBound来定位thumb。
对于redraw,他的作用是触发paintControl方法进行重绘,因为重绘出了边框还要根据value绘制填充格子,而value已经在redraw方法调用前被赋了值,所以这时候应该进行重绘。
现在你可以托拽thumb了,美中不足的是滑动块不能随时跟随鼠标的轨迹移动,这个稍后会实现。

三、填充格子
现在的组件外观只是绘制了边框,现在进行格子填充。在讲述边框绘制的时候提到了一行代码“int fillLength = valueToPels(getValue());”现在不难理解吧,就是将当前业务值value转换成实际的长度。在绘制边框之后,加入如下代码:
if (getEnabled()) {
    gc.setBackground(FILL_COLOR);
    for (int i = 2; i < w - 2; i += 4) {
     if (i > fillLength) {
      gc.setBackground(BLANK_COLOR);
     }
     gc.fillRectangle(i, 4, 3, h - 8);
    }
   } else {
    gc.setBackground(DISABLE_COLOR);
    gc.fillRectangle(1, 4, w - 1, h - 8);
   }
首先判断是否是enable,然后设置填充颜色FILL_COLOR,然后在for循环中执行正方形格子的填充,递增量“i”从2开始是空开2像素间隔,同理i也不能超过w-2(对称性),i+=4是相邻两个格子左边坐标间距4像素,然后“gc.fillRectangle(i, 4, 3, h - 8);”这一行进行填充绘制正方形。留意,x坐标是i,y坐标是从4开始画的,出于对称高度也要“h-8”,长度之所以是“3”是保持相邻两个格子之间保持1像素的间隔(想想“i+=4”就不难得出答案)。此外还要对fillLength进行判断以便决定颜色采用绿色还是白色以示区分。对于绘图操作来说,千万不要埋怨考虑细节过多,事实上,GUI编程过程中“坐标系”这个概念是需要经常被考虑的,试想如果上述代码for循环中把“i+=4”,写成“i+=5”,只是一个像素之差绘制效果差之千里,如果想知道笔者是如何得到这些坐标数据的,实话说,是靠多次调试得出的结果。
现在你运行程序,应该能根据value进行格子填充了。到此为止,绝大多数的功能已经实现,但是人性化的界面设计应该在托拽时出现一个虚拟的滑动块用来标识将要移动到的位置。好,继续实现这一功能。
定义一个int变量纪录thumb的临时位置,private int tempLocation;然后在paintControl添加如下红色代码
case HORIZONTAL:
   gc.setForeground(BORDER_COLOR);
   gc.drawRectangle(0, 2, w - 1, h - 5);
   if (getEnabled()) {
    gc.setBackground(FILL_COLOR);
    for (int i = 2; i < w - 2; i += 4) {
     if (i > fillLength) {
      gc.setBackground(BLANK_COLOR);
     }
     gc.fillRectangle(i, 4, 3, h - 8);
    }
    if (controlPoint != null) {
     gc.drawImage(TEMP_H, tempLocation, 0);
    }
   } else {
    gc.setBackground(DISABLE_COLOR);
    gc.fillRectangle(1, 4, w - 1, h - 8);
   }
   break;
很直观,对于水平布局就是在(tempLocation,0)画出“TEMP_H”图标。外面的if很重要,还记得mouseDown方法中的语句“controlPoint = new Point(e.x, e.y);”和mouseUp方法中的语句“controlPoint = null;”吗,当鼠标按下托拽开始时,为controlPoint赋值,当鼠标完成托拽松开时,将controlPoint置null,在这个托拽过程中controlPoint一直保持非null状态,所以paintControl方法才将图标画出。如果现在就着急运行程序则会发现,鼠标托拽时候虚拟图标总停留在最左边(如果垂直布局停留在最上边,因为并没有为tempLocation赋值默认是0),不会跟随鼠标移动。如果要实现真正的托拽那么必须在鼠标移动时反复执行paintControl,下面花大量的笔墨详细地介绍mouseMove方法的实现,并简单介绍绘图操作的一些原理和流程。
开门见山,直接将mouseMove函数的全部代码列出。
public void mouseMove(MouseEvent e) {
  if (controlPoint == null) {
   return;
  }
  int maxLength;
  int maxLocator;
  switch (orientation) {
  case HORIZONTAL:
   maxLength = valueToPels(getMaxValue());
   maxLocator = maxLength - TEMP_H.getBounds().width;
   int movedX = e.x - controlPoint.x;
   redraw(tempLocation, 0, TEMP_H.getBounds().width,
     TEMP_H.getBounds().height, false);
   tempLocation = valueToPels(getValue()) + movedX
     - TEMP_H.getBounds().width / 2;
   if (tempLocation < 0) {
    tempLocation = 0;
   } else if (tempLocation > maxLocator) {
    tempLocation = maxLocator;
   }
   break;
  case VERTICAL:
   ......
   break;
  }
 }
最前面的if语句表明,只有鼠标按下时移动鼠标才算托拽,道理前面已经阐明了。maxLength代表最大值转换得到的像素,maxLocator是虚拟图标左端(上端)最大坐标,movedX代表托拽的位移量。最下面的if-else if目的很明了。整个mouseMove函数中
redraw(tempLocation, 0, TEMP_H.getBounds().width,
     TEMP_H.getBounds().height, false);
   tempLocation = valueToPels(getValue()) + movedX
     - TEMP_H.getBounds().width / 2;
是整个托拽操作最难懂也是技术含量最高的两条语句。简单起见暂时用下面的语句代替
tempLocation = valueToPels(getValue()) + movedX
     - TEMP_H.getBounds().width / 2;
   if (tempLocation < 0) {
    tempLocation = 0;
   } else if (tempLocation > maxLocator) {
    tempLocation = maxLocator;
   }
   redraw();
其中“tempLocation”的赋值语句不变,变化的是redraw函数的调用位置和参数。这样的变化使得意思就不难理解了,首先确定tempLocation的值,等号右边的计算结果也不难理解,然后调用redraw方法重画组件。如果这时候你运行程序,托拽时确实虚拟光标会跟随鼠标移动,但是也会发现组件闪烁得很厉害!具体程度取决于用户计算机的性能,关于“组件重绘时闪烁”的问题是绘图操作的一个常见问题,不仅仅是Java,任何支持绘图的计算机语言都可以暴露这样的问题,当年在大学用MFC、VB的编写过画图板的人应该熟悉这类问题。
在具体讨论之前先简单讲述鼠标监听器中的mouseMove操作
一旦为GUI组件添加鼠标移动监听器,当鼠标光标在组件上移动时便调用监听器接口的mouseMove(MouseEvent e)方法,mouseMove调用的频率与鼠标移动的快慢有关,移动越快mouseMove被调用的次数就越少,反之就越多。假设鼠标从组件上的A点移动到B点,如果鼠标移动得足够快,那么就可以理解为从A直接到B而不经过中间的任何一个点,那么mouseMove函数仅仅调用一次。反之鼠标慢慢从A移动到B,那么中间可能会经过C、D、E、F......一系列的点,而mouseMove也会被执行多次。总之当鼠标在组件上移动时,mouseMove会频繁被调用,之所以闪烁问题出在redraw方法,如果redraw方法不加任何参数那么将对组件全部重绘,对于鼠标托拽这种操作鼠标每移动一次就要对组件全部重绘,性能的代价可想而知,不闪才怪呢。解决的办法就是只重绘变化的部分。
如何做到这一点,要先了解必要的绘图机制,在SWT中(Swing绘图原理与其类似)redraw其实会包含2个含义,擦除、绘制,所以得名于redraw,意思就是重绘。首先是根据传入redraw方法的参数确认需要擦除的范围,如果没有则擦除全部,然后底层会创建一个绘制请求交给操作系统去处理,但是这个请求不会在redraw方法调用完毕后立即被处理,即重绘操作不会立即执行,它是被送进底层的事件队列中。处理时的绘制操作是由paintControl完成,所以redraw的调用会导致paintControl的执行。
再将那两行代码列出来。
redraw(tempLocation, 0, TEMP_H.getBounds().width,
     TEMP_H.getBounds().height, false);
   tempLocation = valueToPels(getValue()) + movedX
     - TEMP_H.getBounds().width / 2;
如果光标从A移到B处,redraw方法所做的事情是通知重绘光标在A点时虚拟图标范围内的图像,然后立即创建一个绘制请求送到系统事件队列,然后更新tempLocation的值。现在再将绘制的那部分代码列出
if (controlPoint != null) {
     gc.drawImage(TEMP_H, tempLocation, 0);
    }
将这两部分代码列出来做个比较,还有非常重要的一点需要指出,托拽时虚拟光标能呈连续性移动,这一功能之所以能得以实现非常重要的一点是:从底层请求送入事件队列到请求被执行存在时间差,利用这个时间差(时间非常短)可以执行一些“更新操作”,比如上面的更新tempLocation。简单的理解是redraw方法调用会以异步方式执行擦除、绘制。当执行绘制操作“gc.drawImage(TEMP_H, tempLocation, 0);”时,tempLocation已经是更新后的值了,而redraw表明擦除旧区域的图像,因为当paintControl执行时这部分图像区域根据计算结果已经不再是虚拟滑块了。下面做一个试验。
添加下面红色代码
redraw(tempLocation, 0, TEMP_H.getBounds().width,
     TEMP_H.getBounds().height, false);
   try {
    Thread.sleep(1000);
   } catch (InterruptedException e1) {
    e1.printStackTrace();
   }
   tempLocation = valueToPels(getValue()) + movedX
     - TEMP_H.getBounds().width / 2;
这样在绘制执行时,tempLocation还没有得到更新,效果运行下知晓。

现在,你可以运行完整的程序了,看一下托拽时的效果。
完整的程序这里下载



BlogJava-Java桌面技术-随笔分类-NetBeans - October 23, 2007 05:21 AM
求《Pro Netbeans IDE 6 Rich Client Platform Edition》电子版

前些日子在netbeans.org上看到年底要出版一本关于netBeans6.0的指南《Pro Netbeans IDE 6 Rich Client Platform Edition》,突然萌生翻译此书的想法,为国内Java开发者做一点贡献。翻译的工作很辛苦,最初当然只是在博客上发表,还望同仁多多指教。
    首要任务是希望尽快拿到该书的电子版,请业界同仁帮忙,不胜感激!

该书的详细介绍参见http://www.amazon.com/Pro-Netbeans-Rich-Client-Platform/dp/1590598954



平步星云 - August 25, 2007 03:24 AM
SUN 将股票代码 SUNW 换为 JAVA

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

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

平步星云 - August 10, 2007 02:09 AM
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 社团,也许你就是其中一位了!

平步星云 - July 08, 2007 04:40 AM
Open NetBeans 6 module project with NetBeans 5.x

要打开 NetBeans 6 的源代码工程,最方便的当然是使用配套的最新的开发版,但由于开发版或多或少会不稳定(特别是最近的M10)。所以还是要使用稳定的 release 版本来打开或者开发。
但是由于平台的兼容性问题,在尝试 NetBeans 5.5 来打开从 NetBeans CVS checkout 或下载下来的源代码模块工程时,同样也遇到了问题:在打开对话框中 Project Name 中会以红色显示:

java.io.IOException:
Misconfigured project in ... has no defined "code-name-base"


经过一番周折,我打到一个解决办法:

1. 在 NetBeans 5.x 中注册安装的 NetBeans 6.0 平台 Tools | NetBeans Platform Manager -> Add Platform... , 选择NB6 的安装目录
1.1 在 Harness TAB 中要确保选择第二项 "Harness supplied with Platform", 默认好象是第一项"Harness supplied with IDE" (如果工程使用新的平台中的一些API的话,第一项显然是不对的。) 
1.2 记住 "Platform Name" 的值,因为下面要用到,所以最好拷贝一下。


2. 修改要打开的NetBeans 模块工程的 $工程目录\nbproject\project.xml 文件
<data xmlns="http://www.netbeans.org/ns/nb-module-project/
这个值在 NetBeans 6 中为 <data xmlns="http://www.netbeans.org/ns/nb-module-project/

也就是将新版中使用的命名空间定义"3" 回退到 NetBeans 5.x 使用的 "2"

3. 在要打开的NetBeans 模块工程的 $工程目录\nbproject\ 的目录下,增加 platform.properties 文件, 其内容为:
nbplatform.active=NetBeans_IDE_6.0_M10_(build_200706281431)

即让它指向前面记下的 “Platform Name” 的值:


4. 现在打开试试! (你们也许还会遇到其它问题,因为我也记不清是否改了其它配置)

5. 最后,我还遇到一问题,右击工程 | properties
弹出一个对话框说"没找到定义的平台,回退到默认的平台", 我没办法,只有选择是,然后进入到工程属性中后:
Libraries -> NetBeans Platform 把它给改回来

6. build 试试!


好运!