五.改进控制器
通过前面的几个小步骤,运行了一个最简单的struts2的应用,但是还需要进一步的改进前面的Action类,例如该Action类可以通过实现Action接口,利用该接口的优势,前面应用的Action类没有与JavaBean交互,没有将业务逻辑操作的结果显示给客户端.
(1) 实现Action接口
表面上看起来,实现Action的接口没有太大的好处,但是事实上,实现了Action的接口可以帮助开发者更好的实现Action类.下面的代码是Action接口的定义.
public interface Action { public static final String SUCCESS="success"; public static final String NONE="none"; public static final String ERROR="error"; public static final String INPUT="input"; public static final String LOGIN="login"; public String execute() throws Exception; }
在上面的Action代码中,我们发现该Action接口中已经定义了5个标准的字符串常量,SUCCESS,NONE,ERROR,INPUT,LOGIN他们可以简化execute方法的返回值,并且可以使execute的返回值标准化.例如,对于成功处理,返回SUCCESS常量,避免直接返回一个success字符串(程序中应该尽量的避免直接返回数字,字符串等等).因此,借助于上面的Action接口,我们可以将原来的Action类的代码修改为如下:
package com.supermos.app; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionContext; public class LoginAction implements Action{ private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String execute()throws Exception{ if("supermos".equalsIgnoreCase(username)&&"ziwen".equalsIgnoreCase(password)){ return SUCCESS; }else{ return ERROR; } } }
对比前面Action类和此处的Action的实现类,我们发现两个Action类的代码基本上是相似的.除了后面的Action类实现了Action接口,因为实现了Action接口,故Action类的execute方法可以返回Action接口里的字符串常量.
(2) 跟踪用户的状态
前面的Action在处理完用户的登录之后仅仅的执行了页面的转发,并没有跟踪用户的状态信息,通常,当一个用户成功登陆之后,就需要将用户的用户名添加为Session状态信息.
为了访问HttpSession实例,Struts2提供了一个ActionContext类,该类提供了一个getSession方法,该方法的值不是HttpSession,而是一个Map对象,这是怎么会事呢?实际上,这与Struts2的设计哲学有关,Struts2为了简化Action类的测试,将Action与ServletAPI完全分离,因此getSession方法的返回值类型是Map,而不是HttpSession.
虽然ActionContext的getSession返回的不是HttpSession对象,但是Struts2的系列拦截器会负责Session和HttpSession之间的转换.
为了跟踪用户的信息,我们修改Action的execute方法,在execute方法中通过ActionContxt访问Web应用的Session,修改之后的execute方法如下:
public String execute()throws Exception{ if("supermos".equalsIgnoreCase(username)&&"ziwen".equalsIgnoreCase(password)){ //通过ActionContext对象访问Web应用的Session ActionContext.getContext().getSession().put("user", getUsername()); return SUCCESS; }else{ return ERROR; } }
上面的代码仅提供了Action类的execute方法,该Action类的其他部分与前面的Action代码完全一样,在上面的Action类通过ActionContext设置了一个Session属性:user.为了检验我们设置的session属性是否成功,我们修改welcoe.jsp页面,在welcome.jsp页面中使用JSP2.0表达式语法输出Session中的user属性.下面是修改后的welcome.jsp页面的代码:
<%@page language="java" contentType="text/html;charset=utf-8"%> <html> <body> <h2 align="center">${sessionScope.user},登录成功</h2> </body> </html>
上面的JSP页面与前面的JSP页面相比没有太大的改变,除了使用JSP2.0语法来输出Session中的user属性.
(3) 添加处理信息
到目前为止,Action仅仅控制转发用户的请求,JSP页面并没有获得Action的处理结果,对于大部分的Web应用而言,用户需要获得请求Action的处理结果,比如说吧,在在线购物系统中,需要查询某个种类下面的所有的商品的信息,则Action调用业务逻辑组件的业务逻辑方法来得到该种类下的所有的商品的信息.而JSP负责获取该Action的处理结果,并且将所有的结果迭代输出.
下面将为应用增加一个Action,该Action负责获取某个系列的全部的书籍,为了让Action可以获取整个系列的所有的书籍,我们增加一个业务逻辑组件,它包含有一个业务逻辑方法,该方法可以获得某个系列的所有的图书.
下面是系统所用的业务逻辑的代码:
package com.supermos.service; public class BookService { private String[] books=new String[]{ "c++内容", "java内容", "php内容", "corejava书籍", "金庸古龙", "unix入门" }; //业务逻辑方法,返回全部的图书 public String[] getBooks(){ return this.books; } }
上面的业务逻辑组件实际上就是MVC模式中的model,他负责实现系统业务逻辑方法,理论上,业务逻辑组件实现业务逻辑方法时,必须依赖底层的持久层组件,但是此处的业务逻辑的组件只是返回一个静态的字符串的数组,因为这只是一个模拟.
在系统中增加如下Action类,该Action类先判断Session中的User属性是否存在,如果用户已经登录本系统,那么就可以获取系统中所有的书籍,否则就返回登录页面.
新增的Action类的代码如下:
package com.supermos.app; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionContext; import com.supermos.service.BookService; public class GetBooksAction implements Action { private String[] books; public String[] getBooks() { return books; } public void setBooks(String[] books) { this.books = books; } public String execute() throws Exception { String user=(String)ActionContext.getContext().getSession().get("user"); if(user!=null&&user.equalsIgnoreCase("SuperMos")){ BookService bs=new BookService(); setBooks(bs.getBooks()); return SUCCESS; } return LOGIN; } }
通过上面的Action类,我们发现Action类中的成员属性,并不一定用于封装用户的请求参数,也可能是封装了Action需要传入下一个JSP页面中显示的属性.(Action中的成员属性,并不一定用于封装用户的请求参数,也可能是封装了Action需要传入下一个页面显示的值,实际上,这些值将被封装到ValueStack对象当中.)
当我们的控制器需要调用业务逻辑方法时,我们直接创建了一个业务逻辑组件的实例,这并不是一种很好的做法,因为控制器不应该关心业务逻辑组件的实例化过程,比较成熟的做法可以利用工厂模式来管理业务逻辑组件,当然,目前最流行的方法是利用依赖注入IOC.(实际项目当中不会在控制器当中直接创建业务逻辑组件的实例,而是通过工厂模式管理业务逻辑组件实例,或者通过依赖注入将业务逻辑组件实例诸如控制器组件.)
该Action处理用户请求的时候,无需获得用户的任何请求参数,将该Action配置在struts.xml文件中,配置该Action的配置片段如下:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN" "http://struts.apache.org/dtds/struts-2.1.dtd"> <struts> <package name="strutsqs" extends="struts-default"> <action name="login" class="com.supermos.app.LoginAction"> <result name="success">/success.jsp</result> <result name="error">/fail.jsp</result> </action> <action name="GetBooks" class="com.supermos.app.GetBooksAction"> <result name="login">/login.jsp</result> <result name="success">/showBook.jsp</result> </action> </package> </struts>
当用户向GetBooks.action发送请求的时候,该请求将被转发给GetBooksAction处理.
(4) 输出处理信息
如果用户没有登录,直接向getBooks.action发送请求,该请求就被转发到login.jsp页面.如果用户已经登录,getBooks.action将从系统中加载到系统中的所有的图书,并将请求转发给showBooks.jsp页面,因此,showBook.jsp页面必须负责输出所有的图书.
当Action设置了某个属性值之后,Struts2将这些属性值全部封装到一个叫做struts.valueStack的请求属性里面.
为了在JSP页面中输出需要输出的图书信息,我们可以通过如下代码来获取包含全部输出信息的ValueStack对象:
//获取封装输出信息的ValueStack对象 ValueStack vs=(ValueStack)request.getAttribute("struts.valueStack");
上面的代码返回的是一个ValueStack对象,该对象封装了全部的输出信息,该对象是Struts2使用的一个ValueStack对象,可以通过OGNL表达式非常方便的访问该对象封装的信息.
从数据结构上面来看,ValueStack类似于Map结构,但是它比Map结构更加的强大(因为它可以根据表达式来查询值).Action所有的属性都被封装到了ValueStack对象中,Action中的属性名可以理解为ValueStack中value的名字.
大致理解了ValueStack对象的机构之后,我们可以通过如下代码来获取Action中设置的全部的图书的信息.
//调用ValueStack的findValue方法获取Action的books属性值 String[] books=(String[])vs.findValue("books");
理解了上面的关键的两个步骤,整个JSP页面的代码就非常的容易了,下面就是showBook.jsp页面的代码:
<%@ page language="java" pageEncoding="utf-8"%> <%@ page import="java.util.*,com.opensymphony.xwork2.util.*" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>显示书籍信息</title> </head> <body> <table align="center" border="1"> <caption>图书</caption> <% //获取封装输出信息的ValueStack对象 ValueStack vs=(ValueStack)request.getAttribute("struts.valueStack"); //调用ValueStack的findValue方法获取Action的books属性值 String[] books=(String[])vs.findValue("books"); for(int i=0;i<books.length;i++) { %> <tr> <td>书名</td> <td><%=books[i]%></td> </tr> <%} %> </table> </body> </html>
不可否认,上面的JSP页面的代码是非常的丑陋的,而且是非常难以维护的,因为里面嵌入了大量的JSP脚本,但它对于初学者理解Struts2如何处理封装在Action的ValueStack却有非常大的帮助.
六.改进视图组件
通过前面的介绍,我们已经明白了Struts2 MVC的基本的数据流,已经完成了Struts中Model,Controller,View三个组件的开发,但是应用中的视图组件:JSP页面非常丑陋,特别是输出Action返回信息的JSP页面,使用了大量的Java脚本来控制输出,下面将会使用Struts2的标签来改善整个应用视图组件.
(1) 改善输出页面
为了控制输出Struts2的ValueStack中封装的值,Struts2提供了大量的标签,其中比较常用的标签有:
l if:该标签支持标签体,如果if标签里面判断的表达式返回真,则输出标签体中的内容.
l else:该标签不能独立使用,它需要与if标签结合使用,如果if标签内判断的表达式为false,则输出该标签里面的标签体.
l iterator:迭代器,主要用于迭代输出某个集合属性里面的各个集合元素
l property:该标签用于输出指定的属性值
通过上面的几个标签,替换showBook.jsp页面中的java脚本,修改之后的showBook.jsp页面的代码如下:
<%@ page language="java" pageEncoding="utf-8"%> <!-- -导入struts2的标签库 --> <%@taglib uri="/struts-tags" prefix="s"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>显示书籍信息</title> </head> <body> <table align="center" border="1"> <caption><h3>书籍信息</h3></caption> <!-- 迭代输出ValueStack中的books对象,其中status是迭代的序号 --> <s:iterator value="books" status="index"> <!-- 判断序号是否为奇数 --> <s:if test="#index.odd==true"> <tr style="background-color:#cccccc" mce_style="background-color:#cccccc"> </s:if> <!-- 判断迭代元素的序号是不是为偶数 --> <s:else> <tr> </s:else> <td>书名</td> <td><s:property/></td> </tr> </s:iterator> </table> </body> </html>
上面的JSP页面使用了Struts2的标签库,因此必须在JSP页面的首部添加taglib指定,该taglib指令用于导入标签库.(如果需要使用某个标签库中的标签,那么必须在页面开始导入该标签库.)
页面中使用Struts2的iterator标签迭代输出ValueStack中的books数组,并为每个数组元素定义了一个序号:index.通过判断序号是否为奇数,如果行序号为奇数,则输出一个有背景色的表格行,否者输出一个没有背景色的表格行.在页面中使用Struts2脚本控制输出,完全消除了页面中的Java脚本,降低了该页面的后期的维护成本.
(2) 使用UI标签简化表单的页面
Struts2为常用的表单域都提供了对应的标签,下面是常用的表单域标签:
l form:对应一个表单元素
l checkbox:对应一个复选框元素
l password:对应一个密码输入框
l radio:对应一个单选框元素
l reset:对应一个重设按钮
l select:对应一个下拉列表框
l submit:对应一个提交按钮
l textarea:对应一个多行文本域
l textfield:对应一个单行文本框
关于这些界面相关的标签,还有很多..下面是使用了Struts2的表单相关标签简化用户登录的login.jsp页面,修改之后的login.jsp页面的代码如下:
<%@ page language="java" pageEncoding="utf-8"%> <!-- -导入struts2的标签库 --> <%@taglib uri="/struts-tags" prefix="s"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>struts2的Helloworld</title> </head> <body> <s:form action="login"> <!-- 生成一个用户名文本输入框 --> <s:textfield name="username" label="用户名"/> <!-- 生成一个密码文本输入框 --> <s:textfield name="password" label="密 码"/> <!-- 生成一个提交按钮 --> <s:submit value="登录"/> </s:form> </body> </html>