Struts分页的一个实现

在Web应用程序里,分页总让我们开发人员感到很头疼,倒不是因为技术上有多么困难,只是本来和业务没有太多关系的这么一个问题,你却得花不少功夫来处理。要是稍不留神,时不时出点问题就更郁闷了。我现在做的一个项目也到了该处理分页的时候了,感觉以前处理得都不好,所以这次有所改变,基本目标是在现有(未分页)的代码基础上,尽量少做修改,并且同样的代码可以应用于不同模块的分页。以下就是我用的方法:

首先,考虑分页绝大多数发生在列表时,组合查询时也需要用到。在我的项目里,列表的Action一般名字为ListXXXActioin,例如客户列表是ListClientsAction等等。在未分页前,ListXXXAction里会把所有的对象取出,通过request.setAttribute()放在request里,然后将请求转向到列表的jsp(例如listClients.jsp)显示出来(你可能会说不要在Action里放业务逻辑,但现在这不是我们考虑的重点)。而分页后,我们只取用户请求页对应的那些对象。为了最大限度的达到代码重用,我做了以下工作:

1、新建一个Pager类,该类有beginPage、endPage、currentPage、pageSize和total等int类型的属性,分别代表开始页、结束页、当前页、每页记录数和总记录数,它主要是让jsp页面显示页导航使用的。请注意currentPage属性是从0开始的。

2、新建一个AbstractListActioin类,并让所有ListXXXAction都继承它。在这个类里覆盖execute()方法,可以在这里判断权限等等,并在判断权限通过后执行一个abstract的act()方法,这个act()由ListXXXAction来实现。

3、在AbstractListAction里增加getPage()方法,用来从request得到用户请求的页码(若未请求则认为是第0页):

protected int getPage(HttpServletRequest request) { 
    String p = request.getParameter("p"); 
    if (p == null) 
        return 0; 
    else 
        try { 
            return Integer.parseInt(p); 
        } catch (NumberFormatException e) { 
            return 0; 
        } 
} 

4、在AbstractListAction里增加makePager()方法,用来向request里增加一个Pager类的实例,供jsp页面显示页导航:

protected Pager makePager(HttpServletRequest request, int total) { 
    Pager pager=new Pager(); 
    pager.setTotal(total); 
    pager.setPageSize(Config.getInstance().getPageSize()); 
    pager.setBeginPage(0); 
    pager.setEndPage(((pager.getTotal()) - 1) / pager.getPageSize() + 1); 
    pager.setCurrentPage(getPage(request)); 
    return pager; 
}  

注意在我的项目里,每页记录数是写在配置文件里的,如果你没有配置文件,上面第4行setPageSize()的参数直接填数字即可,例如pager.setPageSize(10);

5、这样,所有的ListXXXAction都可以使用getPage()得到请求的页码,并且能够方便的通过makePager()构造需要放在request里的pager对象了。现在要在从数据库取数据的代码上再做一些修改,即只取所需要的那一部分数据。由于我的项目中使用了Hibernate,所以这个修改也不是很困难。未分页前,在我的ListClientsAction里是通过构造一个Query来得到全部Client的,现在,只要在构造这个Query后再加两句(setMaxResults和setFirstResult)即可:

Query query =  ;//构造query的语句  
int total =  ;//得到总记录数  
Pager pager = makePager(request, total);//调用父类中的方法构造一个Pager实例 
query.setMaxResults(pager.getPageSize());//设置每页记录数 
query.setFirstResult(pager.getCurrentPage() * pager.getPageSize()); //设置开始位置 
request.setAttribute(Pager.class.getName(), pager);//把pager放在request里 
request.setAttribute(Client.class.getName(), query.list());

目前存在一个问题,就是在上面代码的第二句中,应该是获得总记录数,但我暂时没有特别好的办法不得到全部对象而直接得到记录数,只能很恐怖的用“int total = query.list().size();”,汗……

6、最后,我写了一个页导航的jsp页面pager.jsp,供各个显示列表的jsp来include,代码如下:

<%Pager pager=(Pager)request.getAttribute(Pager.class.getName());%>
<table width="90%" border="0" align="center" cellpadding="2" cellspacing="1" bgcolor="#CCCCCC">
<tr>
    <td bgcolor="#EEEEEE" align="right">
    <bean:message key="prompt.pager" arg0="<%=String.valueOf(pager.getTotal())%>"/>
        [
<%
for(int i=pager.getBeginPage();i<=pager.getEndPage();i++){
    if(i==pager.getCurrentPage()){
    %>
        <%=(i+1)%>
    <%}else{
        String qs=request.getQueryString()==null?"":request.getQueryString();
        String op = "p="+pager.getCurrentPage();//Original page parameter expression
        String np = "p="+i;//New expression
        if(qs.indexOf(op)==-1)
            qs=np+"&"+qs;
        qs=qs.replaceAll(op,np);
        %>
        <a href="<%="?"+qs%>"><%=(i+1)%></a>
    <%}%>
    <%if(i<pager.getEndPage()-1){%>
     
    <%}%>
<%}%>
]
</td></tr>
</table>

