[置顶] 在Struts2应用中使用JSF插件

时间:2021-07-31 20:08:43

    最近对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的字符串长度校验器,这可以很方便的实现输入校验功能。