详解Java的Struts框架以及相关的MVC设计理念
struts简介
Struts是Apache软件基金会(ASF)赞助的一个开源项目。它最初是jakarta项目中的一个子项目,并在2004年3月成为ASF的顶级项目。它通过采用JavaServlet/JSP技术,实现了基于JavaEEWeb应用的MVC设计模式的应用框架,是MVC经典设计模式中的一个经典产品。
Struts发展历史
Struts是作为ApacheJakarta项目的组成部分,项目的创立者希望通过对该项目的研究,改进和提高JavaServerPages、servlet、标签库以及面向对象的技术水准。
Struts这个名字的来源于在建筑和旧式飞机中使用的支持金属架。之所以这个框架叫做“struts”,是为了提醒我们记住那些支撑我们房屋,建筑,桥梁,甚至我们踩高跷的基础支撑。这也是解释struts在开发web应用程序中所扮演的角色的精彩描述。
Struts的含义是”支柱,枝干”,它的目的是为了减少程序开发的时间,项目的创建者认为JSP,servlet的存在虽然可以帮助用户解决大部分问题,但是由于它们的编码对项目的开发带来了许多的不方便,可重用性也差,所以struts应运而生,帮助用户在最短的时间内解决这些问题。Struts框架提供如下服务:
(1)作为控制器的Servlet。
(2)提供大量的标签库。
(3)提供了用于国际化的框架,利用不同的配置文件,可以帮助用户选择合适自己的语言。
(4)提供了JDBC的实现,来定义数据源和数据库连接池。
(5)XML语法分析工具。
(6)文件下载机制。
Struts原理
Struts是对JSPModel2设计标准的一种实现,下面分别从模型(Model)、视图(view)和控制器3个部分介绍Struts的体系结构和工作原理。调用流程如下所示。
(1)视图(view)
在Struts中,视图层包含两个部分,JSP页面和ActionForm。
ActionForm封装了用户提交的表单信息,其实ActonForm本质上就是JavaBean,这些JavaBean中没有业务逻辑,只提供了所有属性的getter和setter方法,这些属性和用户表单中的输入项是一一对应的。在Struts中就是通过ActionForm把用户表单信息提交给控制器。
JSP页面是经典MVC中主要的视图组件,主要是信息显示和控制器处理结果显示的功能。
除了以上,struts还提供了一个强大的struts标签库,来帮助用户解决显示逻辑,并且利用ActonForm组件将信息传递到控制层。
(2)控制器(Controller)
在控制层,struts提供了一个控制器组件ActionServlet,它继承自HttpServlet,并重载了HttpServlet的doGet(),doPost()方法,可以接受HTTP的响应,并进行转发,同时还提供了使用XML进行转发Mapping(映射)的功能。
(3)模型(Model)
模型表示状态和业务逻辑的处理,在一般的web应用程序中,用JavaBean或者EJB来实现系统的业务逻辑。在Struts中,struts提供Action对象,来管理业务逻辑的调用,帮助用户分离业务逻辑,也就是说struts本身不实现业务逻辑,但可以调用已完成的业务逻辑。
Struts工作流程
Struts工作流程如下所示。
ActionServlet是struts中核心的控制器,所有的用户请求都必须通过ActionServlet的处理,而struts-config.xml是struts中核心的配置文件,在这个文件中配置了用户请求URL和控制器Action的映射关系,ActionServlet通过这个配置文件把用户的请求发送到对应的控制器中。
在struts web应用程序中,当web应用程序启动的时候,就会初始化ActionServlet在初始化ActionServlet的时候会加载struts-config.xml配置文件,在加载成功后会把这些URL和控制器映射关系存放在ActionMapping对象或者其他对象中。当ActionServlet接收到用户请求的时候,就会按照下面的流程对用户请求进行处理。
(1)ActionServlet接收到用户的请求后,会根据请求URL寻找匹配的ActionMapping对象,如果匹配失败,说明用户请求的URL路径信息有误,所以返回请求路径无效的信息,当找到匹配的ActionMapping的时候,进入到下一步。
(2)当ActionServlet找到匹配的ActionMapping对象的时候,会根据ActionMapping中的映射信息判断对应的ActionForm对象是否存在,如果不存在对应的ActionForm对象就创建一个新的ActionForm对应,并把用户提交的表单信息保存到这个ActionForm对象中。
(3)在struts-config.xml中这个配置文件,可以配置表单是否需要验证,如果需要验证,就调用ActionForm中的validate()方法对用户输入的表单进行验证。
(4)如果ActionForm的validate()方法返回了ActionErrors对象,则表明验证失败,ActionServlet把这个页面返回到用户输入的界面,提示用户重新输入。如果方法的返回值为null,就表明验证已经通过,可以进入下一步处理。
(5)ActionServlet可以根据ActionMapping对象查找用户请求转发给哪个控制器Action,如果对应的Action对象不存在,就创建这个对象,并调用这个Action的excute()方法。
(6)业务逻辑控制器Action的execute()方法就会返回一个ActionForward对象,ActionServlet把控制器处理的结果转发到ActionForward对象指定的JSP页面。
(7)ActionForward对象指定的JSP页面根据返回的处理结果,用合适形式把服务器处理的结果展示给用户,到这里为止,一个客户请求的整个过程完毕。
以上初步struts框架进行了介绍,和对原理进行了简单的分析。至于struts是如何实现MVC的,ActionServlet属于Controller部分,Action和ActionForm属于Model层,还是Action属于Controller层,不同的人对struts有不同的理解。接下来真正的运用到实践中,在实践中深刻去体会,原理固然重要,重要的是运用,是能驾驭和使用这个框架。就像学习开车一样,不是一蹴而就的。
MVC向struts MVC框架演变过程
版本一 基本的MVC
首先是创建一个jsp索引页面index.jsp,index.jsp创建一个表单。
如果我们完成添加用户的模块,我们提交表单的时候要把这个表单提交给一个servlet,于是我们在src自己的包中建立自己的servlet继承HttpServlet,TestServlet同时覆盖父类的doGet和doPost方法。
提交的的表单信息,我们可以在TestServlet中通过request.getParameter(“username”);取出来。之后把从表单中的数据提交到数据库,我们的servlet作为控制器没有添加到数据库的职责,于是我们把访问数据库的业务逻辑放到UserManager.java类中。在这个类中实现添加用户的任务。
package com.bjpowernode.servlet; public class UserManager { public void add(String username) { System.out.println("UserManager.add()-->>"+ username); } }
在TestServlet中调用UserManager中的方法。同时让页面转向到添加成功的页面add_success.jsp。
TestServlet代码以及在web.xml中配置TestServlet.
<servlet> <servlet-name>TestServlet</servlet-name> <servlet-class>com.bjpowernode.servlet.TestServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>TestServlet</servlet-name> <url-pattern>/servlet/TestServlet</url-pattern> </servlet-mapping>
Testservlet代码如下所示:
package com.bjpowernode.servlet; import java.io.IOException; import java.util.List; import javax.servlet.ServletException; importjavax.servlet.http.HttpServlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class TestServlet extendsHttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter(“username”); UserManager userManager = new UserManager(); userManager.add(username); request.getRequestDispatcher(“/add_success.jsp”).forward(request,response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request,response); } }
索引页面index.jsp代码,通过servlet/TestServlet来转到TestServlet。
<form action="servlet/queryUser.action" method="post"> 姓名:<input type="text" name="username"><br> <input type="submit" value="提交"><br> </form>
这样采用MVC完成了用户的添加任务。
版本二 通过判断标识变量
当我们不仅要完成用户的添加而是要完成用户的增删改查的时候我们可以在servlet中进行判断,是添加还是删除等。并且在传递字符串的时候要加上一个标识表示相应的操作。这样在servlet中会有很多的if和else语句。
版本三 servlet模式匹配 截取字符串判断
我们可以在配置servlet的时候配置成为匹配模式<url-pattern>*.action</url-pattern>任何的*.action的都会转到到TestServlet,这样我们可以截取URL来判断转到哪个页面。但在TestServlet中仍然有很多的ifelse语句进行判断。这样我们的TestServlet代码如下所示。
import java.io.IOException; import java.util.List; importjavax.servlet.ServletException; importjavax.servlet.http.HttpServlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class TestServlet extendsHttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //截取url. String requestURI = request.getRequestURI(); //System.out.println("requestURI="+ requestURI); //截取http://localhost:8080/test_servlet/servlet/addUser.action test_servlet后面的东西。 String path = requestURI.substring(requestURI.indexOf("/", 1),requestURI.indexOf(".")); //截取得到虚目录。/servlet/addUser System.out.println("path="+ path); String username = request.getParameter("username"); UserManager userManager = new UserManager(); String forward = ""; //判断截取的path和哪个要加载的页面相等. if("/servlet/delUser".equals(path)) { userManager.del(username); forward = "del_success.jsp"; //request.getRequestDispatcher("/del_success.jsp").forward(request,response); }else if("/servlet/addUser".equals(path)) { userManager.add(username); forward= "add_success.jsp"; //request.getRequestDispatcher("/add_success.jsp").forward(request,response); }else if("/servlet/modifyUser".equals(path)) { userManager.modify(username); forward= "modify_success.jsp"; //request.getRequestDispatcher("/modify_success.jsp").forward(request,response); }else if("/servlet/queryUser".equals(path)) { List userList = userManager.query(username); request.setAttribute("userList",userList); forward= "query_success.jsp"; //request.getRequestDispatcher("/query_success.jsp").forward(request,response); }else { throw new RuntimeException("请求失败!"); } request.getRequestDispatcher(forward).forward(request,response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throwsServletException, IOException { doGet(request,response); } }
UserManager代码如下所示:
package com.bjpowernode.servlet; import java.util.ArrayList; import java.util.List; public class UserManager { public void add(String username) { System.out.println("UserManager.add()-->>"+ username); } public void del(String username) { System.out.println("UserManager.del()-->>"+ username); } public void modify(String username) { System.out.println("UserManager.modify()-->>"+ username); } public List query(String username) { System.out.println("UserManager.query()-->>"+ username); List userList = new ArrayList(); userList.add("a"); userList.add("b"); userList.add("c"); return userList; } }
同时建立查询成功,删除成功,修改成功jsp页面。
这样在index.jsp页面中通过表单的action属性来转到相应的页面。
<body> <form action="servlet/queryUser.action" method="post"> 姓名:<input type="text" name="username"><br> <input type="submit" value="提交"><br> </form> </body>
版本三的缺点是if太多,不稳定,当我们添加或者删除一个if的时候还需要重新编译源程序。这样无法适应变化的需求。所以我们在此基础上进行改进就需要去掉if语句,可以把改变的部分分离出来,变成可配置的,这样就变得灵活的,需要改动jsp的文件名,在配置文件中配置一下就可以了。
版本四 if else抽象出接口和类+配置文件
首先我们把转到的地址字符串放到一个字符串变量中,这样TestServlet中的doGet方法,代码如下。
String username = request.getParameter("username"); UserManager userManager = new UserManager(); String forward = ""; //判断截取的path和哪个要加载的页面相等. if("/servlet/delUser".equals(path)) { userManager.del(username); forward= "del_success.jsp"; }else if("/servlet/addUser".equals(path)) { userManager.add(username); forward= "add_success.jsp"; }elseif("/servlet/modifyUser".equals(path)) { userManager.modify(username); forward= "modify_success.jsp"; }else if("/servlet/queryUser".equals(path)) { List userList = userManager.query(username); request.setAttribute("userList",userList); forward= "query_success.jsp"; }else { thrownew RuntimeException("请求失败!"); } request.getRequestDispatcher(forward).forward(request,response);
统一完成了转向。当if语句是满足某种条件,条件不同的时候转到不同的页面,添加有添加的逻辑,修改有修改的逻辑,这样我们可以抽象出接口。对添加的action操作添加,对删除的action做出删除任务,对修改的action做出修改。把每一个if语句中的变成为类去实现,抽象出一个Action接口,接口中方法execute()方法,返回转向路径字符串。类图如下所示。
共同的Action接口,不同的实现。
Action接口代码。
package com.bjpowernode.servlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public interface Action { public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception; }
AddUserAction代码。
package com.bjpowernode.servlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class AddUserActionimplements Action { @Override public String execute(HttpServletRequest request,HttpServletResponse response) throws Exception { String username = request.getParameter("username"); //Stringsex = request.getParameter("sex"); //........... //调用业务逻辑. UserManager userManager = new UserManager(); userManager.add(username); return"/add_success.jsp"; } }
DelUserAction代码。
package com.bjpowernode.servlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class DelUserActionimplements Action { @Override public String execute(HttpServletRequest request,HttpServletResponse response) throws Exception { String username = request.getParameter("username"); //String sex = request.getParameter("sex"); //........... //调用业务逻辑. UserManager userManager = new UserManager(); try { userManager.del(username); }catch(Exceptione) { return"del_error.jsp"; } return"/del_success.jsp"; } }
ModifyUserAction代码。
package com.bjpowernode.servlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class ModifyUserActionimplements Action { @Override public String execute(HttpServletRequest request,HttpServletResponse response) throwsException { String username = request.getParameter("username"); //String sex = request.getParameter("userId"); //...........等其他... //调用业务逻辑. UserManager userManager = new UserManager(); userManager.modify(username); return"/modify_success.jsp"; } }
QueryUserAction代码。
package com.bjpowernode.servlet; import java.util.List; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; public class QueryUserActionimplements Action { @Override public String execute(HttpServletRequest request,HttpServletResponse response) throwsException { String username = request.getParameter("username"); //Stringsex = request.getParameter("userId"); //其他查询条件. //...........等其他... //调用业务逻辑. UserManager userManager = new UserManager(); List userList = userManager.query(username); request.setAttribute("userList",userList); return"/query_success.jsp";//转向路径都可以通过配置文件读取。 } }
这样使用多态方式调用不同的Action,转向代码如下所示。
//用多态的方式. Action action = null; if("/servlet/delUser".equals(path)) { action= new DelUserAction(); }else if("/servlet/addUser".equals(path)) { action= new AddUserAction(); }else if("/servlet/modifyUser".equals(path)) { action= new ModifyUserAction(); }else if("/servlet/queryUser".equals(path)) { action= new QueryUserAction(); }else { throw new RuntimeException("请求失败!"); } //取得action后传递过去。动态调用ACtion中的execute方法。 String forward = null; try{ forward= action.execute(request,response); }catch (Exception e) { e.printStackTrace(); } //根据路径完成转向。 request.getRequestDispatcher(forward).forward(request, response);
上述调用不同的action,我们可以把不同的请求和对应的action配置到自己的xml文件中。配置哪个请求对应哪个Action。
<action-config> <action path="/servlet/delUser" type="com.bjpowernode.servlet.DelUserAction"> <forward name="success">/del_success.jsp</forward> <forward name="error">/del_error.jsp</forward> </action> <action path="/servlet/addUser" type="com.bjpowernode.servlet.AddUserAction"> <forward name="success">/add_success.jsp</forward> <forward name="error">/add_error.jsp</forward> </action> <action path="/servlet/modifyUser" type="com.bjpowernode.servlet.ModifyUserAction"> <forward name="success">/modify_success.jsp</forward> <forward name="error">/modify_error.jsp</forward> </action> <action path="/servlet/queryUser" type="com.bjpowernode.servlet.QueryUserAction"> <forward name="success">/query_success.jsp</forward> <forward name="error">/query_error.jsp</forward> </action> </action-config>
这样我们就可以读取配置文件来进行相应的操作。每个action对应着一些信息,它的class,执行成功以及执行失败的配置。我们在读取配置文件的时候可以把这些放到Map对象中。代码如下所示。
ActionMapping { private String path; private String type; Map forwardMap; } forwardMap { key="success"; value="/del_success.jsp" key="error"; value="/del_error.jsp"; } Map map = new HashMap(); map.put("/servlet/delUser",); map.put("/servlet/addUser",); map.put("/servlet/modifyUser",); map.put("/servlet/queryUser",); //如果是删除的ActionMapping存储如下: actionMapping { path="/servlet/delUser"; type="com.bjpowernode.servlet.DelUserAction"; forwardMap { key="success",value="/del_success.jsp"; key="error",value="/del_error.jsp"; } } String path ="/servlet/delUser"; //根据截取的URL请求,到Map中取得本次请求对应的Action。 ActionMapping actionMapping =(ActionMappint)map.get(path); // 取得本次请求对应的Action类的完成路径。 String type =actionMappint.getType();//com.bjpowernode.servlet.DelUserAction //通过反射动态实例化Action Aciton action =(Action)class.forName(type).newInstance(); //取得action后传递过去。动态调用ACtion中的execute方法。 String forward =action.execute(request,response); //根据路径完成转向。 request.getRequestDispatcher(forward).forward(request,response);
我们用时序图来描述上述调用过程(如图)。
Struts就是采用上述思路。Struts框架是把上图中前端控制器servlet进行了封装,我们只要继承struts的Action去调用业务逻辑和转向。读取配置文件的工作也是封装到了struts框架中。前篇讲解了struts框架的应用实例,我们可以进行对比。