我觉得有必要解释一下,在上面的代码中,关于每一页对应的url是这样处理。request.getQueryString()中可能包含“q=2”这样的页码请求,也可能不包含即缺省请求第0页,所以统一用replaceAll()方法将其去掉,然后将对应的页码请求串(如“q=3”)加在qs的前面。这样做的好处是,每个模块都可以使用这个页导航,并且不会丢失url中的其他参数(例如今后加入排序功能后,url中可能包含“direction=desc”这样的参数)。

05-4-14 Update:我发现在Tomcat4.1和Websphere5.0里,request.getRequestURL()方法得到的地址是不一样的,所以考虑到兼容性,每个页码的链接都使用相对本页的链接。

在列表jsp(listClients.jsp)中,很简单的这样include它(之所以要放在里,是希望在没有记录可显示的时候就不显示页导航了):

<logic:notEmpty name="<%=Client.class.getName()%>"> 
    <%@include file="/pager.jsp"%> 
</logic:notEmpty>

经过上面几步的处理,我的客户列表已经可以实现分页了,效果见下图。如果在另外一个模块中也需要分页,比如部门列表时,只需要1、修改ListDeptsAction继承AbstractListAction,2、在ListDeptsAction里增加setMaxResults()和setFirstResults()方法,3、在listDepts.jsp中适当的位置include页导航,就可以了,改动是相当小的。

file

最后,如果希望组合查询的结果也能够分页,必须指定组合查询表单的method属性为“GET”,这样查询要求会被记录在url中,分页导航从而能够正常的工作(每次换页都将查询要求和请求的页码提交)。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/12/06/73417.html

多模块Struts应用程序的几个问题(及部分解决方法)

Struts从1.1版本开始支持把应用程序分为多个模块,每个模块可以看作独立的应用程序,在带来方便的同时,我也发现了一些问题。比如有一个struts应用程序分了大约十个模块,现在有以下问题不知道大家一般是怎么解决的:

1、因为要进行验证,所以在每个模块对应的资源文件里都要有“errors.required={0} is required.”等资源,有没有只用在一个文件里定义的方法?

2、用tiles的时候,要在每个模块对应的tiles-defs.xml里定义几乎相同的definition,有没有只用在一个文件里定义的方法?(我试过在缺省模块里定义一个definition,然后在模块里extends它,但不行,extends似乎只找当前模块)

3、使用ExceptionHandler的时候,为什么在exception标签里指定了bundle属性还是只在当前模块里找资源?我希望把一些重复使用的异常处理声明在一个文件里,例如NotLoginException、NoSuchObjectException等等,并且它们对应的key也指向同一个资源文件里的资源(利用bundle属性),怎么实现?

经过一段时间的摸索,第一个和第三个问题基本上解决了,其实它们可以看作同一类问题,就是资源的问题。在struts-config-xxx.xml里定义资源文件时,可以指定一个factory属性,不指定时使用缺省的“org.apache.struts.util.PropertyMessageResourcesFactory”类。我的解决方法是自定义一个CustomMessageResourcesFactory类,将多个资源文件以逗号分隔的形式作为参数(即message-resources的parameter属性)传给它,在需要资源的地方会遍历它们进行查找。同时还要自定义一个CustomMessageResources类,它的getMessage()方法里是查找资源的关键代码,而factory只是解析逗号分隔的参数构造并返回CustomMessageResources实例。

CustomMessageResourcesFactory的代码比较简单,如下所示:

package com.acme;

import java.util.Arrays;

import org.apache.struts.util.MessageResources;
import org.apache.struts.util.MessageResourcesFactory;

public class CustomMessageResourcesFactory extends MessageResourcesFactory{

    public MessageResources createResources(String config) {

        return new CustomMessageResources(Arrays.asList(config.split(",")));
    }

}

CustomMessageResources就稍微复杂一些,不过很幸运,我在网上找到了一个完全符合自己要求的类,下载地址在这里,如果链接已失效请联系我。

这样,在每个模块的struts-config-xxx.xml里,只要像下面这样定义资源文件就可以实现共享资源的功能了,其中ErrorResources中是所有模块都需要的错误信息资源:

<message-resources factory="eg.CustomMessageResourcesFactory" 
    parameter="eg.ApplicationResources,eg.ErrorResources" />

上面参考了这篇文章http://javaboutique.internet.com/tutorials/Dynaform/index-7.html,它是通过修改ActionServlet使用CustomMessageResources的,我觉得还是自定义factory的方式更自然些。

第二个问题暂时还没有解决,也许要修改handler实现。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/11/23/67425.html

