Struts 2与AJAX

转贴自http://www.blogjava.net/max/archive/2007/06/12/123682.html

在当今——Web 2.0概念铺天盖地的Internet环境下,简易的AJAX集成对于一个成功的WEB框架来说是不可或缺的。因此,Struts 2其中的一个重要的功能(Feature)就是“First-class AJAX support - Add interactivity and flexibility with AJAX tags that look and feel just like standard Struts tags(大意:一流的AJAX支持——通过AJAX标志增加互动性和灵活性,而且使用这些AJAX标志与普通的Struts标志同样简单)”。

实现原理

基于不重新发明轮子的原则,Struts 2并没有开发新的AJAX框架,而是使用时下Java EE平台中比较流行的AJAX框架——Dojo和DWR。

最近在Musachy Barroso等同志的无私奉献下,开发了Struts 2的JSON插件(Plugin),极大地方便了我们输出JSON结果(Result)。

JSON插件(Plugin)

在Struts 2的showcase中的AJAX部分,JSON的结果输出是通过Freemaker模板实现。这种方法在简易性和灵活性上都比不上JSON插件,所以JSON插件值得向大家五星推荐。

下面让我们看一个JSON插件的例子。

首先到以下网址http://code.google.com/p/jsonplugin/downloads/list下载JSON插件的JAR包,并将其加入你的WebContent/WEB-INF/lib下。

接下是本例子的Action代码:

package tutorial;

import java.util.ArrayList;
import java.util.List;

import com.googlecode.jsonplugin.annotations.JSON;
import com.opensymphony.xwork2.ActionSupport;

public class JsonPluginAction extends ActionSupport {
   
private static final long serialVersionUID = -6784977600668791997L;
   
   
private int bookId;
   
private String title;
   
private double price;
   
private List<String> comments;    
   
private transient String secret1;
   
private String secret2;

   @JSON(name
="ISBN")
   
public int getBookId() {
       
return bookId;
   }


   
public void setBookId(int bookId) {
       
this.bookId = bookId;
   }


   
public List<String> getComments() {
       
return comments;
   }


   
public void setComments(List<String> comments) {
       
this.comments = comments;
   }


   
public double getPrice() {
       
return price;
   }


   
public void setPrice(double price) {
       
this.price = price;
   }


   
public String getTitle() {
       
return title;
   }

   
   
public void setTitle(String title) {
       
this.title = title;
   }


   @Override
   
public String execute() {
       bookId
= 15645912;
       title
= "Max On Java";
       price
= 0.9999d;
       comments
= new ArrayList<String>(3);
       comments.add(
"It's no bad!");
       comments.add(
"WOW!");
       comments.add(
"No comment!");
       secret1
= "You can't see me!";
       secret2
= "I am invisible!";
       
return SUCCESS;
   }

}
清单1 src/tutorial/JsonPluginAction.java

以上代码值得注意的是,通过@JSON的JAVA注释(Annotation),我们可以改变JSON结果的属性名称,另外带有transient修饰符与没有Getter方法的字段(field)都不会被串行化为JSON。

然后,我们来配置一下此Action,代码如下:

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

<! DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd"
>

< struts >    
   
< package name ="Struts2_AJAX_DEMO" extends ="json-default" >
       
< action name ="JsonPlugin" class ="tutorial.JsonPluginAction" >
           
< result type ="json" />
       
</ action >            
   
</ package >
</ struts >
清单2 src/struts.xml

上面配置文件的“package”元素和以往不同的是,它扩展了“json-default”而不是“struts-default”。“json -default”是在jsonplugin-0.11.jar包里的struts-plugin.xml中定义的。该文件同时定义了“json”的结果 类型,有兴趣的朋友可以打开此文件看看。

发布运行应用程序,在浏览器中键入:http://localhost:8080/Struts2_Ajax/JsonPlugin.action,出现下载文件对话框,原因是JSON插件将HTTP响应(Response)的MIME类型设为“application/json”。把文件下载下来,用记事本打开,内容如下:

{ " ISBN " : 15645912 , " comments " :[ " It's no bad! " , " WOW! " , " No comment! " ], " price " : 0.9999 , " title " : " Max On Java " }
清单3 例子1输出的JSON串

当然这还不是一个完整的AJAX的例子,下面让我们写一个HTML文件将其完成,HTML代码如下:

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
   
< title > JSON Plugin </ title >
   
< script type ="text/javascript" >    
   
var bXmlHttpSupport = ( typeof XMLHttpRequest != " undefined " || window.ActiveXObject);
     
   
if ( typeof XMLHttpRequest == " undefined " && window.ActiveXObject) {
       
function XMLHttpRequest() {
           
var arrSignatures = [ " MSXML2.XMLHTTP.5.0 " , " MSXML2.XMLHTTP.4.0 " ,
                                 
" MSXML2.XMLHTTP.3.0 " , " MSXML2.XMLHTTP " ,
                                 
" Microsoft.XMLHTTP " ];
                             
           
for ( var i = 0 ; i < arrSignatures.length; i ++ ) {
               
try {        
                   
var oRequest = new ActiveXObject(arrSignatures[i]);            
                   
return oRequest;        
                }
catch (oError) { /* ignore */ }
            }          
    
           
throw new Error( " MSXML is not installed on your system. " );               
        }
    }    
            
   
function retrieveBook() {        
       
if (bXmlHttpSupport) {
           
var sUrl = 'JsonPlugin.action';
           
var oRequest = new XMLHttpRequest();
            oRequest.onreadystatechange
= function () {
               
if (oRequest.readyState == 4 ) {                    
                   
var oBook = eval('(' + oRequest.responseText + ')');
                   
var bookHolder = document.getElementById('bookHolder');
                   
var sBook = ' < p >< b > ISBN: </ b > ' + oBook.ISBN + ' </ p > ';
                    sBook
+= (' < p >< b > Title: </ b > ' + oBook.title + ' </ p > ');
                    sBook
+= (' < p >< b > Price: </ b > $' + oBook.price + ' </ p > ');
                    sBook
+= (' < b >< i > Comments: </ i ></ b >< hr /> ');
                   
for (i = 0 ; i < oBook.comments.length; i ++ ) {
                        sBook
+= (' < p >< b > #' + (i + 1 ) + ' </ b > ' + oBook.comments[i] + ' </ p > ');
                    }
                    bookHolder.innerHTML
= sBook;
                }
            };
            oRequest.open('POST', sUrl);
            oRequest.send(
null );
        }
    }
   
</ script >
</ head >
< body >
   
< input type ="button" value ="Retrieve Book" onclick ="retrieveBook()" />
   
< div id ="bookHolder" ></ div >
</ body >
</ html >
清单4 WebContent/JsonPlugin.html

