这是一篇关于使用IBM Rational Application Developer 6.0进行JSF (JavaServer Faces)和JSR (Java Specification Request)168开发的系列文章的第二部分(共两部分)。第一部分主要关注于JSF 和 JSR 168开发的基础产品特性并创建了两个portlet和三个CRMBrowser应用视图。在第二部分,你将会了解如何在JSR 168 portlet之间进行通信实现复杂的屏幕流。
这是一篇关于使用IBM?; Rational? Application Developer 6.0(IRAD)进行portal开发的系列文章的第二部分(共两部分,第一部分见Resources部分)。除了理解这些功能性的知识外,你也可以学到有关包含在IRAD中基础的JavaServer? Faces (JSF) 和 IBM ? WebSphere Portal Server 5.1特性。最后,这篇文章将会讨论如何去实现:
- 通过WSDL (Web Services Description Language) 将portlet属性展现给WebSphere Portal Server 属性代理。
- 在portlet之间传递复杂的、用户定义的数据列。
- 重写由IRAD产生的portlet类的行为,从而转换基于其它portlet传递过来的数据的 portlet视图。
使用混合型技术开发协作性portlet,这些技术包括以下几个重要步骤:
- 设计portlet之间如何相互影响,也就是说,描述两个portlet之间怎样交换数据。
- 启动源portlet
- 启动目标portlet
- 启动发生在源portlet和目标portlet之间的不同的交互。
- 定义portlet之间的连接
- 通过WebSphere Portal Server 属性代理调用触发属性传输的事件.
首先,我接着第一部分开始,完成这些步骤实现我们CRMBrowser应用程序中的整个概要-细节的浏览功能.你可以从该篇文章的下载部分下载这些实例代码.
在定义如何在Summary 和Detail portlet之间启动交互之前,该篇文章将首先论述一下这些Portlet如何在运行期进行交互.不管你什么时候点击Summary Portlet中链接事件,它都会发送一个消息给 Detail Portlet,让它显示相应的实体和数据。
Summary portlet通过你所点击的事件链接向Detail portlet发送不同的数据.比如:当你点击customer id链接时,Summary portlet将向Detail portlet发送一个com.ibm.sample.crmbrowser.CustomerDetailInfo bean的实例.同样,如果你点击了issue id 链接,那么Summary portlet将会向这个Detail portlet发送一个com.ibm.sample.crmbrowser.Issue bean的实例.
要想让WebSphere Portal Server属性代理器知道两个portlet是如何进行交互的话,你必须在他们两个portlet之间定义一个wire。 使用WSDL(接下来会有更具体的介绍)来对这个连接进行描述。这个连接的源数据和目标数据必须是相同的;因此,要想把你的bean从Summary Portal传送到Detail Portal中,你必须定义一个额外的bean作为CustomerDetailInfo和Issue bean实例的持有者。com.ibm.sample.crmbrowser.ShowDetailMessage bean的实例将会在Summary portlet 和Detail portlet之间传输。通过你在CustomerSummaryView.jsp中所点击的链接,它将会很准确的容纳CustomerDetailInfo bean或Issue bean中的一个实例。
接下来将描述如何配置这个连接来体现出这些语义,以及如何实现在portlet和源、目标JSP(JavaServer? Pages)中需要的java代码。
启动这个源portlet包括以下几个步骤:
- 使用WSDL定义这个源的属性和动作。
- 启动这个源的动作。
- 配置这个动作的控制属性从而触发源portlet和目标portlet之间的传递。
- 写一些动作句柄代码把正确的信息放入正确的portal编程模型对象中(比如:请求属性)。
JSR168 portlet可以通过属性代理交换属性达到相互协作。一个WSDL文件描述了那些源portlet发布(或传递)给属性代理的属性。在接下来的处理过程中,你将会了解到一些IRAD用来支持协作性portlet开发的工具。
要想把我们的Summary portlet作为一个属性源,可以简单的右击Project Explorer 视图中的portlet来显示上下文菜单。如图1所示点击Cooperative > Enable Source。
图1、启动可协作性源的向导
可协作性源向导启动后(如图2所示),输入下面示例的值:
- 数据类型(Data type)的名称
- 这个新数据类型的命名空间(Namespace)
- 你想将你的参数绑定到的(Bound to)位置。
图2、可协作性源向导
这个参数(Parameter)术语涉及到这个值如何从源portlet传递到目标portlet中。
- None:这个设置表明你不会指定这个值传递的方式,因此属性代理将会使用这个默认行为。
- Render参数:仅支持字符型。这个字符串值将会绑定到
RenderRequest
>对象上。这个render参数可以在动作阶段设置并且在portlet生命周期的render阶段获得。但是它不能在动作阶段取得。 - Request参数:仅支持字符型。这个字符串值将会绑定到
ActionRequest
对象上,并且可以在portlet的生命周期的动作处理阶段得到。这个参数值在请求处理的结束时没有任何意义。(也就是说,当它离开了单个调用时不能持续)。 - Request属性:支持任何JavaBean?类型。这个bean将会绑定到
ActionRequest
对象上。生命周期方面和Request 参数一样。 - Session:支持任何JavaBean?类型。这个bean将会绑定到session对象并且将会在客户端和portal 服务器之间的HTTP session期间持续。
你将把属性值作为request的一个属性。这样你就可以使用复杂的Java类型在portlet之间传递信息,而不用通过在session中存储多余的信息而降低了服务性能。
可协作性源向导生成了一个WSDL文件(如图3所示),它为属性代理描述了这个portlet。注意到这个Summary portlet由一个特殊的图标标记着,表明这是一个属性源。这个WSDL文件包括以下几个部分:
- 类型(Types):这部分描述了数据类型(使用XML schema),它们由源portlet产生。
- 消息(Messages):这部分描述了可以通过portlet产生或引用的消息。
- 端口类型(Port Type):这部分描述了属性代理看到的portlet抽象接口。
- 绑定(Binding):这部分描述了如何实现这个抽象接口(端口类型)。
图3.由可协作性源向导生成的结果
这个可协作性源向导将会自动的产生一个表示消息的简单类型(见列表1)。你需要手动修改所产生的WSDL来描述一个复杂的类型(这是因为我们的portlet需要相互传递值――而不是简单的字符)。列表2显示了这个复杂类型的定义
<xsd:schema targetNamespace="http://crmbrowser.sample.ibm.com"> <xsd:simpleType name="ShowDetailMessageType"> <xsd:restriction base="xsd:string"></xsd:restriction> </xsd:simpleType> </xsd:schema> |
你需要改变这个简单的类型从而正确的描述这个Summary portlet将要发送给细节portlet的消息。这个WebSphere Portal Server Wiring工具稍后将会使用这个类型定义来定义在源和目标之间的一个连接。
<xsd:complexType name="ShowDetailMessageType"> <xsd:all> <xsd:element name="customerDetail" type="tns:CustomerDetailType" minOccurs="0" maxOccurs="1"/> <xsd:element name="issueDetail" type="tns:IssueType" minOccurs="0" maxOccurs="1"/> </xsd:all> </xsd:complexType> <xsd:complexType name="CustomerDetailType"> <xsd:all> <xsd:element name="customerId" type="xsd:string" /> <xsd:element name="firstName" type="xsd:string" /> <xsd:element name="lastName" type="xsd:string" /> <xsd:element name="workPhone" type="xsd:string" /> <xsd:element name="homePhone" type="xsd:string" /> </xsd:all> </xsd:complexType> <xsd:complexType name="IssueType"> <xsd:all> <xsd:element name="issueId" type="xsd:string" /> <xsd:element name="shortDesc" type="xsd:string" /> <xsd:element name="desc" type="xsd:string" /> <xsd:element name="openDate" type="xsd:string" /> <xsd:element name="status" type="xsd:string" /> </xsd:all> </xsd:complexType> |
在你的WSDL中的这些复杂类型是属性代理定义一个连接用的元数据。他们应能够正确的描述从Summary portlet传递到Detail portlet中的bean的内容(目的是为了正确描述这些连接和他们所实现的功能)。然而,在WebSphere Portal Server 5.1中属性代理并不能在运行期执行校验来保证你所传递的bean与这些抽象的定义相对应。
除了类型定义以外,你需要对生成的绑定信息做一些修改。列表3中显示了工具所生成的默认的绑定,而列表4显示了你在制作可协作性行为时所做的手动的修改。
<binding name="Summaryportlet_Binding" type="tns:Summaryportlet_Service"> <portlet:binding /> <operation name="Summaryportlet"> <portlet:action type="standard" /> <output> <portlet:param name="CustomerDetailType" partname="CustomerDetailType_Output" boundTo="request-attribute" /> </output> </operation> </binding> |
列表4.对绑定信息的手动修改(注意:手动修改的部分都用粗体表示)
<binding name="Summaryportlet_Binding" type="tns:Summaryportlet_Service"> <portlet:binding /> <operation name="Summaryportlet"> <portlet:action type="standard" name="sendShowDetailResponse" caption="Send.Detail.Response" description="Send Detail Response" /> <output> <portlet:param name="detailInfo" class="com.ibm.sample.crmbrowser.bean.ShowDetailMessage" partname="ShowDetailType_Output" boundTo="request-attribute" caption="Detail.Info.Type" description="Detail Info Type" /> </output> </operation> </binding> |
生成的WSDL描述的关键变化包括以下几点:
- 在操作元素中添加名称(name)属性
- 在绑定的param元素中添加类(class)属性。(这个类属性指定了在portlet之间传递的Java类型)
- 添加可选择标题(caption)和描述(description)属性(在使用WebSphere Portal Server 5.1提供的连接工具时很必要)。
在列表4中的标记的变化不是简单的格式上的变化,而是在制作portlet消息时本质上的变化。除了在列表3和列表4中所强调的绑定部分的类型变化以外,也建议你修改文件中的消息和端口类型,从而给这些消息和操作提供更多描述性的名称。这在创建更复杂的用户接口包括它们之间的多portlet和连接时有很大的帮助。
假如你选择采用这些变化,你将需要保证这个WSDL在匹配消息、端口类型和绑定部分(这该篇文章的下载部分提供的实例源中有所阐述)中的消息和操作名称时的合法性。
为完成启动你的源portlet,你需要再完成两步:
- 描述一个关键的动作参数,该动作触发了向属性代理的数据传输。
- 写动作实现代码将数据放入这个请求对象
第一步,先在页面设计器中打开CustomerSummaryView.jsp,并选择Customer Id指令超链接。如图4所示在属性视图中编辑动作的属性。
图4. 输入动作参数,该动作触发了通过WPS属性代理传输属性值。
你需要定义的关键参数是com.ibm.portal.propertybroker.action,这个值必须与操作绑定(列表4中的sendShowDetailResponse
)的动作名称属性的定义的值相同
剩下的步骤包括将你的复杂类型――com.ibm.sample.crmbrowser.bean.ShowDetailMessage bean――放入请求对象中。接下来,通过点击在属性视图(如图5所示)中Quick Edit图标为Customer Info链接生成一个动作句柄方法。
图5.为CustomerId生成一个动作句柄代码
如下面的列表5所示完成动作句柄的实现。
列表5.将CustomerDetailInfo放入请求对象中
public String doLink1Action() { // Determine which action link has been clicked and dispatch the corresponding // object to the property broker ShowDetailMessage message = new ShowDetailMessage(); message.setCustomerDetail(getCustomerSummary().getCustomerD etailInfo()); getRequestScope().put("detailInfo", message); return ""; } |
完成上述步骤后。你的Summary portlet能有效的作为一个属性源。尽管如此,假如你回到最初的应用场景中,这个Summary portlet可以向Detail portlet发送两个不同类型的消息。
- 一个是当用户点击customerId链接时发送客户具体信息。
- 另一个时当客户点击issueId链接时发送issue信息
因此本质上,你所要做的是为issueId链接产生动作句柄代码。
- 在页面设计器中选择链接
- 输入参数:
- 1 – 为com.ibm.portal.propertybroker.action 输入同样的参数。
- 2 –配置一个参数用来获取起源动作的链接的值
- 写一个动作句柄代码用com.ibm.sample.crmbrowser.bean.Issue bean的一个合适的实例定位com.ibm.sample.crmbrowser.bean.ShowDetailMessage bean ,然后存储这个bean作为这个请求的一个属性。
创建一个参数,它将会持有第2步所选择的论题的值:
- 在属性视图中点击 Add Parameter 按钮(如图5所示)。
- 在参数表剩下的列中输入
issueId
的名称。 - 点击参数表的右侧的列中—在那个列中将会出现一个按钮。
- 点击按钮提示Select Page Data Object对话框。
- 导航在这个对话框中的对象,找到issues 列表(如图6所示)Issue对象中issueId 域。
图6.将issueId参数映射到一个变量上
这个变量将会传输选定的issueId到我们的动做句柄代码中。这个动作句柄代码(如列表6所示)使用这个参数来取得合适的Issue对象。
public String doLink2Action() { // Type Java code that runs when the component is clicked ShowDetailMessage message = new ShowDetailMessage(); String issueId = (String)getRequestParam().get("issueId"); if (issueId != null) { for (int i = 0; i < getCustomerSummary().getIssues().size(); i++) { Issue issue = (Issue)getCustomerSummary().getIssues().get(i); if (issueId.equals(issue.getIssueId())) { message.setIssueDetail(issue); getRequestScope().put("detailInfo", message); break; } } } return ""; } |
完成这些步骤以后,你的Summary portlet可以作为一个属性源支持你应用设计。
因此本质上,你所要做的是为issueId链接产生动作句柄代码。
- 在页面设计器中选择链接
- 输入参数:
- 1 – 为com.ibm.portal.propertybroker.action 输入同样的参数。
- 2 –配置一个参数用来获取起源动作的链接的值
- 写一个动作句柄代码用com.ibm.sample.crmbrowser.bean.Issue bean的一个合适的实例定位com.ibm.sample.crmbrowser.bean.ShowDetailMessage bean ,然后存储这个bean作为这个请求的一个属性。
创建一个参数,它将会持有第2步所选择的论题的值:
- 在属性视图中点击 Add Parameter 按钮(如图5所示)。
- 在参数表剩下的列中输入
issueId
的名称。 - 点击参数表的右侧的列中—在那个列中将会出现一个按钮。
- 点击按钮提示Select Page Data Object对话框。
- 导航在这个对话框中的对象,找到issues 列表(如图6所示)Issue对象中issueId 域。
图6.将issueId参数映射到一个变量上
这个变量将会传输选定的issueId到我们的动做句柄代码中。这个动作句柄代码(如列表6所示)使用这个参数来取得合适的Issue对象。
public String doLink2Action() { // Type Java code that runs when the component is clicked ShowDetailMessage message = new ShowDetailMessage(); String issueId = (String)getRequestParam().get("issueId"); if (issueId != null) { for (int i = 0; i < getCustomerSummary().getIssues().size(); i++) { Issue issue = (Issue)getCustomerSummary().getIssues().get(i); if (issueId.equals(issue.getIssueId())) { message.setIssueDetail(issue); getRequestScope().put("detailInfo", message); break; } } } return ""; } |
完成这些步骤以后,你的Summary portlet可以作为一个属性源支持你应用设计。
要想为属性值启动这个Detail portlet作为目标,你需要执行下面的几个步骤:
- 生成WSDL为WPS属性代理描述portlet接口。
- 重写所提供的portlet类,并实现
processAction
()
方法。它可以处理来自于属性代理的动作请求,并重定位到相应的视图中。 - 修改生成的页面代码,当页面下载后初始化模型bean。
你将使用可协作性目标向导生成WSDL。启动这个向导,在Project Explorer视图中选择这个portlet,右击它,并点击Cooperative > Enable Target(这个步骤和启动协作性源很相似)。
可协作性源向导如图7所示。你需要填写下面一些值:
- 数据类型(Data type):使用和可协作性源portlet完全一样的名称,生成WSDL以后,你还需要手动修改保证在目标WSDL中定义的类型与在源WSDL中的一样。
- 命名空间(Namespace):同样,这个命名空间和在源中输入的一样。
- 动作(Action):输入任意你想输入的名称。尽管你可以使用浏览按钮定位目标页的动作,这个方法在这种情况下并不推荐,因为目标porlet会动态的转换视图。建议在动作的名称前缀加上receive(并不强制要求)。
- 参数:你可以使用任何名称来标识这个类型实例。你很可能使用类似与类型名称的名称帮助定位不同的类型实例。
- 绑定(Bound to):在这个域中的值与启动源部分所描述的一样。生命周期方法也一样。这种情况下,你将会使用Request Attribute设置。这样保证你的消息(属性值)作为
ActionRequest
对象的属性传递给目标portlet。 - 标签和描述(Label 和 Description):这些项是可选的,但是应该填写上,因为当你使用连接工具时,它们可以帮助你创建连接。在这个域中你可以使用任意的值。
图7.可协作性目标向导
可协作性目标向导生成了一个WSDL文件(如图3所示), 它描述了目标portlet通过属性代理取得的属性。要想启动这个目标,你需要手动编辑所生成的WSDL:
- 修改生成的类型定义使其与源portlet一致。源类型和目标类型必须相同;否则,连接工具将不允许你在单元测试期间创建两个portlet之间的连接。
- 编辑消息、端口类型和绑定的信息使其能够反映出你在使用消息和操作命名时的习惯(在获取相关的消息时是可选择的,但是对于带有很多消息的复杂接口强烈建议你采用)。
- 编辑绑定信息。
图8.由可协作性目标向导生成目标WSDL文件
列表7显示了这个生成的WSDL 绑定。列表7显示了为启动协作性行为在绑定信息中进行的手动的修改。
<binding name="Detailportlet_Binding" type="tns:Detailportlet_Service"> <portlet:binding /> <operation name="Detailportlet"> <portlet:action name="receiveShowDetailRequest" type="standard" caption="Receive.Detail.Request" description="Receive Detail Request" /> <input> <portlet:param name="detailInfo" partname="ShowDetailMessageType_Input" boundTo="request-attribute" /> </input> </operation> </binding> |
列表8. 对WSDL部分的手动修改(修改部分用粗体标志)
<binding name="DetailPortlet_Binding" type="tns:DetailPortlet_Service"> <portlet:binding /> <operation name="receiveShowDetailRequest"> <portlet:action name="receiveShowDetailRequest" type="standard" caption="Receive.Detail.Request" description="Receive Detail Request" /> <input> <portlet:param name="detailInfo" class= "com.ibm.sample.crmbrowser.bean.ShowDetailMessage" partname="ShowDetailType_Input" boundTo="request-attribute" caption="Detail.Info.Type" description="Detail Info Type" /> </input> </operation> </binding> |
对绑定信息(并不仅仅依据你的习惯性的选择)的相当重要的修改包括添加一个类属性,它描述了整个符合条件的java类型通过属性代理传递给目标。这个类型必须与启动源portlet使用的属性一样。在这种情况下它就是com.ibm.sample.crmbrowser.bean.ShowDetailMessage。你同样也必须添加标题和描述信息到这个param
元素中。这在你创建连接时很有帮助。
完成这个目标属性的WSDL描述后,你需要重写所提供的portlet类和实现processAction()方法。IRAD JSR 168 Faces portlet项目提供一个促进JSF portlet开发的类-- com.ibm.faces.webapp.FacesGenericPortlet。这个类可以在jsf-portlet.jar文件中找到。
该类的实现包括一些限制和不能处理的地方:
- 动态实体转化
- 处理用户自定义的数据类型
为了克服这些限制,你需要重写目标portlet中的processAction()方法。此外你需要在目标portlet中写某个processAction方法。还要写某些客户代码处理来自于源portlet的消息。
创建重写方法,按照以下几个简单的步骤:
- 在JavaSource目录下创建一个新的包(在示例代码中使用com.ibm.sample.crmbrowser.portlet)。
- 创建一个命名为DetailPortlet新的Java类。
- 在New Class向导中,扩展com.ibm.faces.webapp.FacesGenericPortlet。
- 打开Java编辑器后,点击里面的类的定义并按Ctrl+Space键得到编码帮助。
- 在编码帮助窗口中选择processAction()。
编辑器将会生成一个processAction()重写的内核实现,它包括一个对基类的调用。
列表9显示了你的DetailPortlet.processAction()方法。在这个方法的实现中,你需要执行一些关键的动作:
- 在ActionRequest中查找com.ibm.portal.propertybroker.action属性。它会告诉你由属性代理所触发的动作(它与终端用户点击一个在当前portlet视图中的动作控制相反)。
- 从ActionRequest中获取复杂类型。
- 基于这个类型的内容来决定显示哪个视图。
- 在session中存储复杂类型。
- 重定位这个portlet到一个合适的视图中。它可以通过设置com.ibm.faces.portlet.page.view session属性指向想要得到的视图JSP的URL。当处理请求响应序列的提交阶段时,FacesGenericPortlet实现将使用这个属性显示正确的视图。
- 调用基类实现来处理其它所有的事件(为视图的动作保持一个默认的行为)。
列表9. DetailPortlet.processAction()方法的实现。
public void processAction(ActionRequest request, ActionResponse response) throws PortletException { // Reterive target action name String actionName = (String)request.getParameter ("com.ibm.portal.propertybroker.action"); if (actionName != null) { // Invoke custom processing only if property broker action String actionURL = ""; if(actionName.equals("receiveShowDetailRequest")) { ShowDetailMessage detailInfo = (ShowDetailMessage)request.getAttribute ("detailInfo"); if (detailInfo.getCustomerDetail() != null) actionURL = "/CustomerDetailView.jsp"; else actionURL = "/IssueDetailView.jsp"; request.getportletession().setAttribute("detailInfo", detailInfo); } if (!actionURL.equals("")) request.getportletession().setAttribute ("com.ibm.faces.portlet.page.view", actionURL); } // Otherwise invoke default behaviour super.processAction(request, response); } |
|
|
剩下的编码是要确保目标视图在portlet加载它时能适当地进行初始化。按照JSF架构,最好的时机是当作为JSF页的模型的bean被创建的时候。IRAD JSF工具生成JavaBean,你可以与JSF控制器进行程序上的交互(如图9中所示)。
图9. JSF工具生成的包和bean
列表10显示了在IssueDetailView.java 中pagecode.IssueDetailView.getIssue() 方法的实现。该方法在JSF运行时重构对象树时的任意时间内被调用,这棵树将会最终为终端用户创建一个HTML网页。在这个方法的实现中关键的是:
- 只有当结果为空时进行初始化。
- 我们这里所说的复杂类型――Issue――由session中获取。
列表10.实现pagecode.IssueDetailView.getIssue()方法。
public Issue getIssue() { if (issue == null) { ShowDetailMessage detailInfo = (ShowDetailMessage)getSessionScope().get("detailInfo"); if (detailInfo != null) issue = detailInfo.getIssueDetail(); if (issue == null) issue = new Issue(); } return issue; } |
为了使这个应用更加完整的实现,需要为pagecode.CustomerDetailView.getCustomerDetail()写一个类型的方法实现。(你可以在这篇文章的下载部分看到更多的有关代码的信息)。
剩下的最后一步是对该应用进行单元测试。如果你还没有这样做的话,按照下面的步骤创建一个新的服务器:
- 在 Server 的视图中,右击显示内容菜单,然后点击New > Server。
- 在NewServer向导中选择WebSphere Portal 5.1 Unit Test Environment。
- 为这个服务器的配置应用的列表添加CRMBrowserEAR。
- 点击Finish退出这个New Server向导。
- 双击这个在Servers视图中的新的服务器,打开服务配置编辑器。
- 转换到Portal页,并选择Enable base portlet for portal administration and customization复选框。
- 保存你所做的修改并退出编辑器。
在Project Explorer视图中选择CRMBrowser动态Web项目。
你的WebShere Portal UTE现在可以打开,并且一个显示你的portlet的浏览器将会自动打开。
下一步你需要定义你的Summary portlet和Detail portlet之间的连接。这需要完成下面的几个步骤:
- 点击浏览器中的portal接口的Edit Page页。
- 点击Wires页
- 使用Wires页面中的下拉框定义Summary portlet(源)和Detail portlet(目标)之间的一个连接。这需要首先选择Source Portlet,然后选择Sending动作,在选择Receiving Portlet,最多在合适的下拉框中选择Receiving动作(如图10所阐述的那样)。
- 点击Done退出该页并返回到你的应用UI中。
图10.Portlet连接工具
现在你可以开始使用CRMBrowser应用。需要变更Detail portlet中的视图――显示客户具体信息或问题的具体信息――通过Summary portlet视图中你所点击的动作链接。
WebSphere Portal Server Property Broker使得你可以在portlet之间传递复杂的数据。你可以使用这个特性实现portlet之间复杂的用户接口流。IRAD提供了能够帮助你在开发中利用Property Broker的特性的工具。你可以通过IRAD重写所生成的代码从而加强它的性能并增强应用的行为。要想学习这方面更多的技术,请浏览下面的Resourses部分中列出的其它文章和参考资料。
本文转自kenty博客园博客,原文链接http://www.cnblogs.com/kentyshang/archive/2008/03/06/1093234.html如需转载请自行联系原作者
kenty