Web页中级联下拉选择框问题的解决方法

示范中心项目里有一些页面要求几个下拉选择框的内容是具有关联的关系的,例如在编辑一个实验项目时,要先在一个下拉框里选择该项目所在的示范中心,这时实验室下拉框里的内容要根据用户选中的示范中心改变。为了实现这个目的,我们先后想了几种方法。

1、在用户选择示范中心时,刷新页面,并把示范中心代码加在url的后面传给action。这样的最大问题是,如果用户已经填写了一部分表单,在更改示范中心选择的时候会丢失已填写的信息。除非在刷新时把整个表单的所有域都加在url里传回,但这样做要在jsp页面里增加非常多的逻辑,因此不能使用。

2、把与示范中心关联的几个下拉框作为iframe的方式,当选择了一个示范中心时,根据所选的示范中心重新load这个iframe,这样不会影响到其他可能已经填写的域。这样做的问题是,iframe对应的页面里要重复考虑权限问题,这是不能忍受的,同时和所有使用框架技术的应用一样,有可能造成一些页面不一致的情形。

3、前面两种方式虽然都被否决了,但它们共同的好处是在用户改变对示范中心的选择时,数据是动态加载的。第三种方式是把所有数据一次读到客户端,使用javascript的方法实现关联。我在网上找到了一些js代码,但大部分都不适合动态生成数据,这也是一开始没有使用js方式的原因之一。不过最后还是找到了一个我认为不错的js,对它进行少量修改后比较漂亮的完成了任务,下面就说说具体的方法。

这个js的演示和下载地址在http://fason.nease.net/samples/select/,这里我要感谢这位fason朋友提供这么好的工具。在需要关联的页面里,例如编辑实验项目的页面,首先要引入xselect.js这个script,然后构造一个Dsy对象,使用这个对象的add方法增加关联下拉框的各个选项。还有两个数组,sel是关联下拉框的id数组,def是这些下拉框缺省的选项值数组。最后使用attachSelect函数将dsy对象和这两个数组关联起来就大功告成。对于选项是动态生成的情况,就是要动态生成这么一段javascript,如下所示:

<script language="JavaScript" src="../xselect.js"></script>
<script language="JavaScript">
<!--
var dsy = new Dsy();
var sel = ["demoCenter","lab"];
var def = ['<bean:write name="projectForm" property="demoCenterCode"/>','<bean:write name="projectForm" property="labCode"/>'];

dsy.add("0",[['<bean:message key="select.prompt" bundle="root"/>','']
<logic:iterate name="<%=DemoCenter.class.getName()%>" id="dc">
    ,['<bean:write name="dc" property="name"/>','<bean:write name="dc" property="code"/>']
</logic:iterate>
]);

dsy.add("0_0",[['<bean:message key="select.prompt" bundle="root"/>','']]);
<logic:iterate name="<%=DemoCenter.class.getName()%>" id="dc" indexId="index">
    dsy.add('0_<%=index.intValue()+1%>',[['<bean:message key="select.prompt" bundle="root"/>','']
    <bean:define id="code" name="dc" property="code" type="String" />
    <logic:iterate name="<%=Lab.class.getName()%>" id="lab">
        <logic:equal name="lab" property="demoCenter.code" value="<%=code%>">
            ,['<bean:write name="lab" property="name"/>','<bean:write name="lab" property="code"/>']
        </logic:equal>
    </logic:iterate>
    ]);
</logic:iterate>
attachSelect(dsy,sel,def);
//-->
</script>

生成的静态页面就是下面这样,页面显示后会缺省选择“北京大学数学示范中心”和“软件工程实验室”:

<script language="JavaScript" src="../xselect.js"></script>
<script language="JavaScript">
<!--
var dsy = new Dsy();
var sel = ["demoCenter","lab"];
var def = ['10000001','10000007'];

dsy.add("0",[['--------请选择--------','']
    ,['教育部','10000000']
    ,['北京大学数学示范中心','10000001']
    ,['北京师范大学普通话示范中心','10000002']
    ,['北京邮电大学卫星通信示范中心','10000003']]);
dsy.add("0_0",[['--------请选择--------','']]);
dsy.add('0_1',[['--------请选择--------','']
        ,['生物实验室','10000004']
        ,['软件工程实验室','10000007']
]);
dsy.add('0_2',[['--------请选择--------','']
        ,['数据挖掘实验室','10000002']
        ,['微电子实验室','10000003']
        ,['Web服务实验室','10000006']
]);
dsy.add('0_3',[['--------请选择--------','']
        ,['航空航天实验室','10000001']
        ,['湍流实验室','10000005']
        ,['互联网实验室','10000008']
]);
dsy.add('0_4',[['--------请选择--------','']
]);    
attachSelect(dsy,sel,def);
//-->
</script>