以上代码中,我没有使用任何的AJAX的Javascript包,而是参考《Professional Javascript For Web Developer》手工创建XHR(XMLHttpRequest),并在XHR完成后使用eval()方法将JSON字符串变为JSON对象。需要注 意的是,要调用eval函数时,必须使用“(”和“)”将JSON字符串括起来,否则会出错的。

打开http://localhost:8080/Struts2_Ajax/JsonPlugin.html,点击“Retrieve Book”按钮,页面如下图所示:

图1 JsonPlugin.html页面输出
图1 JsonPlugin.html页面输出

Struts 2与Dojo

Dojo是开源Javascript工具包,它引了Widget的概念,方便了Javascript面向对象编程(OOP),改进Javascript的事件模型。在此我不打算对此进行深入的讲解,有兴趣的朋友的可以找网上找一些关于Dojo的资料学习。

Struts 2基于Dojo编写一些AJAX标志(在Dojo中称为Widget),要使用这些标志的AJAX功能,需要将标志的“theme”属性设为 “ajax”。同时,亦需要将加入在<head>与</head>之间加入<s:head theme="ajax" />。当使用这些标志的AJAX功能,有些属性可能会经常用到,所以我会对这些属性稍作解释。

名称描述
hrefXHR(XMLHttpRequest)请求的地址
listenTopics监听的Dojo话题(Topic)以触发自身,如可以在可以通过发布(Publish)相应的话题,通知<s:autocompleter />重新加载其备选项(Options)
notifyTopics完成远程调用后,发出通知,触发相应的Javascript函数或Dojo Widget
formId 需要提交到服务器的表单的ID
formFilter过滤表单字段的Javascript函数名称
indicator在XHR处理过程中,包含用户提示的信息的HTML元素的ID,如图片或DIV等
表1 常用的AJAX标志属性

这些标志包括:<s:a />、<s: submit />、<s:autocompleter />和<s:tree />等,下面我将分别讲解。

1、<s:a />和<s:submit />

这两个标志方便了我们的调用XHR实现AJAX,所以上面的HTML如果使用了这两标志将会变得更简单,因为我们不用再去理会繁锁的XHR创建和设定的工作。下面是示例代码:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > JSON Plugin </ title >
       
< s:head theme ="ajax" />
       
< script type ="text/javascript" >                    
        dojo.addOnLoad(
function () {
            dojo.event.topic.subscribe('retrieveBook',
this , function (data, type, e){
               
if (type == 'load') {
                    showBook(data);
                }
else if (type == 'error') {
                    alert('Can not retrieve the book');
                }
            });
        });
        
       
function showBook(strBook) {
           
var oBook = eval('(' + strBook + ')');
           
var bookHolder = document.getElementById('bookHolder');
           
var sBook = ' < p >< b > ISBN: </ b > ' + oBook.ISBN + ' </ p > ';
            sBook
+= (' < p >< b > Title: </ b > ' + oBook.title + ' </ p > ');
            sBook
+= (' < p >< b > Price: </ b > $' + oBook.price + ' </ p > ');
            sBook
+= (' < b >< i > Comments: </ i ></ b >< hr /> ');
           
for (i = 0 ; i < oBook.comments.length; i ++ ) {
                sBook
+= (' < p >< b > #' + (i + 1 ) + ' </ b > ' + oBook.comments[i] + ' </ p > ');
            }
            bookHolder.innerHTML
= sBook;
        }
       
</ script >
   
</ head >
   
< body >
       
< s:url id ="bookUrl" value ="/JsonPlugin.action" />
       
< s:submit href ="%{bookUrl}" theme ="ajax" indicator ="indicator"
            value
="Retrieve Book" align ="left" notifyTopics ="retrieveBook" />
       
< s:a theme ="ajax" href ="%{bookUrl}" indicator ="indicator"
            notifyTopics
="retrieveBook" > Retrieve Book </ s:a >
       
< img id ="indicator"
            src
="${pageContext.request.contextPath}/images/indicator.gif"
            alt
="Loading" style ="display:none" />
       
< div id ="bookHolder" ></ div >
   
</ body >
</ html >
清单5 WebContent/LinkButton.jsp

可能上述代码还不够简洁,因为我将HTML格式化的工作都放在Javascript中完成。但如果你的XHR返回的是HTML片段,你可以简单地将 <s:a />或<s:submit />的“targets”属性设为“bookHolder”即可,详情大家可以参考Struts 2 Showcase。至于返回HTML片段,可以通过Action + Freemaker完成。

2、<s:autocompleter />

Autocomplete是比较经典的AJAX应用,虽然谷歌已经停止使用这一功能,但就Autocompleter自身而言的确是很酷的。下面是一个<s:autocompleter />的例子。

首先,我要伪造一些字符串数据,代码如下:

package tutorial;

import java.util.ArrayList;
import java.util.List;

public final class Datas {
   
public static final List<String> NAMES;
   
static {
       NAMES
= new ArrayList<String>();
       NAMES.add(
"Alabama");
       NAMES.add(
"Alaska");
       NAMES.add(
"American Samoa");
       NAMES.add(
"Arizona");
       NAMES.add(
"Arkansas");
       NAMES.add(
"Armed Forces Europe");
       NAMES.add(
"Armed Forces Pacific");
       NAMES.add(
"Armed Forces the Americas");
       NAMES.add(
"California");
       NAMES.add(
"Colorado");
       NAMES.add(
"Connecticut");
       NAMES.add(
"Delaware");
       NAMES.add(
"District of Columbia");
       NAMES.add(
"Federated States of Micronesia");
       NAMES.add(
"Florida");
       NAMES.add(
"Georgia");
       NAMES.add(
"Guam");
       NAMES.add(
"Hawaii");
       NAMES.add(
"Idaho");
       NAMES.add(
"Illinois");
       NAMES.add(
"Indiana");
       NAMES.add(
"Iowa");
       NAMES.add(
"Kansas");
       NAMES.add(
"Kentucky");
       NAMES.add(
"Louisiana");
       NAMES.add(
"Maine");
       NAMES.add(
"Marshall Islands");
       NAMES.add(
"Maryland");
       NAMES.add(
"Massachusetts");
       NAMES.add(
"Michigan");
       NAMES.add(
"Minnesota");
       NAMES.add(
"Mississippi");
       NAMES.add(
"Missouri");
       NAMES.add(
"Montana");
       NAMES.add(
"Nebraska");
       NAMES.add(
"Nevada");
       NAMES.add(
"New Hampshire");
       NAMES.add(
"New Jersey");
       NAMES.add(
"New Mexico");
       NAMES.add(
"New York");
       NAMES.add(
"North Carolina");
       NAMES.add(
"North Dakota");
       NAMES.add(
"Northern Mariana Islands");
       NAMES.add(
"Ohio");
       NAMES.add(
"Oklahoma");
       NAMES.add(
"Oregon");
       NAMES.add(
"Pennsylvania");
       NAMES.add(
"Puerto Rico");
       NAMES.add(
"Rhode Island");
       NAMES.add(
"South Carolina");
       NAMES.add(
"South Dakota");
       NAMES.add(
"Tennessee");
       NAMES.add(
"Texas");
       NAMES.add(
"Utah");
       NAMES.add(
"Vermont");
       NAMES.add(
"Virgin Islands, U.S.");
       NAMES.add(
"Virginia");
       NAMES.add(
"Washington");
       NAMES.add(
"West Virginia");
       NAMES.add(
"Wisconsin");
       NAMES.add(
"Wyoming");
   }

}
清单6 src/tutorial/Datas.java

