9.5 Web层设计
在架构综述里面提到,系统的Web层采用的是经典的J2EE Web MVC框架Struts,其表现层也大量使用Struts的标签库,关于Struts标签库的详细用法,请参考第3章的介绍。
9.5.1 Action的实现
Struts的Action实现非常简单,通过继承Struts的Action基类重写execute方法,并在该方法里调用业务逻辑组件的业务方法。在这里,可以发现所有的Action有个共同之处——都需要调用业务逻辑组件。
在Spring与Struts的整合策略里介绍过,业务逻辑组件的实现也不是由Action自己控制的,而是接受Spring容器的依赖注入。因此必须为Action提供对应的setter方法,而且每个Action都必须为其注入业务逻辑组件,因此可写成一个Action基类,让所有的Action都从该基类派生。
下面是Action基类的源代码:
//BaseAction,作为其他Action的父类
public class BaseAction extends Action
{
// FacadeManager属性,面向接口编程
protected FacadeManager mgr;
//依赖注入业务逻辑组件必需的setter方法
public void setMgr(FacadeManager mgr)
{
this.mgr = mgr;
}
}
注意:在BaseAction中,故意将mgr属性设置成protected的访问权限,目的是为了让其子类可以直接访问该属性,从而提供更简单的访问方式。
而其他的Action则简单地从BaseAction派生出来,派生出来的Action具有一个属性:mgr,该属性就是业务逻辑组件的引用。
下面以几个Action作为代表,分别介绍Action的实现。
下面的AddReviewAction用于拦截用户提交消息回复时的请求,其代码如下:
//业务控制器,以BaseAction作为基础
public class AddReviewAction extends BaseAction
{
/ /必须重写该核心方法,该方法actionForm将表单的请求参数封装成值对象
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws
Exception
{
//解析ActionForm,用于获取请求参数
DynaValidatorForm addForm = (DynaValidatorForm)form;
//获取请求参数
String content = (String)addForm.get("content");
String newsId = (String)addForm.get("newsId");
try
{
//调用mgr的业务方法加载News对象
News news = mgr.getNews(newsId);
//获取session中的user值
String username = (String)request.getSession(true)
.getAttribute
(AppConstants.LOGIN_USER);
//调用mgr的业务方法加载User对象
User poster = mgr.getUser(username);
//创建消息回复实例
NewsReview newsReview = new NewsReview();
//设置消息回复的属性
newsReview.setNews(news);
newsReview.setPoster(poster);
newsReview.setContent(content);
newsReview.setPostDate(new Date());
newsReview.setLastModifyDate(new Date());
//持久化消息回复
mgr.saveNewsReview(newsReview);
}
//捕捉异常
catch (Exception e)
{
//出现异常就跳转到failure
request.setAttribute("newsId" , newsId);
return mapping.findForward("failure");
}
//执行成功,跳转到success
request.setAttribute("newsId" , newsId);
return mapping.findForward("success");
}
}
下面的LoadReviewsByNews用于拦截用户查看消息细节的Action,该Action根据用户请求的id加载该消息的回复以及消息本身:
public class LoadReviewsByNews extends BaseAction
{
//必须重写该核心方法,该方法actionForm将表单的请求参数封装成值对象
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws
Exception
{
//希望加载的消息id
String newsId = null;
//考虑到请求有可能从不同地方转发过来,request的参数和属性
//分别获取newsId属性。
if(request.getAttribute("newsId") == null)
{
newsId = request.getParameter("newsId");
}
else
{
newsId = (String)request.getAttribute("newsId");
}
//获取News实例
News news = mgr.getNews(newsId);
//将News实例设置成Request属性后转发
request.setAttribute("news" , news);
//同时将消息的回复也设置成Request属性后转发
request.setAttribute("reviews",news.getNewsReviews());
return mapping.findForward("success");
}
}
上面介绍了两种Action:一种有ActionForm的Action;另一种无需ActionForm的Action。这两种Action大同小异,都需要调用mgr的业务逻辑方法,区别在于获取请求参数的方式不同。
注意:在上面的Action中,多次重复访问PO对象,而Action中通常不允许访问PO对象,因为PO对象是持久层的组件,应该使用更普通的JavaBean作为VO(值对象);VO用于封装业务逻辑组件访问的值,并将这些值传递到JSP页面。因为本示例是个较小的示例,所以在Action中可直接访问PO对象。在第10章的示例中,读者将可以看到更加严格的控制。
9.5.2 Spring容器管理Action
正如前面介绍的,推荐使用Spring管理Struts的Action。因为这样可以充分利用Spring的IOC功能,使Action无须关心业务逻辑组件的实现,而由Spring负责为Action注入业务逻辑组件引用,从而实现更好地解耦。
为了让Struts将请求转发到Spring容器内的bean,系统将采用DelegatingRequest Processor的整合策略。因为这种策略无需Struts创建Action实例,直接由Spring容器负责创建Action实例,并为其注入依赖关系。使系统更早将请求转发给Spring容器控制。
采用这种整策略,必须在struts-config.xml文件中配置controller元素,并通过指定processorClass属性指定DelegatingRequestProcessor处理器。即在配置文件中增加如下代码:
<controller inputForward="true"
processorClass="org.springframework.web.struts.DelegatingRequestProcessor"/>
经过这个简单的配置,则无须为struts-config.xml中的Action配置class属性,因为Struts无须负责创建Action实例,由DelegatingRequestProcessor直接将请求转发到Spring容器内。
下面是struts-config.xml文件中Action的配置代码:
<!-- 添加消息评论 -->
<action path="/addNewsReview"
name="addNewsReviewForm"
scope="request"
validate="true"
input="input">
<forward name="failure" path="/loadNewsReviewByNews.do"/>
<forward name="success" path="/loadNewsReviewByNews.do"/>
</action>
在上面的配置代码中,没有为action元素确定class属性,只有当采用Delegating RequestProcessor代替了默认的RequestProcessor后,才允许这样配置。
DelegatingRequestProcessor转发请求时,请求被转发给Spring容器中同名的bean处理,因此必须在Spring容器中配置同名的bean。对于上面Action的配置,必须在Spring容器中配置如下的bean:
<bean name="/addNewsReview" class="org.yeeku.action.AddReviewAction">
<property name="mgr" ref="facadeManager"/>
</bean>
下面是整个应用struts-config.xml配置文件的源代码:
<?xml version="1.0" encoding="GBK"?>
<!-- Struts配置文件的文件头,包含DTD等信息-->
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"
"http://struts.apache.org/dtds/struts-config_1_2.dtd">
<!-- Struts配置文件的根元素-->
<struts-config>
<!-- 在form-beans元素里配置所有的ActionForm-->
<form-beans>
<!-- 配置登录所使用的Form-->
<form-bean name="loginForm" type="org.apache.struts.validator.
DynaValidatorForm">
<!-- 配置loginForm的两个属性-->
<form-property name="user" type="java.lang.String" />
<form-property name="pass" type="java.lang.String" />
</form-bean>
<!-- 添加消息所用的Form-->
<form-bean name="addNewsForm" type="org.apache.struts.validator.
DynaValidatorForm">
<!-- 配置addNewsForm的三属性-->
<form-property name="title" type="java.lang.String" />
<form-property name="content" type="java.lang.String" />
<form-property name="categoryId" type="java.lang.String" />
</form-bean>
<!-- 添加消息评论所用的Form-->
<form-bean name="addNewsReviewForm" type="org.apache.struts.
validator.DynaValidatorForm">
<!-- 配置addNewsReviewForm的两个属性-->
<form-property name="content" type="java.lang.String" />
<form-property name="newsId" type="java.lang.String" />
</form-bean>
</form-beans>
<!-- 配置所有的Action映射-->
<action-mappings>
<!-- 处理登录 -->
<action path="/processLogin"
name="loginForm"
scope="request"
validate="true"
input="input">
<forward name="input" path="/index.jsp" />
<forward name="success" path="/listCate.do" />
</action>
<!-- 登出系统 -->
<action path="/logout" scope="request">
<forward name="success" path="/index.jsp"/>
</action>
<!-- 进入主页面 -->
<action path="/listCate" scope="request">
<forward name="success" path="/main.jsp"/>
</action>
<!-- 根据种类加载所有消息 -->
<action path="/loadNewsByCategory" scope="request">
<forward name="failure" path="/listCate.do"/>
<forward name="success" path="/category_view.jsp"/>
</action>
<!-- 添加消息 -->
<action path="/addNews"
name="addNewsForm"
scope="request"
validate="true"
input="input">
<forward name="failure" path="/loadNewsByCategory.do"/>
<forward name="success" path="/loadNewsByCategory.do"/>
</action>
<!-- 根据消息id加载所有评论 -->
<action path="/loadNewsReviewByNews" scope="request">
<forward name="success" path="/news_view.jsp"/>
</action>
<!-- 添加消息评论 -->
<action path="/addNewsReview"
name="addNewsReviewForm"
scope="request"
validate="true"
input="input">
<forward name="failure" path="/loadNewsReviewByNews.do"/>
<forward name="success" path="/loadNewsReviewByNews.do"/>
</action>
</action-mappings>
<!-- 配置控制属性-->
<controller inputForward="true"
processorClass="org.springframework.web.struts.DelegatingRequest
Processor"/>
<!-- 配置国际化的消息资源-->
<message-resources parameter="resource"/>
<!-- 配置数据校验的框架-->
<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
<set-property property="pathnames" value="/WEB-INF/validator-rules. xml,
/WEB-INF/validation.xml"/>
<set-property property="stopOnFirstError" value="true"/>
</plug-in>
<!-- 配置用于Spring整合的插件框架-->
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
<set-property property="contextConfigLocation" value="/WEB-INF/
daoContext.xml,
/WEB-INF/applicationContext.xml,
/WEB-INF/action-Servlet.xml"/>
</plug-in>
</struts-config>
这个配置文件与Struts基本的配置文件并没有太多的不同,区别在于该配置文件的action元素没有class属性,以及使用DelegatingRequestProcessor代替了系统默认的RequestProcessor。
注意:虽然action元素没有确定class属性,但也允许指定class属性,只是不会有任何作用。
Spring对Action的配置采用单独的文件配置action-Servlet.xml,该文件中配置了所有的Action bean。
因为所有的Action都需要为其注入业务逻辑组件,所以此处采用继承简化了Action bean的配置。具体的配置文件代码如下:
<?xml version="1.0" encoding="GBK"?>
<!-- Spring配置文件的文件头,包含DTD等信息-->
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<!-- Spring配置文件的根元素-->
<beans>
<!-- 配置Action模板,用于被其他Action继承-->
<bean id="actionTemplate" abstract="true" singleton="false">
<property name="mgr" ref="facadeManager"/>
</bean>
<!-- 处理登录-->
<bean name="/processLogin" class="org.yeeku.action.LoginAction"
parent="actionTemplate"/>
<!-- 登出系统-->
<bean name="/logout" class="org.yeeku.action.Logout"/>
<!-- 列出所有的消息分类-->
<bean name="/listCate" class="org.yeeku.action.ListCate" parent="
actionTemplate"/>
<!-- 根据种类列出所有消息-->
<bean name="/loadNewsByCategory" class="org.yeeku.action.LoadNewsBy
Category"
parent="actionTemplate"/>
<!-- 添加消息-->
<bean name="/addNews" class="org.yeeku.action.AddNewsAction" parent="
actionTemplate"/>
<!-- 根据消息查看所有的评论-->
<bean name="/loadNewsReviewByNews" class="org.yeeku.action.LoadReviews
ByNews"
parent="actionTemplate"/>
<!-- 添加消息评论-->
<bean name="/addNewsReview" class="org.yeeku.action.AddReviewAction"
parent="actionTemplate"/>
</beans>
至此,已经基本完成了Struts与Spring的整合。当ActionServlet拦截到用户请求时,则调用DelegatingRequestProcessor,该处理器将请求转发到Spring容器中的bean,由该bean负责调用业务逻辑组件处理用户用户请求,并将处理结果呈现给用户。
9.5.3 数据校验的选择
数据校验是表现层必须处理的基本问题。根据第3章的介绍,表现层的数据校验分成客户端校验和服务器端校验。不管是客户端校验,还是服务器端校验,Struts都有很好的支持,完全可以弹出JavaScript校验。
此处要提醒读者的是,Struts的客户端校验有一个弊端。
看如图9.5所示的简单页面。