不过这个js有个小问题,就是它的def数组中的每个元素只能是一个字符串,当选择框是可多选的列表框时,就不能实现多个选项的预选择(populate)。为了解决这个问题,我对xselect.js进行了少量的修改,允许def中的每个元素是一个数组。以下是修改后的xselect.js文件中的doChange()函数:

function doChange(v) {
    var str = "0";
    for (var i = 0; i < v; i++) { str += ("_" + Sel[i].selectedIndex); };
    var ss = Sel[v];
    if (oDsy.Exists(str)) {
        with (ss) {
            length = 0;
            var ar = oDsy.Items[str], xx = 0;
            for (var i = 0; i < ar.length; i++) {
                var ot = ar[i][0], ov = ar[i][1] ? ar[i][1] : ot;
                //if (ov == Store[v]) xx = i;
                options[i] = new Option(ot, ov);

                //Added by zhanghao
                if(Store[v] instanceof Array){
                    for(var j=0;j<Store[v].length;j++)
                        if(Store[v][j] == ov)options[i].selected = true;
                }else{
                    if(ov == Store[v])options[i].selected = true;
                }
            }
            //options[xx].selected = true;
            if (++v < Sel.length) doChange(v);
        } 
    } else {
        for (var i = v; i<Sel.length; i++) {
            with (Sel[i]) {
                length = 0;
                options[0] = new Option("--", "");
                options[0].selected = true;
            }
        }
    }
}

其中注释掉了两句,并增加了一段对数组元素的处理,目前这个js在示范中心系统里工作良好。为了让jsp页面中的代码进一步减少,我们今后还要把动态生成js的代码写为tag的方式。

Web页中级联下拉选择框问题是一个非常普遍的问题,本贴里介绍的是我认为十分圆满的一个客户端方式的解决方法,用户在使用时不会感到任何不便,而且因为数据权限问题是在action里已经解决的,在对应的jsp页面里所做的任何处理都不需要考虑数据权限了。但在一些数据量特别大的系统里要考虑网络连接速度是否适合将所有数据传到客户端的问题。

Update: 相关代码下载

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/11/15/64006.html

[Struts]在JSP里处理比较复杂的内容?

今天遇到一个问题,到现在也没能比较圆满的解决,是不是Struts在标签库上还不够完善呢。比如有一个界面是显示课件列表的,在最后一栏里可以对数据进行操作,如下所示:

Code Name Author OP
10000001 风洞模型课件 刘金东 View Edit Delete
10000002 卡门涡阶课件 季铭义 View Edit Delete
10000003 复变函数课件 秦江 View Edit Delete
10000004 听力课件 郭长凯 View Edit Delete

现在希望当用户按删除时先弹出个确认框,提示“是否确认删除风洞模型课件”,用户可以选择确认或取消。其中“是否确认删除”是在资源文件里定义的(prompt.confirm.delete=是否确认删除{0}),“风洞模型课件”是课件的名称,课件bean名为"ware"。如果写成HTML,就是:

<a href="" onclick="return confirm('是否确认删除风洞模型课件')">Delete</a>

但因为信息都是动态的,所以就有问题了。因为在<html:link>的onclick="..."里,"<%"必须紧跟在第一个单引号后才能正确解析,即不能写为onclick="return confirm('<%=str%>')"。所以,现在有两种方法实现所需要的功能:

1、不用<html:link>,直接用HTML的<a>标记:

<bean:define id="toDel" name="ware" property="name" type="String"/>
<a href="delete.do?code=<bean:write name="ware" property="code"/>" 
    onclick="return confirm('<bean:message key="prompt.confirm.delete" bundle="root" arg0="<%=toDel%>"/>');">
    <bean:message key="course.list.op.delete"/>
</a>

2、使用<html:link>,事先定义一个只含一个参数的script函数,代码如下:

<script language="JavaScript">
<!--
function confirmDelete(str){
    return confirm('<bean:message key="prompt.confirm.delete" bundle="root" arg0="'+str+'"/>');
  }
-->
</script>

然后在删除链接的地方这样写:

<bean:define id="toDel" name="ware" property="name" type="String"/>
<html:link action="/delete" paramId="code" paramName="ware" paramProperty="code" onclick="<%="return confirmDelete('"+toDel+"');"%>">
    <bean:message key="course.list.op.delete"/>
</html:link>

这两种方法都能达到目的,我暂时使用了第2种用法,毕竟在struts程序的jsp里直接使用<a>标记有点别扭。我看了一下struts文档,能把资源中的{0}转换为实际内容的标签好象只有<bean:message>这一个,其实如果有办法让<bean:message>得到的内容放进某个bean里就很好办了,可惜……。

另外,没研究过EL标签库,不知道会不会有帮助。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/08/26/36764.html

[Struts]使用tiles管理界面遇到困难