然后是用于获取和过滤数据的Action,代码如下:

package tutorial;

import java.util.ArrayList;
import java.util.List;

import com.opensymphony.xwork2.ActionSupport;

public class AutocompleterAction extends ActionSupport {
   
private static final long serialVersionUID = -8201401726773589361L;
   
   
private List<String[]> names;    
   
private String start;
   
   
public void setStart(String start) {
       
this.start = start;
   }

   
   
public List<String[]> getNames() {
       
return names;
   }


   @Override
   
public String execute() {
       names
= new ArrayList<String[]>();
       
if(start == null || "".equals(start.trim())) {
           start
= "a";
       }

       
for(String s : Datas.NAMES) {
           
if(s.toLowerCase().startsWith(start.toLowerCase())) {
               names.add(
new String[]{ s, s });
           }

       }

       
return SUCCESS;
   }

}
清单7 src/tutorial/AutocmpleterAction.java

上述Action会以JSON的形式返回以start开头的Datas.NAMES的中字符串,以下是此Action的配置:

< action name ="Autocompleter" class ="tutorial.AutocompleterAction" >
   
< result type ="json" >
       
< param name ="root" > names </ param >
   
</ result >
</ action >
清单8 Autocompleter Action的配置代码片段

在JSON类型结果的参数中加入“root”参数可以设定输出JSON结果的根,以上述情况为例,如果没有“root”参数,输出将为“{ "names": [ ["xxx", "xxx"]...] }”,加了之后变就会成“[ ["xxx", "xxx"]...] ”。接下来,让我们看看页面的代码:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - Autocompleter </ title >
       
< s:head theme ="ajax" />
   
</ head >
   
< body >
       
< h2 >
            Autocompleter
       
</ h2 >
       
< s:form action ="autocompleterForm" >
           
< s:textfield label ="abc" name ="abc" />
           
< tr >
               
< td class ="tdLabel" >
                   
< label class ="label" >
                        No AJAX Autocompleter:
                   
</ label >
               
</ td >
               
< td >
                   
< s:autocompleter theme ="simple" name ="user"
                        list
="@tutorial.Datas@NAMES" />
               
</ td >
           
</ tr >
           
< tr >
               
< td class ="tdLabel" >
                   
< label class ="label" >
                        AJAX Autocompleter:
                   
</ label >
               
</ td >
               
< td >
                   
< s:url id ="dataUrl" value ="/Autocompleter.action" />
                   
< s:autocompleter theme ="ajax" name ="start" href ="%{dataUrl}"
                        loadOnTextChange
="true" loadMinimumCount ="1" indicator ="indicator"
                        autoComplete
="false" showDownArrow ="false" />
                   
< img id ="indicator"
                        src
="${pageContext.request.contextPath}/images/indicator.gif"
                        alt
="Loading" style ="display:none" />
               
</ td >
           
</ tr >
       
</ s:form >
   
</ body >
</ html >
清单9 WebContent/Autocompleter.jsp

上述页面包含两个<s:autocompleter />标志,前者使用“simple”模板,所以不具有AJAX功能,它的数据将以HTML方式输出到最终页面里;而后者则使用了“ajax”模板, 每当输入框的值发生改变时,它都向URL“/Autocompleter.action”发送请求,Action根据请求中的start参数的值,返回相 当的JSON,在请求完成后页面通过回调函数改变输入框的下拉提示,效果如下图所示:

图2 Autocompleter.jsp页面输出 
图2 Autocompleter.jsp页面输出

3、<s:tree />

树是是比较常用的数据结构,因为它可以很好地体现真实世界中对象之间的关系。<s:tree />的使用也相对简单,但需要说明的是——Struts 2.0.6 GA版本的<s:tree />是有BUG的,大家可以点击这个链接https://issues.apache.org/struts/browse/WW-1813了 解详细的情况。这个BUG主要是在<s:tree />的通过“treeCollapsedTopic”、“treeExpandedTopic”和“treeSelectedTopic”设定的话 题(Topic)都没有起作用,上述链接相应给出了解决方法,但我认为该方法太麻烦(需要自己重新编译和打包Struts 2),所以下面的例子,我将另辟徯径,请参考以下代码。

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - Tree </ title >
       
< s:head theme ="ajax" debug ="true" />
       
< script type ="text/javascript" >
           
function treeNodeSelected(arg) {
                alert(arg.source.title
+ ' selected');
            }
            dojo.addOnLoad(
function () {                
               
var s = dojo.widget.byId('parentId').selector;                
                dojo.event.connect(s, 'select', 'treeNodeSelected');
            });
       
</ script >
   
</ head >
   
< body >
       
< h2 >
            Tree
       
</ h2 >
       
< div style ="float:left; margin-right: 50px;" >
           
< s:tree label ="parent" id ="parentId" theme ="ajax"
                templateCssPath
="/struts/tree.css" showRootGrid ="true"
                showGrid
="true" >
               
< s:treenode theme ="ajax" label ="child1" id ="child1Id" >
                   
< s:treenode theme ="ajax" label ="grandchild1" id ="grandchild1Id" />
                   
< s:treenode theme ="ajax" label ="grandchild2" id ="grandchild2Id" />
                   
< s:treenode theme ="ajax" label ="grandchild3" id ="grandchild3Id" />
               
</ s:treenode >
               
< s:treenode theme ="ajax" label ="child2" id ="child2Id" />
               
< s:treenode theme ="ajax" label ="child3" id ="child3Id" />
               
< s:treenode theme ="ajax" label ="child4" id ="child4Id" />
               
< s:treenode theme ="ajax" label ="child5" id ="child5Id" >
                   
< s:treenode theme ="ajax" label ="gChild1" id ="gChild1Id" />
                   
< s:treenode theme ="ajax" label ="gChild2" id ="gChild2Id" />
               
</ s:treenode >
           
</ s:tree >
       
</ div >
   
</ body >
</ html >
清单10 WebContent/Tree.jsp

因为Dojo的树控件,即使在没有设定“selector”情况下,也会自动生成一个默认的Selector,所以只要将其事件绑定到特定的事件处理函数即可。