图9.5 简单的登录页面
在这个简单的登录页面中,假设只需要对页面中三个表单域进行校验,如用户名、密码及电子邮件这三项必填,并且电子邮件为有效的地址,可以采用如下的JavaScript代码校验:
<script>
function check(form)
{
var errMsg = "";
if (form.user.value == null || trim(form.email.value)=="")
{
errMsg += "用户名必填" ;
}
if (form.pass.value == null || trim(form.pass.value)=="")
{
errMsg += "密码必填" ;
}
if (form.email.value == null || trim(form. email.value)=="")
{
errMsg += "电子邮件必填" ;
}
if(trim(form. email.value)!="" && !/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\ w+([-.]\w+)*$/.test(form.email.value))
{
errMsg += "电子邮件格式不对 ;
return false;
}
if(errMsg == null )
{
return true;
}
return false
}
//用于截取两端的空格
function trim(s)
{
return s.replace( /^\s*/, "" ).replace( /\s*$/, "" );
}
</script>
这段JavaScript代码结构清晰,相当简洁。
通过第3章的学习,我们也可以使用Struts的验证框架来生成客户端校验,但Struts生成的JavaScript的校验代码非常多,笔者在此处不可能完全列出,但读者以通过客户端查看网页源代码看到这将近1000行的JavaScript代码。
仔细检查Struts生成的JavaScript代码,可以发现这些JavaScript代码包含了最小长度校验、最长长度校验及有效范围校验等,而这些与该页面的需求没有丝毫关系。
这正是手写JavaScript校验和Struts自动生成JavaScript校验的区别:Struts生成的校验会包含更多的代码,即使页面只需要校验一个简单的必填项,Struts也会生成将近1000行的JavaScript代码。虽然这些代码不需要程序员手写,但这些代码必须要下载到客户端执行,显然这些无用的代码将加重客户端网络带宽的负担。因此客户端校验依然建议使用手写校验。
注意:笔者在这里介绍给读者一个技巧,仔细观察Struts生成的JavaScript客户端校验代码,就可发现每个页面的JavaScript代码只有前面数行不同,其他部分则完全相同的。没错,这些部分是通用,我们完全可以将这些通用的部分提取出来,作为通用的JavaScript代码,并以单独的JavaScript文件保存,当每个页面需要进行校验时,只需导入该通用的JavaScript文件即可;而页面则只需加入Struts为不同页面生成的不同部分。关于客户端校验,还可以直接采用ProtoType校验。
在网络带宽受限的情况下,建议不要采用Struts的JavaScript校验。并不是意味可以不使用Struts的校验框架。因为服务器端校验依赖于Struts的校验要简单得多。
增加校验的详细步骤请参考第3章的内容。此处给出本应用的校验规则文件,validation.xml文件的源代码如下:
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- 校验文件的文件头,包含DTD等信息-->
<!DOCTYPE form-validation PUBLIC
"-//Apache Software Foundation//DTD Commons Validator Rules
Configuration 1.1.3//EN"
"http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">
<!-- 校验文件的根元素-->
<form-validation>
<!-- 需要校验的Form都放在formset元素里-->
<formset>
<!-- 需要校验的第一个form:登录用的loginForm-->
<form name="loginForm">
<!-- 需要校验的user域,需满足最小长度,必填两个规则-->
<field property="user" depends="required,minlength">
<arg key="loginForm.user" position="0"/>
<arg name="minlength" key="${var:minlength}" resource="
false" position="1"/>
<var>
<var-name>minlength</var-name>
<var-value>4</var-value>
</var>
</field>
<!-- 需要校验的pass域,需满足最小长度,必填两个规则-->
<field property="pass" depends="required,minlength">
<arg key="loginForm.pass" position="0"/>
<arg name="minlength" key="${var:minlength}" resource="
false" position="1"/>
<var>
<var-name>minlength</var-name>
<var-value>4</var-value>
</var>
</field>
</form>
<!-- 需要校验的第二个form:添加消息用的addNewsForm -->
<form name="addNewsForm">
<!-- 需要校验的title域,需满足必填规则-->
<field property="title" depends="required">
<arg key="addNewsForm.title" position="0"/>
</field>
<!-- 需要校验的content域,需满足必填规则-->
<field property="content" depends="required">
<arg key="addNewsForm.content" position="0"/>
</field>
<!-- 需要校验的categoryId域,需满足必填,整数规则-->
<field property="categoryId" depends="required,integer">
<arg key="addNewsForm.categoryId" position="0"/>
</field>
</form>
<!-- 需要校验的第三个form:添加消息评论用的addNewsReviewForm -->
<form name="addNewsReviewForm">
<!-- 需要校验的content域,需满足必填规则-->
<field property="content" depends="required">
<arg key="addNewsReviewForm.content" position="0"/>
</field>
<!-- 需要校验的newsId域,需满足必填,整数规则-->
<field property="newsId" depends="required,integer">
<arg key="addNewsReviewForm.categoryId" position="0"/>
</field>
</form>
</formset>
</form-validation>
推荐的校验策略是:在客户端采用手写的JavaScript校验;而服务器端采用Struts的校验框架完成。
9.5.4 访问权限的控制
本系统访问权限的控制非常简单,在使用本消息发布系统之前,必须先登录本系统。如果某个未登录的用户企图进入系统使用页面时,则系统将请求转到登录页面。
权限控制的最传统做法是:在Action里手动控制,在每次调用业务逻辑方法之前,首先判断用户是否有足够的权限。在本系统中就是判断用户是否登录,但这种方法相当烦琐,而且需要重复判断用户是否登录,这与DRY(不要书写重复的代码)规则相违背。
但权限控制有更好的选择:利用Filter过滤请求,或者使用AOP框架拦截请求。笔者在本章采用Filter过滤请求完成权限检查,在第10章将采用AOP框架来完成更复杂的权限检查。
相对而言,利用AOP框架的权限检查支持更细的粒度,灵活性更好。
下面是控制权限检查的Filter源代码:
//将请求转换成HttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//将响应转换成HttpServletResponse
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//获取客户端的请求的地址
String requesturi = httpServletRequest.getRequestURI();
// 通过检查session中的变量,过滤请求
HttpSession session = httpServletRequest.getSession();
Object currentUser = session.getAttribute(AppConstants.LOGIN_USER);
// 当前会话用户为空而且不是请求登录,则退出登录,欢迎页面和根目录则退回到应用的根目录
if (currentUser == null
&& !requesturi.endsWith("/processLogin.do")
&& !requesturi.endsWith("/logout.do")
&& !requesturi.endsWith("/index.jsp")
&& !requesturi.endsWith(httpServletRequest.getContextPath()
+ "/"))
{
httpServletResponse.sendRedirect(httpServletRequest.
getContextPath() + "/");
return;
}
//否则,允许继续处理请求
chain.doFilter(request, response);
}
Filter本身不能处理用户请求,也不能生成响应,它是个典型的链式处理,拦截用户请求。增加Filter的额外处理后,依然将请求转发给Servlet,由Servlet生成客户端响应。
在该用户请求中,如果请求地址不是请求登录、退出登录、欢迎页面或根目录,并且当前用户没有登录时,则退回到应用的根目录。
9.5.5 解决中文编码问题
来自中国地区的请求,基本都以GBK方式编码,而Struts的ActionServlet默认以ISO8859-1方式解码。在这种情况下,不可避免地会产生乱码问题,为避免这种乱码问题,可以有以下两个做法:
扩展ActionServlet。
利用Filter处理编码方式。
两种方式的处理原理相似,都是通过设置HttpServletRequest的解码方式。关于扩展ActionServlet在第3章已经介绍过了,此处介绍第二种方式——利用Filter处理编码。
类似于扩展ActionServlet,利用Filter也只需要在doFilter方法的中增加设置HttpServletRequest的解码方式即可。
下面是本应用所使用的Filter的源代码:
public class UserLoginFilter implements Filter
{
//用于本系统所使用的编码方式
protected String encoding = null;
//保存Filter的配置对象
protected FilterConfig filterConfig = null;
//是否忽略配置的编码方式
protected boolean ignore = false;
protected String forwardPath = null;
//销毁Filter时调用该方法
public void destroy()
{
this.encoding = null;
this.filterConfig = null;
}
//处理用户请求
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
// 设置编码方式,web.xml里面有filter参数的初始化设置
if (ignore || (request.getCharacterEncoding() == null))
{
String encoding = selectEncoding(request);
//设置编码方式
if (encoding != null)
request.setCharacterEncoding(encoding);
}
//将请求转换成HttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//将响应转换成HttpServletResponse
HttpServletResponse httpServletResponse =
(HttpServletResponse) response;
//获取客户端的请求地址
String requesturi = httpServletRequest.getRequestURI();
//通过检查session中的变量,过滤请求
HttpSession session = httpServletRequest.getSession();
Object currentUser = session.getAttribute(AppConstants.LOGIN_USER);
//当前会话用户为空而且不是请求登录,则退出登录,欢迎页面和根目录则退回到应用的根目录
if (currentUser == null
&& !requesturi.endsWith("/processLogin.do")
&& !requesturi.endsWith("/logout.do")
&& !requesturi.endsWith("/index.jsp")
&& !requesturi.endsWith(httpServletRequest.getContextPath()
+ "/")) {
httpServletResponse.sendRedirect(httpServletRequest
.getContextPath()
+ "/");
return;
}
chain.doFilter(request, response);
}
//初始化该Filter时调用该方法
public void init(FilterConfig filterConfig) throws ServletException
{
//初始化FilterConfig对象
this.filterConfig = filterConfig;
//通过filterConfig获取请求参数:encoding
this.encoding = filterConfig.getInitParameter("encoding");
//通过filterConfig获取请求参数:forwardpath
this.forwardPath = filterConfig.getInitParameter("forwardpath");
//通过filterConfig获取请求参数:ignore
String value = filterConfig.getInitParameter("ignore");
//处理ignore参数的值
if (value == null)
this.ignore = true;
else if (value.equalsIgnoreCase("true"))
this.ignore = true;
else if (value.equalsIgnoreCase("yes"))
this.ignore = true;
else
this.ignore = false;
}
protected String selectEncoding(ServletRequest request) {
return (this.encoding);
}
}
该Filter用于拦截整个应用的全部请求,但必须为其配置对应的参数,关于该Filter的配置代码如下:
<filter>
<!-- 指定Filter的名字-->
<filter-name>Login Filter</filter-name>
<!-- 指定Filter的实现类-->
<filter-class>
org.yeeku.webapp.filter.UserLoginFilter
</filter-class>
<!-- 指定Filter的第一个初始化参数:encoding -->
<init-param>
<param-name>encoding</param-name>
<param-value>GBK</param-value>
</init-param>
<!-- 指定Filter的第二个初始化参数:ignore -->
<init-param>
<param-name>ignore</param-name>
<param-value>false</param-value>
</init-param>
<!-- 指定Filter的第三个初始化参数:forwardpath -->
<init-param>
<param-name>forwardpath</param-name>
<param-value>index.jsp</param-value>
</init-param>
</filter>
<!-- 指定Filter负责拦截的URL:负责拦截所有请求 -->
<filter-mapping>
<filter-name>Login Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
9.5.6 JSP页面输出
JSP的功能相当简单,除了完成数据的收集,就是完成简单的数据显示。此处的数据显示主要用于从HttpServletRequest中获取Attribute,然后将其显示在JSP页面中,也包括对集合对象的显示。
JSP页面的显示主要依赖于Struts的标签库,下面以几个简单示例,来介绍JSP如何利用Struts输出集合内容。
<logic:present name="categories" scope="request">
<logic:iterate id="item" name="categories" indexId="index" scope="request">
<tr>
<td width="10%" align="center">
<input type="radio" name="categoryId" value='<bean:write name="item" property="id"/>'>
</td>
<td width="30%" align="center">
<bean:write name="item" property="name"/>
</td>
</tr>
</logic:iterate>
</logic:present>
该代码用于输出所有的消息分类,从Action转发到该页面时,则在HttpServletRequest中封装了categories的属性,该属性是个集合对象,集合里每个元素都是Category对象。
页面首先通过logic:present标签判断categories属性是否存在,如果该属性不存在,则不用输出;如果该属性存在,则使用logic.iterate迭代器标签,循环输出categories集合中每个元素。该迭代器每次迭代时将集合中的元素放在page范围内,并在迭代器标签的标签体内使用bean:write标签,输出集合属性中每个元素的属性值。
图9.6显示了该代码的效果。

图9.6 迭代器标签的输出效果
再看下面的的页面输出代码:
<logic:messagesPresent>
<div class="title">
<bean:message key="errors.header"/>
<ul>
<html:messages id="error">
<li><bean:write name="error"/></li>
</html:messages>
</ul>
</div>
</logic:messagesPresent>
该片段主要用于输出校验信息,页面首先通过logic:messagesPresent标签来判断是否包含出错提示信息,如果包含出错提示,则输出该出错提示;如果不存在,则无须输出任何内容。
当出错提示存在时,先使用<bean:message key="errors.header"/>输出国际化信息,然后使用html:messages标签,对系统包含的出错提示逐行输出。
图9.7显示了出错提示的显示效果。

图9.7 服务器端数据校验的提示信息