上个周末都在研究怎么用tiles管理示范中心项目的界面,没想到遇到了不少麻烦,到现在也没解决。首先,示范中心项目有很多个模块,我们是用struts的模块功能分开的。本来想的是在缺省模块里定义几个公用的界面定义(definition),然后再各模块里都继承这个定义,并修改必要的tile就可以了。没想到不管怎么设置,模块里的定义都继承不到缺省的定义。缺省模块里:

<plug-in className="org.apache.struts.tiles.TilesPlugin" >
  <set-property property="definitions-config" value="/WEB-INF/tiles-defs.xml" />
</plug-in>

教师模块里:

<plug-in className="org.apache.struts.tiles.TilesPlugin" >
  <set-property property="definitions-config" value="/WEB-INF/tiles-defs.xml,/WEB-INF/tiles-defs-teacher.xml" />
</plug-in>

tiles-defs.xml里:

<definition name="classicLayout" path="/layout/classic.jsp">
    <put name="header" value="/header.jsp" />
    <put name="menu" value="/teacher/list.do"/>
    <put name="main" value=""/>
    <put name="footer" value="/footer.jsp" /> 
</definition>

tiles-defs-teacher.xml里:

<definition name="listLayout" extends="classicLayout">
    <put name="main" value="/teacher/list.jsp"/>
</definition>

然后在教师模块里forward到listLayout,提示path没有以"/"开头,就是没有找到listLayout这个定义了。我试了很多写法,包括设置moduleAware的属性,都没有成功。

后来想就在每个模块里都写classicLayout的定义吧,都指向同一个.jsp定义文件就可以了。又遇到新问题,我想在teacher模块里显示menu模块里的内容,会提示找不到所需资源,因为我是在teacher模块里,menu模块的资源是无法访的,除非在menu模块的配置文件里指定key,再在.jsp文件里强制指定bundle的名称,我觉得这个方法太不雅了,同时要做不少修改。

<definition name="classicLayout" path="/layout/classic.jsp">
    <put name="header" value="/header.jsp" />
    <put name="menu" value="/menu/list.jsp"/>
    <put name="main" value="/teacher/list.jsp"/>
    <put name="footer" value="/footer.jsp" /> 
</definition>

还有,<put>里的value只能是.jsp吗,用.do行不行,我试的结果是不行,虽然没报任何错误,但页面生成到那之前就截止了。郁闷!

[Struts]处理表单中值为空的日期类型字段

在示范中心项目中,我们把ActionForm中日期类型的字段指定为String类型,而在对应的JavaBean中指定为java.sql.Date类型。当用户提交表单的时候,在Action里使用BeanUtils.copyProperties()方法从ActionForm构造JavaBean对象(详见利用BeanUtils在对象间复制属性)。这个方法在大部分时候都很好,但有一个问题,就是当用户没有填写日期类型字段时(而该字段并非必填),validator不会提出警告,而在copyProperties()时会报类型转换异常,原因是这时ActionForm中的该字段的值是空字符串(""),负责字符串向Date转换的SqlDateConverter类调用Date.valueOf("")方法,显然""是无法转换为日期的,所以会抛出异常。

通过查看代码和资料,我发现这个问题的解决方法其实非常简单。只要把带缺省值参数的SqlDateConverter重新注册一下,覆盖原有的注册信息就可以了,这个注册语句一般是写在系统初试化的地方,对于Struts应用程序,当然做在PlugIn里最方便。代码如下:

package com.acme;

import javax.servlet.ServletException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.SqlDateConverter;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.PlugIn;
import org.apache.struts.config.ModuleConfig;

public class ConverterPlugIn implements PlugIn{

    public void init(ActionServlet servlet, ModuleConfig config) throws ServletException {
        ConvertUtils.register(new SqlDateConverter(null),java.sql.Date.class);
    }

    public void destroy() {
        ConvertUtils.deregister();
    }
}

注意SqlDateConverter的构造方法是带有参数null的,这表示遇到不能解析的字符串就返回空值。而deregister()方法的作用是恢复ConvertUtils的缺省注册表。为了使这个PlugIn起作用,要在struts-config.xml里增加一句话:

<plug-in className="etc.ConverterPlugIn" />

日期字段往往会给我们的开发带来麻烦,其实在Struts应用程序里,只要把这些转换类搞熟了,总可以找到很方便的办法。常见的问题还有如何指定日期输入格式,怎样处理java.util.Date的转换,等等,在这个链接里有解决这些问题的方法,道理都是一样的。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/08/17/34106.html

[Struts]让Dreamweaver显示Struts标签的插件

Dreamweaver(简称DW)的设计视图里不能显示struts标签,只能手动改代码。为此我找了好久,终于还是在DW网站上找到了,只有8K大,虽然没有漂亮的图标,但显示的信息还是很够用的。现在总算可以用DW编辑含有struts标签的jsp文件了!

file
图1 在DW里显示struts标签