打开http://localhost:8080/Struts2_Ajax/Tree.jsp,点击任一树节点,页面如下图所示:

图3 Tree.jsp页面输出 
图3 Tree.jsp页面输出

总结

我原本打算用一篇文章写完这个“Struts 2与AJAX”。不过在写的过程中,发现内容越来越多。如果勉强写成一篇,朋友们读起来也会很麻烦,所以我决定分开几部分,本文为第一部分。

另外,之前有的朋友建议我建一个Google的讨论组,方便大家讨论问题。我觉得这个提议非常好,一直以来都是“一人写,大家留言”,这种相对单向的方式不免有所欠缺,而且本人所知也有限,开个讨论组大家可以相互讨论,共同进步。

因为Struts2已经被创建,所以申请了“struts2cn”,有兴趣的朋友,欢迎加入。


Struts 2与AJAX(第二部分)

在上一篇文章《Struts 2与AJAX(第一部分)》,我已经简单地介绍了<s:tree />的一些用法,接下来我将继续深入讲解<s:tree />的使用和通过DWR实现AJAX校验。

更多<s:tree />

在Struts 2的showcase中有两个<s:tree />的例子,分别是静态树与动态树。所谓的静态树即是在编写JSP代码时通过<s:treenode />生成树节点。我的上一篇文章的例子就是一个典型的静态树。而动态树则是在程序运行期间,Struts 2 运行时(Runtime)根据程序中的数据动态创建树节点。虽然在两个例子中<s:tree />的theme属性都为“ajax”,但是从严格意义上来说,这两种树都不属于AJAX树,因为它们都是在输出页面时将全部节点加载到其中,而不是在父节点展开时通过XHR(XMLHttpRequest)获取节点数据。

动态树

下面我们先看一下动态树的例子,接着再一步步地将其改造为名副其实的AJAX 树。下例将会把WEB应用程序的目录树展现在JSP页面中。因此,我需要先包装一下java.io.File 类,代码如下:

package tutorial;

import java.io.File;

public class FileWrapper {
   
private File file;

   
public FileWrapper(String path) {
       file
= new File(path);
   }

   
   
public FileWrapper(File file) {
       
this.file = file;
   }

   
   
public String getId() {
       
return "file_" + file.hashCode();
   }

   
   
public String getName() {
       
return file.getName();
   }

   
   
public String getAbsolutePath() {
       
return file.getAbsolutePath();
   }

   
   
public FileWrapper[] getChildren() {
       File[] files
= file.listFiles();
       
if(files != null && files.length > 0) {
           
int length = files.length;
           FileWrapper[] wrappers
= new FileWrapper[length];
           
for(int i = 0; i < length; ++i) {
               wrappers[i]
= new FileWrapper(files[i]);
           }

           
return wrappers;
       }

       
return new FileWrapper[0];
   }

}
清单1 src/tutorial/FileWrapper.java

之所以需要对File类进行如此包装,是因为<s:tree />用于动态树时,rootNode、nodeIdProperty、nodeTitleProperty 和 childCollectionProperty等属性都必填的。

然后是Action类的代码如下:

package tutorial;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;

import com.opensymphony.xwork2.ActionSupport;

public class DynamicTreeAction extends ActionSupport implements ServletRequestAware {
   
private static final long serialVersionUID = 1128593047269036737L;
   
   
private HttpServletRequest request;
   
private FileWrapper root;

   
public void setServletRequest(HttpServletRequest request) {    
       
this.request = request;
   }

   
   
public FileWrapper getRoot() {
       
return root;
   }

   
   @Override
   
public String execute() {
       root
= new FileWrapper(request.getSession().getServletContext().getRealPath("/"));        
       
return SUCCESS;
   }

}
清单2 src/tutorial/DynamicTreeAction.java

上述代码取得WEB应用程序的根目录的绝对路径后,初始化FileWrapper对象root。该对象将为JSP页面的<s:tree />的根节点。如下代码所示:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - More Tree </ title >
       
< s:head theme ="ajax" debug ="true" />
       
< script type ="text/javascript" >
       
/* <![CDATA[ */
           
function treeNodeSelected(arg) {
                alert(arg.source.title
+ ' selected');
            }
            
           
function treeNodeExpanded(arg) {
                alert(arg.source.title
+ ' expanded');
            }
            
           
function treeNodeCollapsed(arg) {
                alert(arg.source.title
+ ' collapsed');
            }
            
            dojo.addOnLoad(
function () {                
               
var t = dojo.widget.byId('appFiles');
                dojo.event.topic.subscribe(t.eventNames.expand, treeNodeExpanded);                
                dojo.event.topic.subscribe(t.eventNames.collapse, treeNodeCollapsed);
                
               
var s = t.selector;                
                dojo.event.connect(s, 'select', 'treeNodeSelected');
            });
       
/* ]]> */    
       
</ script >
   
</ head >
   
< body >
       
< h2 >
            Dynamic Tree Example
       
</ h2 >
       
< div style ="float:left; margin-right: 50px;" >
           
< s:tree id ="appFiles" theme ="ajax" rootNode ="root"
                nodeTitleProperty
="name" nodeIdProperty ="id"
                childCollectionProperty
="children" />
       
</ div >
   
</ body >
</ html >
清单3 WebContent/Tree.jsp

因为<s:tree />的treeCollapsedTopic和treeExpandedTopic属性都没有起作用,所以如果我们想要监听这两个事件,就必须使用上述代码的方法。

最后是struts.xml配置文件:

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

<! DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd"
>

< struts >
   
< package name ="Struts2_AJAX_DEMO" extends ="struts-default" >
       
< action name ="DynamicTree" class ="tutorial.DynamicTreeAction" >
           
< result > Tree.jsp </ result >
       
</ action >
   
</ package >
</ struts >
清单4 src/struts.xml

发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/DynamicTree.action,有如下图所示页面:


图1 动态树示例

AJAX 树

正如我在文章开头所说,Struts 2所提供的静态树和动态树都不是严格意义上的AJAX树。下面就让我们来实现一个如假包换的AJAX树。首先要说明的是,Struts 2的<s:tree />默认是不支持这种按需加载数据的AJAX树。不过因为它是基于Dojo的树控件(Widget)所以要扩展也很方便。

Dojo 通过名为“TreeRPCController”的控件实现 AJAX 树,它会监听被控制树的事件。当发生展开节点的事件时,TreeRPCController就会向URL发送XHR请求,该URL由 TreeRPCController的RPCUrl 属性定义。XHR请求格式类似如下格式:

http://localhost: 8080 /Struts2_Ajax2/AjaxTree.action?action = getChildren&data = { " node " :{ " widgetId " : " file_226092423 " , " objectId " : " C://Program Files//Tomcat 5.5//webapps//Struts2_Ajax2 " , " index " : 0 , " isFolder " :true} , " tree " :{ " widgetId " : " appFiles " , " objectId " : "" }}&dojo.preventCache = 1182913465392
清单5 XHR样本

