最近对Struts2非常着迷,整天研究李刚老师的《Struts2权威指南》,对Struts2也越来越有感觉了。刚做了关于在Struts2应用中使用JSF插件的例子。如下:
通过实现一个基本的增加记录、列出记录、修改记录的简单应用来示范如何在Struts2应用中使用JSF页面组件。
一. 实现业务逻辑组件
本系统模拟了一个缩微版的Java EE应用,有单独的业务逻辑组件,业务逻辑组件通过Spring容器来创建、管理。为了简单起见,本应用没有提供持久层,这意味这系统状态只是保存在程序运行过程中,一旦程序运行终止,系统状态将会丢失。
提示:如果需要系统运行状态持久保存,则应该为业务逻辑组件提供底层的DAO组件,当业务逻辑组件进行业务逻辑访问时,DAO组件完成底层数据库的的持久化访问。
本系统的业务逻辑组件实现了简单的增加图书、获取图书列表等方法,该业务逻辑组件的代码如下:
public class BookService
{
//bookDb就是本示例应用的程序状态
private Set<Book> bookDb;
//无参数的构造器
public BookService()
{
//完成程序状态的初始化,程序状态中包含两本图书
bookDb = new HashSet<Book>();
bookDb.add(new Book(1 , "Spring2.0宝典" , "全面介绍了Spring各个知识点"));
bookDb.add(new Book(2 , "轻量级J2EE企业应用实战" , "介绍实际企业的J2EE开发过程"));
}
//列出所有图书
public Set<Book> getAllBook()
{
return bookDb;
}
//根据“主键”来取得对应的图书
public Book getBookById(int id)
{
for (Book b : bookDb)
{
if (b.getId() == id)
{
return b;
}
}
return null;
}
//增加图书
public void addBook(Book b)
{
bookDb.add(b);
}
}
本程序并没有提供修改图书的方法,但实际上需要修改图书的操作。这是因为修改图书也是通过addBook(Book b)方法实现的。注意到本程序中的程序状态是一个HashSet集合,HashSet集合有一个特征:该集合里的元素不能重复——这意味着,如果我们能添加的两个Book实例相等,则后添加的Book实例会覆盖集合中已有的Book实例。
为了让HashSet集合能识别到两本图书的相等,我们必须重写Book实体的equals和hashCode方法。下面是本系统中Book类的代码:
//这是一个简单的JavaBean,充当本示例应用中的Model
public class Book
{
//Model中的三个属性
private int id;
private String name;
private String desc;
//两个构造器
public Book()
{
}
public Book(int id , String name ,String desc)
{
this.id = id;
this.name = name;
this.desc = desc;
}
//id属性的setter和getter方法
public void setId(int id)
{
this.id = id;
}
public int getId()
{
return this.id;
}
//此处省略了name和desc两个属性的setter和getter方法
...
//根据Book实例的id得到对应的hashCode
public int hashCode()
{
return id;
}
//重写equals方法,根据Book实例的id属性来判断两个实例是否相等。
public boolean equals(Object target)
{
if (target instanceof Book)
{
Book b = (Book)target;
if (b.getId() == this.id)
{
return true;
}
}
return false;
}
}
实现了上面的业务逻辑组件之后,必须将该业务逻辑组件配置在Spring容器中,在Spring容器中部署业务逻辑组件的配置文件如下:
<?xml version="1.0" encoding="GBK"?>
<!-- 指定Spring配置文件的Schema信息 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 在Spring容器中配置业务逻辑组件 -->
<bean id="bs" class="lee.service.BookService"/>
</beans>
因为上面的Spring容器中没有配置Action实例,这可见我们将会通过自动装配的方式将该业务逻辑组件注入Action实例。
为了在Struts2应用中使用Spring框架,所以我们还应该在该应用中安装Spring插件,将Struts2框架的lib路径下的struts2-spring-plugin-2.0.6.jar文件复制到Web应用的WEB-INF/lib路径下;当然,还必须将spring.jar文件也复制到WEB-INF/lib路径下。
除此之外,还应该在web.xml文件中配置当应用启动时自动加载Spring容器。
提示:关于Spring与Struts2整合的详细细节,请参阅第十二章的内容。
本应用的web.xml文件代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 指定Web应用配置文件的Schema信息 -->
<web-app id="jsf" 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">
<!-- 定义Struts2的核心过滤器 -->
<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<!-- 定义Struts2的核心过滤器拦截所有请求 -->
<filter-mapping>
<filter-name>struts</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 定义一个Listener,该Listener在应用启动时加载MyFaces的Context -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 定义一个Listener,该Listener在应用启动时创建Spring容器 -->
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
<!-- 配置JSF的FacesServlet,让其在应用启动时加载 -->
<servlet>
<servlet-name>faces</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 让FacesServlet拦截所有以*.action结尾的请求 -->
<servlet-mapping>
<servlet-name>faces</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
</web-app>
至此,已经完成了本应用的业务逻辑层的实现,只剩下MVC的实现了。
二.列出全部图书
列出全部图书的请求无需进行任何处理,只需要该Action完成一个属性的初始化,这个属性用于保持着系统中的全部图书。
因此为Action类中增加如下getter方法:
//定义一个getAllBook方法,增加该方法后可以认为该Action中包含了allBook属性
public List<Book> getAllBook()
{
List<Book> result = new ArrayList<Book>();
//调用bs对象的getAllBook()方法取出系统中的全部图书
//并通过遍历系统中全部图书,将全部图书对象组成一个List对象后返回。
for (Book b : bs.getAllBook())
{
result.add(b);
}
return result;
}
上面方法中用到了一个bs对象,这个对象就是系统的业务逻辑组件,这个业务逻辑组件由Spring插件使用自动装配注入到该Action实例的。为了让Spring插件可以将该业务逻辑组件注入该Action实例,应该为该Action增加如下代码:
//定义Action所需的业务逻辑组件
private BookService bs;
//系统自动装配业务逻辑组件的setter方法
public void setBs(BookService bs)
{
this.bs = bs;
}
定义了上面的Action类后,还必须在struts.xml文件中配置该Action,配置该Action的配置片段如下:
<package name="lee" extends="jsf">
<!-- 定义一个名为list的Action -->
<action name="list" class="lee.action.BookAction">
<!-- 定义sussess逻辑视图对应的Result -->
<result name="success" type="jsf"/>
</action>
...
</package>
上面配置片段配置了系统的list Action,但该Action中并未显式配置jsfStack的拦截器栈,这是因为该Action所在的包继承了前面配置的jsf包,jsf包中配置了默认的拦截器引用:jsfFullStack,这个拦截器栈中包含了jsfStack拦截器栈。可见,虽然list Action并未显式配置jsfStack拦截器栈,但实际还是会使用jsfStack拦截器栈。
完成如上定义后,看到list Action下仅有一个success视图映射,这是因为list Action直接使用ActionSupport类的execute方法处理用户请求,该方法总是返回success逻辑视图。list Action下的success逻辑视图对应一个类型为jsf的Result,该Result会转到list.jsp页面。
为了在list.jsp页面中列出所有的Book实例,我们使用JSF的<h:dataTable .../>页面组件,当然<h:dataTable .../>组件不可单独使用,它通常和<h:column .../>组件一起使用。下面是本应用中的list.jsp页面代码:
<%@ page language="java" contentType="text/html; charset=GBK"%>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<html>
<head>
<title> Struts2+MyFaces+Spring整合</title>
</head>
<body>
<f:view>
<h3>Struts2+MyFaces+Spring整合</h3>
<h3>列出所有图书</h3>
<!-- 使用dataTable组件输出action中的allBook属性 -->
<h:dataTable value="#{action.allBook}" var="b"
style="text-align:center;width:500px" border="1">
<h:column>
<f:facet name="header">
<h:outputText value="图书ID" />
</f:facet>
<!-- 定义一个超级链接,链接到edit.action,传入b对象id属性作为参数 -->
<h:outputLink value="edit.action">
<f:param name="editId" value="#{b.id}" />
<h:outputText value="#{b.id}" />
</h:outputLink>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="图书名" />
</f:facet>
<h:outputText value="#{b.name}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="图书简介" />
</f:facet>
<h:outputText value="#{b.desc}" />
</h:column>
</h:dataTable>
<p>
<!-- 定义一个超级链接,链接到edit.action -->
<h:outputLink value="edit.action">
<h:outputText value="新增图书"/>
</h:outputLink>
</p>
</f:view>
</body>
</html>
从上面页面文件文件中可以看出,上面的页面大量使用了MyFaces的页面组件,为了访问Action实例的属性,MyFaces组件允许通过action引用到该页面对应的Action,例如要访问该页面对应Action里的allBook属性,通过如下代码片段即可:
#{action.allBook}
上面代码相当于获得Action实例的getAllBook()方法的返回值。
在浏览器中直接向list.action发送请求。
三. 添加/修改图书
添加图书、修改图书的页面、以及处理逻辑几乎完全一样,唯一的区别在于,当修改图书时,在进入下一个页面时,必须先获取要修改图书的状态信息;而添加图书,则直接新建一个图书实例即可。
添加/修改图书都是通过BookAction类来实现的,也无需使用处理方法来处理该请求,只需要完成一个名为currentBook属性的初始化即可,currentBook属性就是即将在下一个页面中显示的Book实例。
为了完成currentBook实例的初始化,应该在BookAction中增加如下代码:
//Action里包含的currentBook属性
private Book currentBook;
//currentBook属性的setter方法
public void setCurrentBook(Book currentBook)
{
this.currentBook = currentBook;
}
public Book getCurrentBook()
{
//如果editId请求参数不为空,表示进入修改图书,页面
if (editId != 0)
{
//取出指定“主键”的Book实例
this.currentBook = bs.getBookById(editId);
}
//如果editId参数为0(即不包含editId参数),且currentBook属性为空
//这是进入新增图书的状态
else if (currentBook == null)
{
currentBook = new Book();
}
//还有保存图书也需要使用该方法
return this.currentBook;
}
因为上面的getCurrentBook()在进入修改、新增图书时要使用,执行实际修改、新增时也需要使用该方法,故上面方法分别考虑了如上几种情况。
除此之外,当编辑指定图书时,还需要传入额外的editId参数,故还需要增加如下代码:
//封装editId请求参数的属性
private int editId;
//editId属性的setter和getter方法
public void setEditId(int editId)
{
this.editId = editId;
}
public int getEditId()
{
return this.editId;
}
实际上,保存图书也使用该Action的save方法,save方法的实现非常简单,只需调用简单的bs.addBook(currentBook);代码即可,这个业务逻辑组件会将currentBook的Book实例添加到系统状态中——添加时,如果该图书是系统中已经存在图书,则是修改图书,否则将是新增图书。
下面是BookAction类的代码:
public class BookAction extends ActionSupport
{
//Action的currentBook属性,该属性用于在编辑图书、新增图书时使用
private Book currentBook;
//封装编辑图书ID请求参数的属性
private int editId;
//Action依赖的业务逻辑组件
private BookService bs;
//省略依赖注入业务逻辑组件的setter方法
...
//省略currentBook属性的setter方法
...
//currentBook属性的getter方法参考上面实现
...
//省略editId属性的setter和getter方法
...
//取出全部图书列表的方法
public List<Book> getAllBook()
{
List<Book> result = new ArrayList<Book>();
for (Book b : bs.getAllBook())
{
result.add(b);
}
return result;
}
//修改/新增图书的保存方法
public String save()
{
bs.addBook(currentBook);
//保存成功后返回list逻辑视图
return "list";
}
}
下面在配置文件中配置edit Action,配置edit Action还应该增加一个名为list的Result,该Result返回列出所有图书。
下面是本应用的struts.xml文件代码:
<?xml version="1.0" encoding="GBK"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.custom.i18n.resources" value="messageResource"/>
<constant name="struts.i18n.encoding" value="GBK"/>
<!-- 定义了一个jsf包,该包继承了系统原有的jsf-default -->
<package name="jsf" extends="jsf-default">
<interceptors>
<!-- 重新定义一个拦截器栈 -->
<interceptor-stack name="jsfFullStack">
<!-- 引用Struts2中原有的基本拦截器 -->
<interceptor-ref name="params" />
<interceptor-ref name="basicStack"/>
<!-- 引用JSF插件中的jsfStack拦截器栈 -->
<interceptor-ref name="jsfStack"/>
</interceptor-stack>
</interceptors>
<!-- 将jsfFullStack定义成系统默认的拦截器引用 -->
<default-interceptor-ref name="jsfFullStack"/>
</package>
<package name="lee" extends="jsf">
<!-- 定义一个名为list的Action -->
<action name="list" class="lee.action.BookAction">
<!-- 定义sussess逻辑视图对应的Result -->
<result name="success" type="jsf"/>
</action>
<!-- 定义一个名为edit的Action -->
<action name="edit" class="lee.action.BookAction">
<result name="success" type="jsf"/>
<!-- 定义返回list逻辑视图时,重定向到list.action -->
<result name="list" type="redirect">list.action</result>
</action>
</package>
</struts>
当用户请求编辑/新增图书时,都会使用edit.action,此时将直接调用该Action里的execute方法处理用户请求,该方法继承自ActionSupport类,总是返回success字符串,即总是进入edit.jsp页面。
edit.jsp页面同样大量使用MyFaces的页面组件,下面是edit.jsp页面的代码:
<%@ page language="java" contentType="text/html; charset=GBK"%>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<html>
<head>
<title>Struts2+MyFaces+Spring整合</title>
</head>
<body>
<f:view>
<h3>Struts2+MyFaces+Spring整合</h3>
<h3>修改/保存图书</h3>
<h:form>
<h:inputHidden value="#{action.editId}"/>
<h:panelGrid columns="3">
<h:outputText value="图书ID"/>
<h:inputText id="id" size="5" value="#{action.currentBook.id}" required="true" />
<h:message for="id" />
<h:outputText value="图书名:"/>
<h:inputText id="name" size="30" value="#{action.currentBook.name}" required="true">
<!-- 使用MyFaces的字符串长度校验器 -->
<f:validateLength minimum="2" maximum="100" />
</h:inputText>
<h:message for="name" />
<h:outputText value="图书描述:" />
<h:inputText id="desc" size="30" value="#{action.currentBook.desc}" required="true">、
<!-- 使用MyFaces的字符串长度校验器 -->
<f:validateLength minimum="2" maximum="100" />
</h:inputText>
<h:message for="desc" />
</h:panelGrid>
<!-- 将该按钮的action绑定到Struts2的当前Action的save -->
<h:commandButton value="保存" action="#{action.save}" />
<br/>
</h:form>
</f:view>
</body>
</html>
上面页面都是使用了MyFaces的页面组件来输出Action实例里的currentBook属性,这没有丝毫特别之处,值得注意的是页面中的“保存”按钮,该按钮被绑定到当前Action的save方法——这意味直接将按钮的提交动作关联到指定Action的指定方法。
注意:上面页面代码中还使用了MyFaces的字符串长度校验器,这可以很方便的实现输入校验功能。