这个文件我已经放在FTP上了,请点这里下载。如果连不上,请用这个地址。下载以后直接双击就安装,然后重开DW就行了。不需要了可以在Extension Manager里卸载。

Update: 感谢dudu帮我开通了文件上传功能,本地下载

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/08/07/31028.html

Apache Struts常见异常信息和解决方法

file

以下所说的struts-config.xml和ApplicationResources.properties等文件名是缺省时使用的,如果你使用了多模块,或指定了不同的资源文件名称,这些名字要做相应的修改。

1、“No bean found under attribute key XXX”
在struts-config.xml里定义了一个ActionForm,但type属性指定的类不存在,type属性的值应该是Form类的全名。或者是,在Action的定义中,name或attribute属性指定的ActionForm不存在。

2、“Cannot find bean XXX in any scope”
在Action里一般会request.setAttribute()一些对象,然后在转向的jsp文件里(用tag或request.getAttribute()方法)得到这些对象并显示出来。这个异常是说jsp要得到一个对象,但前面的Action里并没有将对象设置到request(也可以是session、servletContext)里。
可能是名字错了,请检查jsp里的tag的一般是name属性,或getAttribute()方法的参数值;或者是Action逻辑有问题没有执行setAttribute()方法就先转向了。
还有另外一个可能,纯粹是jsp文件的问题,例如<logic:iterate>会指定一个id值,然后在循环里<bean:write>使用这个值作为name的值,如果这两个值不同,也会出现此异常。(都是一个道理,request里没有对应的对象。)

3、“Missing message for key "XXX"”
缺少所需的资源,检查ApplicationResources.properties文件里是否有jsp文件里需要的资源,例如:

<bean:message key="msg.name.prompt"/>

这行代码会找msg.name.prompt资源,如果AppliationResources.properties里没有这个资源就会出现本异常。在使用多模块时,要注意在模块的struts-config-xxx.xml里指定要使用的资源文件名称,否则当然什么资源也找不到,这也是一个很容易犯的错误。

4、“No getter method for property XXX of bean teacher”
这条异常信息说得很明白,jsp里要取一个bean的属性出来,但这个bean并没有这个属性。你应该检查jsp中某个标签的property属性的值。例如下面代码中的cade应该改为code才对:

<bean:write name="teacher" property="cade" filter="true"/>

5、“Cannot find ActionMappings or ActionFormBeans collection”
待解决。

6、“Cannot retrieve mapping for action XXX”
在.jsp的<form>标签里指定action='/XXX',但这个Action并未在struts-config.xml里设置过。

7、HTTP Status 404 - /xxx/xxx.jsp
Forward的path属性指向的jsp页面不存在,请检查路径和模块,对于同一模块中的Action转向,path中不应包含模块名;模块间转向,记住使用contextRelative="true"

8、没有任何异常信息,显示空白页面
可能是Action里使用的forward与struts-config.xml里定义的forward名称不匹配。

9、“The element type "XXX" must be terminated by the matching end-tag "XXX".”
这个是struts-config.xml文件的格式错误,仔细检查它是否是良构的xml文件,关于xml文件的格式这里就不赘述了。

10、“Servlet.init() for servlet action threw exception”
一般出现这种异常在后面会显示一个关于ActionServlet的异常堆栈信息,其中指出了异常具体出现在代码的哪一行。我曾经遇到的一次提示如下:

java.lang.NullPointerException
    at org.apache.struts.action.ActionServlet.parseModuleConfigFile(ActionServlet.java:1003)
    at org.apache.struts.action.ActionServlet.initModuleConfig(ActionServlet.java:955)

为解决问题,先下载struts的源码包,然后在ActionServlet.java的第1003行插入断点,并对各变量进行监视。很丢人,我竟然把struts-config.xml文件弄丢了,因此出现了上面的异常,应该是和CVS同步时不小心删除的。

11、“Resources not defined for Validator”
这个是利用Validator插件做验证时可能出现的异常,这时你要检查validation.xml文件,看里面使用的资源是否确实有定义,form的名称是否正确,等等。

上面这些是我在用Struts做项目时遇到过的问题,其中一些曾困绕我不少时间,其实大部分都是自己不细心造成的。希望这篇文章能对你的开发有所帮助,并欢迎继续补充。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/08/02/29566.html

[测试]将TestCase整合

上一篇贴子里,我简单介绍了如何写一个TestCase(MockStrutsTestCase是TestCase的一个子类),可以看到是十分简单的,基本上只要写一些testXXX方法就可以运行了。当我们选择运行这个TestCase的时候,实际上运行的是一个Test,Test是TestCase的接口,实现这个接口的还有TestSuite类,使用这个类可以把多个TestCase一起运行,从而更加自动化。

要写一个TestSuite更加简单,看一下下面的代码就明白了:

package edu.pku.cc.democenter.test;

import junit.framework.Test;
import junit.framework.TestSuite;