显而易见,请求中包含三个参数,分别是action为“getChildren”(固定值),data一个包含当前节点与树信息的JSON串和 dojo.preventCache随机串,用于缓存不同节点的请求响应(父节点只会在第一次被展开时到服务器端加载数据,之后都是从浏览器的缓存中读取 数据,可以提高应用程序性能)。

首先我要先写一个加载树节点数据的Action类,代码如下:

package tutorial;

import java.util.Map;

import com.googlecode.jsonplugin.JSONExeption;
import com.googlecode.jsonplugin.JSONUtil;

public class AjaxTreeAction extends DynamicTreeAction {
   
private static final long serialVersionUID = 3970019751740942311L;

   
private String action;
   
private String data;
   
private FileWrapper[] wrappers;

   
public void setAction(String action) {
       
this.action = action;
   }

   
   
public void setData(String data) {
       
this.data = data;
   }


   
public FileWrapper[] getWrappers() {
       
return wrappers;
   }


   @Override
   
public String execute() {
       
if("getChildren".equals(action)) {
           
try {
               Object o
= JSONUtil.deserialize(data);
               String path
= ((Map) ((Map) o).get("node")).get("objectId").toString();
               wrappers
= new FileWrapper(path).getChildren();
           }
catch (JSONExeption e) {
               e.printStackTrace();
           }

           
return "ajax";
       }
   
       
return super.execute();
   }

}
清单6 src/tutorial/AjaxTreeAction.java

上述代码可能需要解释一下:

  1. action属性对应于XHR中的action,如果它为“getChildren”时,则需要进行加载子节点操作。否则,会读取树的根节点,并返回JSP页面;
  2. 通过上面XHR的分析,大家可以知道data是代表树和当前节点的JSON串,故应将其反串行化为Map对象,并将其 objectId属性取出。通常情况下,Dojo树的objectId属性代表服务器端的对象的标识,在本例中为文件夹的绝对路径;
  3. wrappers属性表示当前文件夹下的文件数组,它被传送到Freemarker页面,翻译为Dojo树节点数组的JSON串。

下面是Freemarker页面的代码:

[
< #list wrappers as r >
    { "title": "${r.name}", "isFolder":
< #if r.children?size gt 0 > true < #else > false </ #if > , "id": "${r.id}", "objectId": "${r.absolutePath?js_string}" } < #if r_has_next > , </ #if >
</ #list >
]
清单7 WebContent/AjaxTree.ftl

以上代码中<#list></#lsit>的写法是Freemarker中遍历集合的写法;而<#if r.children?size gt 0>判断“r”对象的children属性是否为空;r.absolutePath?js_string 就是将“r”的absolutePath属性的值输出为Javascript 的字串符形式;<#if r_has_next></#if>判断集合是否有下一项数据。如果希望更详细地了解Freemarker的使用,请参考该手册

接下来,让我们看看Action的配置代码片段:

        < action name ="AjaxTree" class ="tutorial.AjaxTreeAction" >
           
< result > AjaxTree.jsp </ result >
           
< result name ="ajax" type ="freemarker" > AjaxTree.ftl </ result >
       
</ action >
清单8 src/struts.xml配置片段

最后是JSP页面代码:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - More Tree </ title >
       
< s:head theme ="ajax" debug ="true" />
       
< script type ="text/javascript" >
       
/* <![CDATA[ */
           
function treeNodeSelected(arg) {
                alert(arg.source.title
+ ' selected');
            }
                
            dojo.addOnLoad(
function () {                
               
var t = dojo.widget.byId('appFiles');                
               
var s = t.selector;                
                dojo.event.connect(s, 'select', 'treeNodeSelected');
            });    
       
/* ]]> */    
       
</ script >
   
</ head >
   
< body >
       
< h2 >
            AJAX Tree Example
       
</ h2 >
       
< div style ="float:left; margin-right: 50px;" >
           
< script type ="text/javascript" >
           
/* <![CDATA[ */
                dojo.require(
" dojo.lang.* " );
                dojo.require(
" dojo.widget.* " );
                dojo.require(
" dojo.widget.Tree " );
                dojo.require(
" dojo.widget.TreeRPCController " );                
             
/* ]]> */
             
</ script >             
           
< div dojoType ="TreeRPCController" widgetId ="treeController"
                DNDcontroller
="create" RPCUrl ="<s:url />" ></ div >
           
< div dojoType ="Tree" widgetId ="appFiles" toggle ="fade" controller ="treeController" >
               
< div dojoType ="TreeNode" title ='<s:property value ="root.name" /> '
                    widgetId='
< s:property value ="root.id" /> '
                    isFolder='
< s:property value ="root.children.length > 0" /> '
                    objectId='
< s:property value ="root.absolutePath" /> '>
               
</ div >
           
</ div >
       
</ div >
   
</ body >
</ html >
清单9 WebContent/AjaxTree.jsp

由于上面所提及的原因,我在上述的代码中并没有使用<s:tree />标志,而是使用了Dojo的写法——创建 widgetId 为“treeController”的 TreeRPCController 并将设为树的控制器。

发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTree.action,点开某个节点,在节点加载的过程中,加号图标变成时钟状图标,如下图所示页面:


图2 AJAX树示例

自定义<s:tree />的AJAX的主题(theme)

Struts 2的标志过人之外在于它允许开发人员自定义标志的页面输出。要做到这一点,你所需要做的只是创建一个自定义的theme并将其应用到相应标志。下面就让我自定义一个真正的AJAX的<s:tree/>的theme。

首先,你的源文件的根目录下新建包“template.realajax”。

然后,在上一步所建的包中新建“tree.ftl”文件,内容如下:

< script type ="text/javascript" >
/* <![CDATA[ */
    dojo.require(
" dojo.lang.* " );
    dojo.require(
" dojo.widget.* " );
    dojo.require(
" dojo.widget.Tree " );
    dojo.require(
" dojo.widget.TreeRPCController " );     < # -- Added by Max -->            
/* ]]> */
</ script >    
< #-- Added by Max -- >             
< div dojoType ="TreeRPCController"  
     widgetId
="${parameters.id?html}_controller"
     DNDcontroller
="create"  
     RPCUrl
="<@s.url />" >
</ div >
< #-- End -- >    
< div dojoType ="Tree"   
    <#if parameters.blankIconSrc?exists
>
    gridIconSrcT="
< @s .url value ='${parameters.blankIconSrc}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcL?exists >
    gridIconSrcL="
< @s .url value ='${parameters.gridIconSrcL}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcV?exists >
    gridIconSrcV="
< @s .url value ='${parameters.gridIconSrcV}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcP?exists >
    gridIconSrcP="
< @s .url value ='${parameters.gridIconSrcP}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcC?exists >
    gridIconSrcC="
< @s .url value ='${parameters.gridIconSrcC}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcX?exists >
    gridIconSrcX="
< @s .url value ='${parameters.gridIconSrcX}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcY?exists >
    gridIconSrcY="
< @s .url value ='${parameters.gridIconSrcY}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.gridIconSrcZ?exists >
    gridIconSrcZ="
< @s .url value ='${parameters.gridIconSrcZ}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.expandIconSrcPlus?exists >
    expandIconSrcPlus="
< @s .url value ='${parameters.expandIconSrcPlus}' includeParams ='none' /> "
   
</ #if >
   
< #if parameters.expandIconSrcMinus?exists >
    expandIconSrcMinus="
< @s .url value ='${parameters.expandIconSrcMinus?html}' includeParams ='none' /> "
   
</ #if >
   
< #if parameters.iconWidth?exists >
    iconWidth="
< @s .url value ='${parameters.iconWidth?html}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.iconHeight?exists >
    iconHeight="
< @s .url value ='${parameters.iconHeight?html}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.toggleDuration?exists >
    toggleDuration=${parameters.toggleDuration?c}
   
</ #if >
   
< #if parameters.templateCssPath?exists >
    templateCssPath="
< @s .url value ='${parameters.templateCssPath}' encode ="false" includeParams ='none' /> "
   
</ #if >
   
< #if parameters.showGrid?exists >
    showGrid="${parameters.showGrid?default(true)?string}"
   
</ #if >
   
< #if parameters.showRootGrid?exists >
    showRootGrid="${parameters.showRootGrid?default(true)?string}"
   
</ #if >
   
< #if parameters.id?exists >
    id="${parameters.id?html}"
   
</ #if >
   
< #if parameters.treeSelectedTopic?exists >
    publishSelectionTopic="${parameters.treeSelectedTopic?html}"
   
</ #if >
   
< #if parameters.treeExpandedTopic?exists >
    publishExpandedTopic="${parameters.treeExpandedTopic?html}"
   
</ #if >
   
< #if parameters.treeCollapsedTopic?exists >
    publishCollapsedTopic="${parameters.treeCollapsedTopic?html}"
   
</ #if >
   
< #if parameters.toggle?exists >
    toggle="${parameters.toggle?html}"
   
</ #if >
    controller="${parameters.id?html}_controller"
< #-- Added by Max -- >    
    >
   
< #if parameters.label?exists >
   
< div dojoType ="TreeNode" title ="${parameters.label?html}"
    <#if parameters.nodeIdProperty?exists
>
    id="${stack.findValue(parameters.nodeIdProperty)}"
   
< #else >
    id="${parameters.id}_root"
   
</ #if >
    >
   
< #elseif parameters.rootNode?exists >
    ${stack.push(parameters.rootNode)}
   
< #-- Edited by Max -- >    
   
< div dojoType ="TreeNode"  
             title
="${stack.findValue(parameters.nodeTitleProperty)}"
             widgetId
="${stack.findValue(parameters.nodeIdProperty)}"
             isFolder
="<#if stack.findValue(parameters.childCollectionProperty)?size gt 0>true<#else>false</#if>"
             objectId
="${stack.findValue(parameters.nameValue)}" >
       
</ div >
   
< #-- End -- >        
   
< #assign oldNode = stack.pop() /> < #-- pop the node off of the stack, but don't show it -- >
   
</ #if >    
清单10 src/template/realajax/tree.ftl

对上述稍作解释,上述代码主要在原版的src/template/ajax/tree.ftl的基础上添加了TreeRPCController的控件,并只输出根节点。由于<s:tree />没有类似nodeObjectIdProperty的属性,所以我用了value属性表示objectId对应的属性名称。

接着新建tree-close.ftl文件,内容和原版的一样,如下所示:

< #if parameters.label?exists ></ div ></ #if ></ div >
清单11 src/template/realajax/tree-close.ftl

再下来就应该是将theme应用到<s:tree />,如下代码所示:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - More Tree </ title >
       
< s:head theme ="ajax" debug ="true" />
       
< script type ="text/javascript" >
       
/* <![CDATA[ */
           
function treeNodeSelected(arg) {
                alert(arg.source.title
+ ' selected');
            }
                
            dojo.addOnLoad(
function () {                
               
var t = dojo.widget.byId('appFiles');                
               
var s = t.selector;                
                dojo.event.connect(s, 'select', 'treeNodeSelected');
            });    
       
/* ]]> */    
       
</ script >
   
</ head >
   
< body >
       
< h2 >
            AJAX Tree Example
       
</ h2 >
       
< div style ="float:left; margin-right: 50px;" >
           
< s:tree id ="appFiles" theme ="realajax" rootNode ="root"
                nodeTitleProperty
="name" nodeIdProperty ="id"
                childCollectionProperty
="children" value ="absolutePath" />
       
</ div >
   
</ body >
</ html >
清单12 WebContent/AjaxTreeTheme.jsp

上述代码中<s:tree />的用法,除了theme改为“realajax”和多了value="absolutePath"外,几乎和静态树中的一样。

为了不影响前一个例子,我们为该JSP文件配置类型相同的Action,如下代码所示:

        < action name ="AjaxTreeTheme" class ="tutorial.AjaxTreeAction" >
           
< result > AjaxTreeTheme.jsp </ result >
           
< result name ="ajax" type ="freemarker" > AjaxTree.ftl </ result >
       
</ action >
清单13 src/struts.xml配置片段

发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTreeTheme.action,结果如图2所示。

总结

通过上述例子,大家知道Struts 2 的AJAX 标志是基于Dojo控件开发的,所以如果大家希望熟练地使用这些标志,最好去了解一下Dojo。

本来还打算介绍一下Struts 2与DWR,不过看看文章的篇幅似乎足够自成一篇了,因此DWR相关的内容要留待下文继续了。


Struts 2与AJAX(第三部分)

很久没有更新BLOG了,前一段时间公司的项目比较忙,另外我还和一位出版社的朋友谈写书的事情,所以一直没有时间,完成《Struts 2与AJAX》。后来写书的事情吹了,趁今天有点空闲就把它完成。

在大家看这部分文章之前,我想对于写书的事情说两句,或者应该叫发牢骚才对。通过这次写书失败的经历,我明白为什么国内的IT书籍多数是滥于充数、粗制滥 造、缺乏经典。其实说白了就是一个“钱”字作怪。为了市场,很多编辑可能会“建议”你去“抄考”一些国内相对畅销的同类书籍,例如写Struts就一定要 按所谓的MVC进行目录分类,美其名曰“容易入门”。我认为“MVC”的概念虽然重要,但对初学者而言,需要对编程有一定的了解才容易明白此概念。另外, 为了“实用”,不惜使用相同的技术重复编写不同的范例。可能是我不太了解读者的心理吧。

言归正传,在上两部分的《Struts 2与AJAX》中我介绍了Struts 2与DOJO结合实现AJAX的知识,本文将介绍在Struts 2中使用DWR实现AJAX表单校验。

什么是DWR

DWR(Direct Web Remoting)是在Java EE中较流行的AJAX框架,它的最大优势就是可以像使用本地的Javascript函数一样,调用服务器上的Java方法。如下图所示:

DWR工作原理
图1 DWR工作原理

其实DWR原理也不复杂,它先在web.xml中配置一个Servlet,映射到特定的路径(通常是%CONTEXT_PATH%/dwr/*)。这个 Servlet的作用就是初始化要暴露给Javascript调用的Java类(通过dwr.xml进行配置),并生成相应的代理的Javascript 类代码。在XHR请求到来的时候,Servlet负责将请求的参数变成对应的Java对象,并以其为参数调用目标Java方法,并将返回值转化为 Javascript代码。详情请参考:http://getahead.ltd.uk/dwr/

Struts 2与DWR

在Struts 2.0.x中使用DWR实现AJAX表单校验。在大家掌握了DWR的原理后,下面我想详细介绍一下实现的步骤。

首先,到以下站点https://dwr.dev.java.net/files/documents/2427/47455/dwr.jar下载DWR的1.1.4版本的JAR包。需要注意的是,DWR虽然已经发布2.0版本,但它与1.1.4有很大的区别,所以请大家不要使用2.0版本,否则会出现异常的;

接着,新建WEB工程,将下图所示的JAR包加入到工程的“Build Path”中;

依赖的JAR包
图2 依赖的JAR包

接下来,配置web.xml文件,内容如下:

<? xml version="1.0" encoding="UTF-8" ?>
< web-app id ="WebApp_9" version ="2.4"
    xmlns
="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation
="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >

   
< display-name > Struts 2 AJAX Part 3 </ display-name >

   
< filter >
       
< filter-name > struts-cleanup </ filter-name >
       
< filter-class >
            org.apache.struts2.dispatcher.ActionContextCleanUp
       
</ filter-class >
   
</ filter >

   
< filter-mapping >
       
< filter-name > struts-cleanup </ filter-name >
       
< url-pattern > /* </ url-pattern >
   
</ filter-mapping >

   
< filter >
       
< filter-name > struts2 </ filter-name >
       
< filter-class >
            org.apache.struts2.dispatcher.FilterDispatcher
       
</ filter-class >
   
</ filter >

   
< filter-mapping >
       
< filter-name > struts2 </ filter-name >
       
< url-pattern > /* </ url-pattern >
   
</ filter-mapping >
   
<!-- 开始DWR配置 -->
   
< servlet >
       
< servlet-name > dwr </ servlet-name >
       
< servlet-class > uk.ltd.getahead.dwr.DWRServlet </ servlet-class >
       
< init-param >
           
< param-name > debug </ param-name >
           
< param-value > true </ param-value >
       
</ init-param >
   
</ servlet >

   
< servlet-mapping >
       
< servlet-name > dwr </ servlet-name >
       
< url-pattern > /dwr/* </ url-pattern >
   
</ servlet-mapping >
   
<!-- 结束DWR配置 -->
   
< welcome-file-list >
       
< welcome-file > index.html </ welcome-file >
   
</ welcome-file-list >

</ web-app >
清单1 WebContent/WEB-INF/web.xml

然后是DWR的配置文件:

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

<!-- START SNIPPET: dwr -->
<! DOCTYPE dwr PUBLIC 
    "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN" 
    "http://www.getahead.ltd.uk/dwr/dwr10.dtd"
>

< dwr >
   
< allow >
       
< create creator ="new" javascript ="validator" >
           
< param name ="class" value ="org.apache.struts2.validators.DWRValidator" />
       
</ create >
       
< convert converter ="bean" match ="com.opensymphony.xwork2.ValidationAwareSupport" />
   
</ allow >

   
< signatures >
       
<![CDATA[
        import java.util.Map;
        import org.apache.struts2.validators.DWRValidator;

        DWRValidator.doPost(String, String, Map<String, String>);
       
]]>
   
</ signatures >
</ dwr >
<!-- END SNIPPET: dwr -->
清单2 WebContent/WEB-INF/dwr.xml

通过以上配置,我们可以将DWRValidator中的方法暴露为Javascript可以调用的远程接口。

在正确完成以上步骤之后,我们发布运行一下应用程序,在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/dwr/,应该会出现如下页面:

DWR Servlet默认输出页面
图3 DWR Servlet默认输出页面

 接下来,我们要开始编写Action类了,代码如下:

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class AjaxValidation extends ActionSupport {
   
private static final long serialVersionUID = -7901311649275887920L;

   
private String name;
   
private String password;
   
private int age;
   
   
public int getAge() {
       
return age;
   }

   
   
public void setAge(int age) {
       
this.age = age;
   }

   
   
public String getName() {
       
return name;
   }

   
   
public void setName(String name) {
       
this.name = name;
   }

   
   
public String getPassword() {
       
return password;
   }

   
   
public void setPassword(String password) {
       
this.password = password;
   }

   
   @Override
   
public String execute() {        
       
return SUCCESS;
   }

}
清单3 src/tutorial/AjaxValidation.java

上述代码一目了然,相信大家已经很熟悉了。下面,我们再来看看表单校验的配置代码:

<! DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd" >
< validators >
   
< validator type ="regex" >
       
< param name ="fieldName" > password </ param >
       
< param name ="expression" >
           
<![CDATA[ (?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$ ]]>
       
</ param >
       
< message > Password must be between 8 and 10 characters, contain at least one digit and one alphabetic character, and must not contain special characters </ message >
   
</ validator >    
   
< field name ="name" >
       
< field-validator type ="requiredstring" >
           
< message > You must enter a name </ message >
       
</ field-validator >
   
</ field >
   
< field name ="age" >
       
< field-validator type ="int" >
           
< param name ="min" > 18 </ param >
           
< param name ="max" > 127 </ param >
           
< message > Age must be between 18 and 127 </ message >
       
</ field-validator >
   
</ field >
</ validators >
清单4 src/tutorial/AjaxValidation-validation.xml

对于AjaxValidation类的name、password和age三个字段,我分别用了非空、正规表达式和范围验证。正规表达式(?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$的作用是保证密码由至少包括一个数字和一个字母,且不能含有符号的长度为8到10的字符串组成。它也是所谓强密码(Strong Password)的普通实现。

接下来的是JSP的代码,内容如下:

<% @ page language = " java " contentType = " text/html; charset=utf-8 "
    pageEncoding
= " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
   
< head >
       
< title > Struts 2 AJAX - Validation </ title >
       
< s:head theme ="ajax" />
   
</ head >
   
< body >
       
< h2 >
            AJAX Validation Using DWR
       
</ h2 >
       
< s:form method ="post" validate ="true" theme ="ajax" >
           
< s:textfield label ="Name" name ="name" />
           
< s:password label ="Password" name ="password" />
           
< s:textfield label ="Age" name ="age" />
           
< s:submit />
       
</ s:form >
   
</ body >
</ html >
清单5 WebContent/AjaxValidation.jsp

以上代码也不复杂,不过需要的是注意的是除了要加入<s:head theme="ajax" />外,<s:form />也必须加入validate="true" theme="ajax"的属性。

最后是Struts 2的配置文件,内容如下所示:

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

<! DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd"
>

< struts >
   
< package name ="Struts2_AJAX_DEMO" extends ="struts-default" >
       
< action name ="AjaxValidation" class ="tutorial.AjaxValidation" >
           
< result name ="input" > AjaxValidation.jsp </ result >
           
< result > AjaxValidation.jsp </ result >
       
</ action >
   
</ package >
</ struts >
清单6 src/struts.xml

最后发布运应用程序,在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action出现如下图所示页面:

AjaxValidation页面输出
图4 AjaxValidation页面输出

在文本框中输入错误的值使页面出现错误提示信息,如下图所示:

AjaxValidation页面错误提示
图5 AjaxValidation页面错误提示

可能有朋友会问怎么知道这是通过AJAX进行校验的呢?在这里我向大家推荐一个AJAX开发必备的工具——Firebug。Firebug是 Firefox的一个功能强大的插件,它可以准确地输出和定位Javascript的错误、通过直观的方式查看HTML文档的DOM及其样式、所见即所得 的编辑方式,更值得一赞的是它可以方便地对Javascript进行跟踪和调试,如果你希望这进一步了解这个工具,请安装Firefox 2.0以上版本,并使用它浏览以下网址http://www.getfirebug.com

在安装完成Firebug之后,在Firefox中打开http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action,按“F12”键找开Firebug窗口,如果你第一次使用Firebug,请点击其窗口中的链接“Enable Firebug”激活插件。之后,点击“Net”,并在出现的菜单中点击选中“XHR”。然后将光标移入文本框,再将光标移出使文本框失去焦点,你可以看到Firebug窗口会多出一项记录,如下图所示:

Firebug中查看XHR请求
图6 Firebug中查看XHR请求

这就证明你在文本框失去焦出时,Struts 2会发送XHR请求到服务器以对该文本框值进行校验。有兴趣的朋友可以通过Firebug,研究XHR的请求与响应,这样可以加深对DWR工作原理的理解。

何时使用AJAX表单校验

虽然在Struts 2实现AJAX表单校验是一件非常简单的事,但我建议大家不要在所有的场合都使用这个功能,原因可以分为以下几个方面:

  1. AJAX校验在服务器上进行数据校验,可能会比较耗时;
  2. AJAX校验可能会过于频繁,加重服务器的负载;
  3. 一些普通的校验,只需要使用纯Javascript便可以实现。

读到这里,有的朋友可能会问:“那么什么时候才应该使用AJAX表单校验呢?”答案其实很简单,当我们的校验在页面加载时还不能够确定的情况下,就应该使用这个功能。例如,注册用户时,校验用户名是否已经存在;或者校验涉及过多的页务逻辑等。

现在让我们来改造一下上述例子,对于name我们可以使用AJAX校验,但对于其它的字段应该使用纯Javascript的校验。

在tutorial.AjaxValidation类加入如下方法:

   @Override
   
public void validate() {
       Set
<String> users = new HashSet<String>();
       users.add(
"max");
       users.add(
"scott");
       
if(users.contains(name)) {
           addFieldError(
"name", "The user name has been used!");
       }

   }
清单7 src/tutorial/AjaxValidation.java代码片段

用于模拟用户注册的场境,当然在真实情况应该在数据库中检查用户是否存在。

接下来再修改JSP文件,将<s:form />里面的内容改为如下所示代码:

        < s:form method ="post" validate ="true" theme ="ajax_xhtml" >
           
< s:textfield label ="Name" name ="name" theme ="ajax" />
           
< s:password label ="Password" name ="password" theme ="xhtml" />
           
< s:textfield label ="Age" name ="age" theme ="xhtml" />
           
< s:submit theme ="xhtml" />
       
</ s:form >
清单8 WebContent/AjaxValidation.jsp代码片段

对比早前的JSP代码,大家可以看出我将<s:form />的theme改成了“ajax_xhtml”,这个theme不是Struts 2自带,需要自定义。另外,除了Name使用了ajax的theme之外,其它的表单标签的theme都为xhtml,如此一来便可以实现只有当Name 文本框失去焦点时才发生AJAX表单校验。

接下来,应该是我们的自定义ajax_xhtml的theme了。在源代码文件夹下新建包“template.ajax_xhtml”,然后在其中加入form.ftl和form-close.ftl文件,内容分别如下:

< #if parameters.validate?exists >
< script type ="text/javascript" src ="${base}/struts/validationClient.js" ></ script >
< script type ="text/javascript" src ="${base}/dwr/interface/validator.js" ></ script >
< script type ="text/javascript" src ="${base}/dwr/engine.js" ></ script >
< script type ="text/javascript" src ="${base}/struts/ajax/validation.js" ></ script >
</ #if >
< #include "/${parameters.templateDir}/xhtml/form-validate.ftl" />
< #include "/${parameters.templateDir}/simple/form.ftl" />
< #include "/${parameters.templateDir}/xhtml/control.ftl" />
清单9 src/template/ajax_xhtml/form.ftl

上述的文件与xhtml theme中的form.ftl文件相似,我只是加入了AJAX表单校验所用的Javascript库,以便theme为ajax的表单标签使用。

< #include "/${parameters.templateDir}/xhtml/control-close.ftl" />
< #include "/${parameters.templateDir}/simple/form-close.ftl" />
< #include "/${parameters.templateDir}/xhtml/form-close-validate.ftl" />
清单10 src/template/ajax_xhtml/form-close.ftl

这个文件与xhtml theme中的form-close.ftl文件相同。

最后发布运行应用程序,大家可以发现在Password与Age的校验,只有在表单提交时才发生,而且是纯Javascript的校验。不过,以上代码还不是很完善,在行为上有些BUG。

总结

Struts 2相比一些其它的框架,在实现AJAX方面的确简单很多。更激动人心的是Struts 2的标签库支持基于模板的输出,使得开发者可以跟据自身的需要方便地改变标签的行为。

在将要发布的Struts 2.1版本中,AJAX表单校验将不再使用DWR,统一使用DOJO实现,详情请参考:http://struts.apache.org/2.0.9/docs/ajax-validation.html

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值