本系列教程将详细介绍Struts 1.x的基本原理和使用方法,在这篇文章中将以一个简单的例子(mystruts)来演示如何使用MyEclipse来开发、运行Struts程序,并给出了解决ActionForm出现乱码问题的方法。读者可以从本文中了解开发Struts 1.x程序的基本过程。
一、本文给出的程序要实现什么功能
mystruts是一个录入和查询产品信息的程序。为了方便起见,本例中的产品信息表只包括了产品ID、产品名称和产品价格三个字段。mystruts的主要功能如下:
1. 接受用户输入的产品ID、产品名称和产品价格。
2. 验证这些字段的合法性。如果某些字段的输入不合法(如未输入产品ID),程序会forward到一个信息显示页,并显示出错原因。
3. 如果用户输入了正确的字段值,程序会将这些字段值保存到数据库中,并显示“保存成功”信息。
4. 用户输入产品名称,并根据产品名称进行模糊查询。如果存在符合要求的产品信息。程序会以表格形式显示这些产品的信息,同时显示记录数。如果未查到任何记录,会显示“没有符合要求的记录!”信息。
二、编写程序前的准备工作
1. 建立数据库
在编写程序之前,需要建立一个数据库(struts)和一个表(t_products),建立数据库和表的SQL脚本如下所示:
# 建立数据库struts CREATEDATABASEIFNOTEXISTS struts DEFAULTCHARACTERSET GBK; # 建立表t_products CREATETABLEIFNOTEXISTS struts.t_products ( product_id varchar(4) NOTNULL, product_name varchar(50) NOTNULL, price floatNOTNULL, PRIMARYKEY (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=gbk;
2 建立一个支持struts1.x的samples工程
用MyEclipse建立一个samples工程(Web工程),现在这个samples工程还不支持Struts1.x(没有引入相应的Struts jar包、struts-config.xml文件以及其他和Struts相关的配置)。然而,在MyEclipse中这一切并不需要我们手工去加入。而只需要使用MyEclipse的【New Struts Capabilities】对话框就可以自动完成这些工作。
首先选中samples工程,然后在右键菜单中选择【MyEclipse】 > 【New Struts Capabilities】,启动【New Struts Capabilities】对话框。对默认的设置需要进行如下的改动:
(1)将Struts specification改为Struts 1.2。
(2)将Base package for new classes改为struts。
(3)将Default application resources改为struts.ApplicationResources。
改完后的【New Struts Capabilities】对话框如图1所示。
图1
在设置完后,点击Finish按钮关闭对话框。在向samples工程添加支持Struts的功能后,主要对samples工程进行了三个操作。
(1)引入了Struts 1.2 的jar包(在samples的工程树中多了一个Struts 1.2 Libraries节点)。
(2)在WEB-INF目录中添加了一个struts-config.xml文件。文件的默认内容如下面的代码所示:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.2//EN" "http://struts.apache.org/dtds/struts-config_1_2.dtd"> <struts-config> <data-sources /> <form-beans /> <global-exceptions /> <global-forwards /> <action-mappings /> <message-resources parameter="struts.ApplicationResources"/> </struts-config> (3)在WEB-INF中的web.xml文件中添加了处理Struts动作的ActionServlet的配置,代码如下: <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>3</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>3</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
到目前为止,samples工程已经完全支持Struts了。读者可以看到,如果不使用MyEclipse,那么上面所列出的配置文件的内容都必须手工输入。因此,使用MyEclipse来开发Struts程序可以省去很多配置xml文件的工作。
三、实现程序的首页(index.jsp)
首先在<samples工程目录>中建立一个mystruts目录,然后在<samples工程目录>" mystruts目录中建立一个index.jsp文件,这个文件的内容如下。
<%@ page pageEncoding="GBK"%> <%-- 引用Struts tag--%> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%> <html> <head> <title>主界面</title> </head> <body> <table align="center" cellpadding="10" width="100%"> <tr> <td align="right" width="50%"> <%-- 使用Struts tag--%> <html:link forward="newProduct">录入产品信息</html:link> </td> <td> <html:link forward="searchProduct">查询产品信息</html:link> </td> </tr> </table> </body> </html>
在MyEclipse中启动Tomcat(如果Tomcat处于启动状态,在修改完配置文件后,建议在MyEclipse的Servers页重新发布samples工程,以使修改生效)。在IE中输入如下的URL:
http://localhost:8080/samples/mystruts/index.jsp
我们发现在输入上面的URL后,在IE中并未显示正确的运行结果,而是抛出了如下的异常:
java.net.MalformedURLException: Cannot retrieve ActionForward named newProduct
这个异常表明程序并未找到一个叫newProduct的forward(forward将在后面详细地讲述)。因此,可以断定,在JSP中使用forward时,这个forward必须存在。下面我们来添加index.jsp页面中所使用的两个forward:newProduct和searchProduct。这两个forward分别引向了建立产品信息的页面(newProduct.jsp)和查询产品信息的页面(searchProduct.jsp)。我们可以在struts-config.xml文件中<struts-config>节点中添加两个全局的forward,代码如下:
<global-forwards> <forward name="newProduct" path="/mystruts/newProduct.jsp"/> <forward name="searchProduct" path="/mystruts/searchProduct.jsp"/> </global-forwards>
上面的代码中所示的newProduct.jsp和searchProduct.jsp目前并不存在(将在以后实现这两个JSP页面),现在重新输入上述的URL,会得到如图2所示的效果。
图2
如果想让index.jsp成为默认的JSP页面,可以在web.xml中的<welcome-file-list>节点中加入如下的内容:
<welcome-file>index.jsp</welcome-file>
这时在IE中只要输入如下的URL就可以访问index.jsp页面了。
http://localhost:8080/samples/mystruts
四、实现添加和查询产品信息页面
在本节中主要实现了用于输入产品信息(newProduct.jsp)和查询产品信息(searchProduct.jsp)的JSP页面。
在newProduct.jsp页面中有一个form,在form中含有三个文本框,用于分别输入产品ID、产品名称和产品价格。
在<samples工程目录>"mystruts目录中建立一个newProduct.jsp文件,代码如下:
<%@ page pageEncoding="GBK"%> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%> <html> <head> <title>录入产品信息</title> </head> <body> <%-- 向saveProduct动作提交产品信息 --%> <html:form action="saveProduct"> <table width="100%"> <tr> <td align="center"> 产品编号: <html:text property="productID" maxlength="4"/> <p> 产品名称: <html:text property="productName"/> <p> 产品价格: <html:text property="price"/> </td> </tr> <tr> <td align="center"> <br> <html:submit value=" 保存 "/> </td> </tr> </table> </html:form> </body> </html>
在searchProduct.jsp页面中有一个form,为了方便起见,在form中只提供了一个文本框用于对产品名称进行模糊查询。在<samples工程目录>" mystruts目录中建立一个searchProduct.jsp文件,代码如下:
<%@ page pageEncoding="GBK"%> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%> <html> <head> <title>查询产品信息</title> </head> <body> <%-- 向searchProduct动作提交查询请求 --%> <html:form action="searchProduct"> <table width="100%"> <tr> <td align="center"> 产品名称: <html:text property="productName"/> </td> </tr> <tr> <td align="center"> <br> <html:submit value=" 查询 "/> </td> </tr> </table> </html:form> </body> </html>
现在启动Tomcat,并使用如下两个URL来访问newProduct.jsp和searchProduct.jsp:
http://localhost:8080/samples/mystruts/newProduct.jsp
http://localhost:8080/samples/mystruts/searchProduct.jsp
在IE中输入上面的两个URL后,并不能显示出相应的界面,而会抛出JspException异常,表明未找到saveProduct和searchProduct动作。从这一点可以看出,如果在JSP中使用Struts Action,这些Action必须事先在struts-config.xml文件中定义,否则,JSP程序就无法正常访问。在这两个页面所使用的动作(saveProduct和searchProduct)将会在下面的部分介绍。
五、通过模型类操作数据库
在这一节我们来编写用于操作数据库的模型类。由于本例子是Web程序,因此,建议在连接数据库时使用数据库连接池。在<Tomcat安装目录>"conf"Catalina"localhost目录中打开samples.xml文件(如果没有该文件,则建立一个samples.xml文件),在<Context>节点中加入如下的内容:
配置连接池(用于连接数据库struts)
<Resource name="jdbc/struts" auth="Container" type="javax.sql.DataSource" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/struts?characterEncoding=GBK" username="root" password="1234" maxActive="200" maxIdle="50" maxWait="3000"/>
本例中提供了两个可以操作数据库的模型类:Product和SearchProduct。其中Product用于验证由客户端提交的产品信息,并向t_products表中写入这些信息。而SearchProduct类用于对t_products表的product_name字段进行模糊查询,并返回查询到的产品信息(包括产品ID、产品名称和产品价格)。
由于Product和SearchProduct都需要使用数据库连接池来连接数据库,因此,可以将连接数据库的工作提出来作为一个父类(Struts类)提供,代码如下:
package util; import java.sql.Connection; publicclass Struts { protected javax.naming.Context ctx = new javax.naming.InitialContext(); protected javax.sql.DataSource ds; protected Connection conn; public Struts() throws Exception { ds = (javax.sql.DataSource) ctx.lookup("java:/comp/env/jdbc/struts"); conn = ds.getConnection(); // 从数据库连接池获得一个Connection } }
在<samples工程目录>"src目录中建立一个Product.java文件,代码所示:
package mystruts.model; import java.sql.*; import mystruts.actionform.*; publicclass Product extends util.Struts { private ProductForm form; public Product(ProductForm form) throws Exception { super(); this.form = form; validate(); } // 验证客户端提交的数据 publicvoid validate() throws Exception { if (form.getProductID().trim().equals("")) thrownew Exception("产品ID不能为空!"); if(form.getProductID().length() > 4) thrownew Exception("产品ID最长为4位!"); if (form.getProductName().trim().equals("")) thrownew Exception("产品名称不能为空"); if (Float.compare(form.getPrice(), 0) <= 0) thrownew Exception("产品价格必须大于0"); } // 将客户端提交的产品信息保存到t_products中 publicvoid save() throws Exception { try { String productID = form.getProductID(); String productName = form.getProductName(); float price = form.getPrice(); String sql = "INSERT INTO t_products VALUES('" + productID + "'," + "'" + productName + "'," + String.valueOf(price) + ")"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.executeUpdate(); // 执行INSERT语句 pstmt.close(); conn.close(); } catch (Exception e) { thrownew Exception(e.getMessage()); } } }
在Product类中使用了一个ProductForm类,这个类是一个ActionForm类,它的功能是保存客户端提交的数据。关于这个类将在下面详细介绍。Product类通过构造方法的form参数将客户端提交的数据传入Product类的对象实例中,并在构造方法中验证这些数据,如果发现数据不合法,就会抛出一个异常。当客户端提交的数据合法后,成功建立了一个Product类的对象实例,然后可以通过简单地调用save方法将数据保存到t_products表中。
与Product类似,在<samples工程目录>"src目录中建立一个SearchProduct.java文件,代码如下:
package mystruts.model; import java.sql.*; import java.util.*; import mystruts.actionform.*; publicclass SearchProduct extends util.Struts { private ProductForm form; public SearchProduct(ProductForm form) throws Exception { super(); this.form = form; } // 查询产品信息,并通过List返回查询结果 public List<String[]> search() throws Exception { List<String[]> result = new LinkedList<String[]>(); String sql = "SELECT * FROM t_products WHERE product_name like '%" + form.getProductName() + "%'"; PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery(); // 开始执行SELECT语句 while(rs.next()) { String[] row = new String[3]; row[0] = rs.getString(1); row[1] = rs.getString(2); row[2] = rs.getString(3); result.add(row); } rs.close(); conn.close(); return result; } }
在SearchProduct类也使用了ProductForm类,但在SearchProduct中并不会验证ProductForm对象实例中的数据,而只是将ProductForm对象作为传递查询请求信息(实际上只需要产品名称)的工具而已。
六、实现控制器
在这一节要实现的控制器是基于Struts的Web程序的核心部分之一:控制器实质上也是普通的Java类,但这个Java类一般要从org.apache.struts.action.Action类继承。控制器的主要功能是接受并处理从JSP页面提交的数据、通过模型(Model)和数据库交互以及forward到相应的页面(可以是任何页面,如html、JSP和Servlet等)。在实现控制器之前,需要先实现一个ActionForm类, 这个类的作用是保存JSP页面提交的数据。在<samples工程目录>"src目录中建立一个ProductForm.java文件,代码如下:
package mystruts.actionform; import org.apache.struts.action.*; publicclass ProductForm extends ActionForm { private String productID; // 产品ID private String productName; // 产品名称 privatefloat price; // 产品价格 public String getProductID() { return productID; } publicvoid setProductID(String productID) { this.productID = productID; } public String getProductName() { return productName; } publicvoid setProductName(String productName) { this.productName = productName; } publicfloat getPrice() { return price; } publicvoid setPrice(float price) { this.price = price; } }
从上面的代码可以看出,ActionForm类一般从org.apache.struts.action.ActionForm类继承,而且在类中需要按着需要保存的数据表字段添加属性。如产品ID的属性是productName。在MyEclipse中可以只定义三个private变量,然后使用MyEclipse的【Source】 > 【Generate Getters and Setters...】功能自动产生getter和setter方法。但在给这些属性取名时要注意,private变量的名子和数据表的字段名没有直接的关系,但必须和JSP页面中的<html>标签的property属性值一致,如<html:text property="productName" />表示输入产品名称的文本框,其中property属性的值就是ProductForm类中的productName变量。如果不一致,将会抛出异常。其他和ProductForm类的属性对应的<html>标签可以查看上面的代码。
光有ActionForm类还不够,还需要在struts-config.xml中的<struts-config>节点中添加如下的内容:
<form-beans> <form-bean name="saveProductForm" type=" mystruts.actionform.ProductForm"/> <form-bean name="searchProductForm" type="mystruts.actionform.ProductForm"/> </form-beans>
上面的代码所配置的两个ActionForm实际上指的是同一个ProductForm类,但这个ProductForm类在后面要讲的两个动作里都要使用,为了更容易理解,为这个ProductForm起了两个不同的别名(saveProductForm和searchProductForm)。
下面来实现saveProduct动作的代码。Struts Action类必须一般从org.apache.struts.action.Action类继承。一般在Struts Action类需要覆盖Action类的execute方法。这个方法有每次客户端访问Struts Action时调用。我们可以在方法中处理客户端提交的数据,访问数据库等工作。这个方法返回一个ActionForward类型的值,表明在执行完execute后,要forward到的页面。描述saveProduct动作的类叫SaveProductAction。代码如下:
package mystruts.action; import javax.servlet.http.*; import org.apache.struts.action.*; import mystruts.actionform.*; import mystruts.model.*; publicclass SaveProductAction extends Action { // 在客户端访问saveProduct动作时执行该方法 public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { ProductForm saveProductForm = (ProductForm) form; try { Product product = new Product(saveProductForm); product.save(); // 保存产品信息 request.setAttribute("info", "保存成功!"); } catch (Exception e) { request.setAttribute("info", e.getMessage()); } return mapping.findForward("save"); } }
在SaveProductAction类中使用了模型类Product验证并保存产品信息。并将操作结果信息保存在request的属性中,key为“info”。在execute的最后,使用了ActionMapping类的findForward方法在struts-config.xml中寻找一个叫“save”的forward。这个forward是一个JSP页,用于显示是否将产品信息保存成功的信息。为了可以在struts-config.xml中查找这个forward,需要在struts-config.xml的<action-mappings>节点中加入如下的内容。
<action name="saveProductForm" path="/saveProduct"scope="request" type=" mystruts.action.SaveProductAction"> <forward name="save" path="/mystruts/save.jsp"/> </action>
从上面的代码可以看出,那个用于显示保存状态信息的JSP页面叫save.jsp。在<samples工程目录>"mystruts目录中建立一个save.jsp文件,代码如下:
<%@ page pageEncoding="GBK"%>
${requestScope.info}
在IE中输入如下的URL:
http://localhost:8080/samples/mystruts/newProduct.jsp
在文本框中输入相应的信息后,点“保存”按钮,如果输入的数据是合法的,就会将数据保存在t_products中,否则会显示出错的原因。 searchProduct动作的实现和saveProduct差不多,也会为三步:实现动作类(SearchProductAction)、在struts-config.xml中添加配置信息和实现用于显示查询结果的JSP文件。下面的代码分别显示了这三步所要编写的代码。
SearchProductAction.java
package mystruts.action; import javax.servlet.http.*; import org.apache.struts.action.*; import mystruts.actionform.*; import mystruts.model.*; import java.util.*; publicclass SearchProductAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { ProductForm searchProductForm = (ProductForm) form; try { SearchProduct searchProduct = new SearchProduct(searchProductForm); List<String[]> result = searchProduct.search(); // 查询产品信息 if (result.size() > 0) // 有符合条件的产品信息 { request.setAttribute("result", result); request.setAttribute("info", "记录数:" + String.valueOf(result.size())); } else// 没有查到任何产品信息 request.setAttribute("info", "没有符合要求的记录!"); } catch (Exception e) { request.setAttribute("info", e.getMessage()); } return mapping.findForward("search"); } }
在struts-config.xml中配置searchProduct动作
<action name="searchProductForm" path="/searchProduct" scope="request" type="mystruts.action.SearchProductAction"> <forward name="search" path="/mystruts/search.jsp"/> </action>
search.jsp
<%@ page pageEncoding="GBK"%> <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <html> <body> <%-- 从request的result中取出查询结果 --%> <c:set var="result" value="${requestScope.result}"/> <table width="100%"> <tr align="center"> <td> ${requestScope.info} </td> </tr> <tr align="center"> <td> <logic:present name="result"> <table border="1"> <tr align="center"> <td>产品ID </td> <td>产品名称</td> <td>价格</td> </tr> <logic:iterate id="row" name="result"> <tr><td> ${row[0]} </td> <td> ${row[1]} </td> <td> ${row[2]} </td> </tr> </logic:iterate> </table> </logic:present> </td> </tr> </table> </body> </html>
在IE中输入如下的URL:
http://localhost:8080/samples/%20mystruts/searchProduct.jsp
在“产品名称”文本框中输入产品名称的一部分,程序就会查询出所有包含输入的产品名称的产品信息,并将结果显示出来。
七、解决ActionForm的乱码问题
到现在为止,程序的功能部分已经全部实现完了。但还存在一个问题。当我们在产品名称中输入中文时,虽然将客户端提交的数据成功保存到数据库中,但是在t_products表中的product_name字段显示的都是乱码。产生这个问题的原因只有一个,就是客户端提交的数据的编码格式和数据库的编码格式不一致造成的。当然,解决这个问题的方法有很多,但笔者认为最容易的就是使用过滤器。所谓过滤器,就是在客户端提交数据后,在交由服务端处理之前所执行的一段服务端代码(一般为Java代码)。一个过滤器是一个实现javax.servlet.Filter接口的类。在本例中要使用的过滤器类叫EncodingFilter,实现代码如下:
EncodingFilter.java
package filter; import java.io.IOException; import javax.servlet.*; publicclass EncodingFilter implements Filter { publicvoid destroy() { } publicvoid doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("GBK"); // 将客户端提交的数据设为GBK编码格式 // 继续处理客户端提交的数据,如果不写这条语句,Servlet引擎将不会处理所过滤的页面 chain.doFilter(request, response); } publicvoid init(FilterConfig filterConfig) throws ServletException { } }
Filter接口的doFilter方法是过滤器的核心方法。其中FilterChain类的doFilter方法允许继续处理客户端提交的数据。我们还可以使用这个方法来临时关闭Web站点的某个或全部的页面(根据过滤器的设置而定)。由于本书的数据库使用的是GBK编码格式,因此,需要使用ServletRequest的setCharacterEncoding方法将客户端提交的数据也设为GBK编码格式。
除了实现过滤器类,我们还需要在web.xml中的<web-app>节点加入如下的配置信息才能使过滤器生效:
在web.xml中配置过滤器
<filter> <filter-name>EncodingFilter</filter-name> <filter-class> filter.EncodingFilter </filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
在重新启动Tomcat后,重新输入一条带中文的产品信息,看看是否可以将中文保存在数据库中?