public class AllTests {

    public static Test suite() {
        TestSuite suite = new TestSuite("Test for democenter");
        //$JUnit-BEGIN$
        suite.addTest(TestTeacherAction.suite());
        suite.addTest(TestHibernateDAO.suite());
        //$JUnit-END$
        return suite;
    }
}

当运行这个TestSuite的时候,就会自动对这两个TestCase进行测试。你可能已经看出来了,我们前文中写的TestTeacherAction类中并没有声明suite方法,是的,因此这里就要增加这个静态方法,如下所示:

public static Test suite() {
    return new TestSuite(TestTeacherAction.class);
}

我们在这个方法里只是简单的返回一个TestSuite对象,JUnit会根据传递的参数(TestTeacherAction.class)找到这个TestCase中全部的testXXX()方法并运行。

上面这种suite()方法的写法被称为动态方式,即利用了java的反射机制。还可以写成静态方式,这就需要在TestCase里写两个方法了,如下:

public static Test suite() {
    TestSuite suite=new TestSuite();
    suite.addTest(new TestTeacherAction());
    return suite;    
}

protected void runTest() throws Throwable {
    testListTeacherAction();
    testEditTeacherAction();
    testSaveTeacherAction();
}

这种方式允许用户选择执行某些testXXX()方法,而且这些方法也不一定以test开头,反正只要在runTest()里指定的都给执行。而suite()方法与动态方式比也有变化。要注意的是,如果按动态方式写suite()就不要再覆盖runTest()方法了,我实验后发现,这样会造成runTest()中指定的方法被反复执行n次,其中n等于textXXX()方法的数目。

另外一点,关于JUnite对Test的计数,在动态方式下,JUnit是按照testXXX()的数目计数的;而在静态方式下,是按照TestCase的数目计数的。

还有一点很重要,动态方式下,setUp()和tearDown()这两个方法是在每个testXXX()方法的前后执行;而静态方式下,是在每个TestCase的前后执行,也就是说,同一个TestCase中两个测试方法之间可能不会经过tearDown()和setUp()的过程。

至于动态方式和静态方式的选择,可以根据上面所说的进行参考。不过先声明,以上都是我自己测试得到的结论,存在出现错误的可能性(欢迎告知),以及没有涉及到的方面。

我本人比较prefer动态方式,毕竟代码量小一些

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/07/29/28309.html

[Struts]使用StrutsTestCase对Action进行单元测试简介

目前,测试驱动开发正变得越来越流行,由于“存在的就是合理的”,这种开发方式必然有其优越之处。作为一个小小程序员,对新鲜技术的追求是工作的重要动力,相信大家都有同感吧。

测试驱动开发是极限编程(XP)的重要组成部分,从字面上就可以看出,它是先有测试再有代码的。这听起来似乎有点奇怪,实际上,可以把测试用例当作需求,程序员的工作就是写出满足这种需求的代码,即让这些测试都能够通过。在刚刚写好测试用例的时候,由于还没有实际代码,因此这时运行测试的结果一定不会通过,随着代码的增加,越来越多的测试得以通过,最后全部通过。这时,基本上可以说系统的每个功能单元都是正确的,剩下的工作就是集成测试了。而这些测试用例的使命并未结束,因为代码会不断修改,我们需要经常(例如每天)运行测试来检验目前的代码是否能够通过测试。很明显,这种检验是完全自动化的,既快速有保证质量。

在使用Struts构件的系统里,的每一个Action都可以看作一个功能单元,它们构成了系统的主体。(当然,你的业务逻辑并不一定都直接写在Action的execute方法中,但在该方法中会以一定方式调用这些逻辑。)StrutsTestCase 是JUnit的一个包装,它提供了非常方便的测试这些Action的方法。它提供两种测试方式:Mock(模拟对象)和Cactus(真实环境),目前我只试验了前者,发现效果很好。

由于我已经写了一些代码,因此我进行的不能算是真正的测试“驱动”开发,我主要是把StrutsTestCase作为一种自动化的测试工具来用,起到保证代码质量的作用。

举例来说,我有一个类名为SaveTeacherAction的Action,其所在模块名为teacher,访问路径为/save,与他相关联的是名为TeacherForm的ActionForm,(struts-config-teacher.xml)配置如下所示:

<action
    attribute="teacherForm"
    input="/form/teacher.jsp"
    name="teacherForm"
    path="/save"
    scope="request"
    type="edu.pku.cc.democenter.teacher.action.SaveTeacherAction">
    <forward name="success" path="/list.do" redirect="true" />
</action>

SaveTeacherAction类中的execute方法如下,其中HibernateDAO是我自己写的用来进行持久化操作的包装类,BeauUtils是Jakarta commons包中的一个实用工具,可以在两个Bean类型对象的相同属性之间进行复制:

public ActionForward execute(
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    HibernateDAO dao=HibernateDAO.getInstance(getServlet().getServletContext());
    TeacherForm teacherForm = (TeacherForm) form;
    Teacher t=null;

    if("Create".equals(teacherForm.getAction())){
        t=new Teacher();
    }
    if("Edit".equals(teacherForm.getAction())){
        t=(Teacher)dao.findByCode(Teacher.class,teacherForm.getCode());
    }

    BeanUtils.copyProperties(t,teacherForm);

    dao.saveOrUpdate(t);
    return mapping.findForward("success");
}

下面,我要写一个测试用例来对这个Action进行测试。在Eclipse里使用StrutsTestCase非常简单,只需要在工程的classpath里包含strutstest-2.1.2.jar这个包以及junit的包就可以开始编写了。我为这个类起名为TestSaveTeacherAction,即Action类名前加上Test字样,所在包也与实际代码分开,使用edu.pku.cc.democenter.test的名称(与之对比,SaveTeacherAction的包名为edu.pku.cc.democenter.teacher.action,democenter是我们这个项目的名称)。

现在来看一下TestSaveTeacherAction的内容:

package edu.pku.cc.democenter.test;

import servletunit.struts.MockStrutsTestCase;
import edu.pku.cc.democenter.teacher.form.TeacherForm;

public class TestTeacherAction extends MockStrutsTestCase{

    protected void setUp() throws Exception {
        super.setUp();
        setConfigFile("teacher","/WEB-INF/struts-config-teacher.xml");
    }

    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void testSaveTeacherAction_Create(){
        setRequestPathInfo("/teacher","/save");
        TeacherForm form=makeForm();
        form.setAction("Create");
        form.setCode("test.create");
        setActionForm(form);
        actionPerform();
        verifyForward("success");
    }

    public void testSaveTeacherAction_Edit(){
        setRequestPathInfo("/teacher","/save");
        TeacherForm form=makeForm();
        form.setAction("Create");
        form.setCode("test.edit");
        setActionForm(form);
        actionPerform();

        form.setAction("Edit");
        form.setBirthDate("1979-1-10");
        setActionForm(form);
        actionPerform();
        verifyForward("success");
    }

    private TeacherForm makeForm(){
        TeacherForm form=new TeacherForm();
        form.setAction("Create");
        form.setBirthDate("1979-1-1");
        form.setCode("test.001");
        form.setEmail("test@test.com");
        form.setName("test");
        form.setShortName("CS");
        form.setTel("62760000");
        return form;
    }
}

使用Mock方式的测试用例都继承servletunit.struts.MockStrutsTestCase这个类,setUp和tearDown方法分别是测试前后进行准备和善后工作的地方。要运行的测试方法都以test开头,系统会自动调用这些方法。

由于SaveTeacherAction根据request中的action参数有两种运行方式:Create和Edit,所以我写了两个test方法对它们分别进行测试。这里要注意的是,这些test方法运行的顺序是不确定的,不要认为可以先运行create的测试,再以此为基础对刚刚create的对象进行edit,看testSaveTeacherAction_Edit方法的写法。

我在setUp方法里指定了模块和对应的配置文件名称,如果没有模块可以省去这一步。在实际的testSaveTeacherAction_Create方法里,首先用setRequestPathInfo方法指定要测试的Action的路径和模块,如果没有模块可以用一个参数的同名方法。然后构造一个ActionForm,对于我的例子就是TeacherForm,为了简化代码,我写了一个makeForm方法来生成一个填好值的TeacherForm。用setActionForm将这个TeacherForm连接到Action,actionPerform方法通知执行Action操作。这时按照我们的设想,Action会将请求转发到一个名为success的Forward,所以我们使用verifyForward("success")方法验证是否进行了转发。在这一步如果失败,就表明此单元测试失败,否则为成功。

testSaveTeacherAction_Edit方法与其类似,只是要注意在这个方法里要自己建立对象再修改,而不能使用testSaveTeacherAction_Create方法里建立的对象,因为这两个方法的执行顺序是不确定的。

要在Eclipse里运行这个测试也很简单,先双击打开这个类,然后在Run菜单里选择Run As->JUnit Test就可以了,你会看到一个JUnit视图,里面有一个绿色的进度条,如果走到头还保持绿色表示所有的测试都成功,否则会变成红色,并在下面显示异常的堆栈信息。(成就感哦)

file

好了,今天对StrutsTestCase作了一个很简单的介绍,我也是刚刚开始使用它,今后肯定还会遇到问题的,敬请关注后续报道。

另,相关的一些文章可以在网上找到,作为入门很好,例如:

http://plateau.sicool.com/article/tdd/strutstestcast_junit_tdd.htm

我的感觉,遇到问题最好先找文档,养成习惯后不但解决问题快了,同时在看文档的同时还可以对其加深理解,何乐而不为呢。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2004/07/29/28221.html