详解Java的Struts2框架的结构及其数据转移方式
struts2的结构
1.为什么要使用框架?
(1)框架自动完成了很多琐屑的任务
对于struts2来说,它帮助我们方便地完成了数据类型转换、数据验证、国际化等等
web开发中常见的任务。还有spring中大量使用的template模式,都是在让我们的开发
过程更加自动化、智能化。使用框架就是避免重新发明*,重新复制这些模板代码。
框架让我们将精力更多地放在更高级别的问题上,而不是常见工作流和基础任务上。
(2)使用框架就是优雅地继承了框架背后的架构
框架背后的架构通常定义了一系列的工作流程,我们要做的就是将特定应用的代码
依附到这套流程上,这样就可以享受到框架带来的种种好处了。有些时候我们也可以
反抗框架的架构规则,但框架通常以一种很难被拒绝的方式提供它的架构。如此简单
就可以优雅地继承一个优秀的架构,而且是免费的,何乐而不为呢?
(3)使用框架更容易找到训练有素的人
我之前所在公司整个项目几乎都没有用过什么框架,从service服务的查找(类似jndi)
到日志打印(类似log4j),再到数据库连接池(类似dbcp),全都是内部人员自己
实现的。一来是因为项目比较老,当时可能还没有什么开源框架可供使用,二来也是因为
公司保守的策略,担心使用不稳定的开源框架可能会给项目带来风险。这在当时的环境下
也许是没错的,公司高层自然会从更大的视角来考虑整个项目。
但是当项目逐渐庞大起来,同时世界上优秀的开源框架越来越多时,如果不能及时重构
并引入一些成熟的开源框架,最后的结果可能就是新招来的开发人员必须从头开始学习
这个复杂的系统(都是内部系统,网上也没有文档帮助),还要小心内部框架的种种bug,
成本真是太高了。
(4)内部框架跟不上行业的发展
前面说到了内部框架的bug。对于开源框架,可能会有框架创始者团队、大批的开源爱好者、
开源社区来支持。人民的力量是无穷的,bug的修复速度可想而知,这点从最近开源后的
textmate的bug修复进程就可以看出了。很多搁置了很久的bug在开源后被爱好者们迅速
解决,而内部框架呢?在当初开发他的人员离开公司后,在没有重大bug时甚至都不会有人
去读他的源代码吧,差距可见一斑!
(5)当然使用框架也不是一本万利的事情
前面也提到过,使用不成熟的框架是有风险的,对于一个不是那么激进的项目还是保守为好。
(除非这是一群*没拘束的技术*分子,可以自行决定使用什么框架,那真是幸福的事)
就像我以前用过的java的ha高可用性服务sequioa一样,这个框架最终不再被开发公司提供支持
了,这时风险就更大了。
此外,使用一些不常见的框架时还要注意框架源码的license协议,不要在项目中随意引用、
修改框架的源码以免引起不必要的法律纠纷。
2.struts2背后的架构
既然前面已经分析了框架的这么多好处,那我们自然会开始学习使用struts2了。但使用struts2
会继承什么样的优雅架构呢?其实从较高的抽象层次上看,它依然是我们熟悉的mvc模式。
对应之前helloworld的例子来看,控制器c(filterdispatcher)也就是我们在web.xml中声明的
struts2核心类。而模型m就是我们的newsaction动作类。而视图v自然就是news.jsp了。模型
的概念似乎有些模糊,什么是模型呢?其实这个听起来很名词的概念在struts2中既包含了静态
从web前端传来的业务数据,也包含了业务逻辑的实现。
有人可能会说这种架构没什么新意嘛,mvc框架有很多,这跟其他框架有什么区别呢?让我们
站在低一级别的抽象层次上解剖struts2,看看它有什么与众不同。
乍看十分复杂,如果只从用户角度来看,在开发时我们只需要实现黄色的部分,也就是我们
helloworld实例中的struts.xml,newsaction和news.jsp。这就是我们要做的全部,就如前面
说的,只需要做很少的事情,我们就成为了这个优秀架构的一部分。
现在来看其他部分。filterdispatcher就是我们配置在web.xml中的servlet过滤器,这是struts2
的入口,所有struts2的web应用都要这样配置。接下来蓝色和绿色的部分就是struts2的核心
了,可以说这些类都是struts2的开发人员精心设计架构的。
(1)客户端发送请求,j2ee容器解析http包,将其封装成httpservletrequest。
(2)filterdispatcher拦截到这个请求,并根据请求路径到actionmapper中查询决定调用哪个action。
(3)根据actionmapper的返回结果,filterdispatcher委托actionproxy去struts.xml中找到这个action。
(4)actionproxy创建一个actioninvocation,开始对interceptor和action进行递归调用。
(5)各个interceptor完成各自任务
(6)真正对action的调用,返回结果路径
(7)result对象将返回数据输出到流中
(8)返回httpservletresponse给j2ee容器,容器发送http包到客户端。
这就是struts2的执行流程,核心对象是actioninvocation和interceptor,以及还未介绍的actioncontext。
actioninvocation是整个流程的总调度,它跟spring aop中的invocation对象很像。而interceptor有很多
都是struts2自带的,最重要的是保存请求参数,并将前台的数据传递到action的成员变量上。
而actioncontext就是保存这些数据的全局上下文对象,最重要的是用来保存action实例的valuestack。
所谓全局是指actioncontext可以在action以及result中访问,其实它是threadlocal类型。每个请求线程
都会有自己的action和actioncontext实例。
可以说学习struts2主要就是学习:
(1)让interceptor和action配合完成任务。
(2)将前台数据保存到action中。
(3)result通过valuestack从action中得到返回数据。
3.struts2与struts1的不同点
从上面的执行流程已经可以看出struts1和2的巨大区别。
(1)actionform哪去了?action还是那个action吗?
最明显的就是我们在整个流程中都看不到actionform对象了,而且action虽然还是叫这个名字,但是
看起来已经跟struts1中的action完全不同了。
首先actionform被抛弃了,从前台传来的数据已经可以保存到任意pojo了。先存到actionform再复制
到dto对象的日子已经是过去了。第二,这个pojo其实是action对象中的一个成员变量。这在struts1
中所有请求共享一个action实例时是不可能的,现在struts2会为每个请求都创建一个action实例,所以
这样做是行得通的。第三,虽然这样可行,可是看起来好像action作为mvc中的模型m既保存数据,又
包含了业务逻辑,这是不是不良的设计啊?其实仔细想想,这样的设计很方便,我们已经得到了数据,
直接就可以去操作service层了。action的职责看似多了,其实并不多。
(2)前端servlet怎么变成了filter?
我们知道struts1和spring mvc都是通过前端servlet来作为入口的,为什么struts2要用servlet的过滤器呢?
因为struts2是基于webwork核心的,与struts1已经完全不同了。webwork可以说降低了应用程序与j2ee
api的耦合,比如将actionservlet改为servlet的filter,再比如对httpservletrequest/response的直接访问,
又如任何pojo都能担任actionform的角色,任何类不用实现action接口就可以作为action使用等等,
因此struts2也继承了这种优秀的非侵入式设计。
这点与spring的设计思想有些相像。比如那些ware接口,不关心的bean完全不需要实现,尽量降低应用
程序代码与框架的耦合。侵入性的确是框架设计时要考虑的一个重要因素。
(3)filter、action、result间的粘合剂ognl
下图可以清晰明了地展示出ognl是如何融入struts2框架的。
在输入页面inputform.html和返回页面resultpage.jsp使用struts2标签中访问action中的数据是如此方便,
ognl使访问valuestack中保存的action的属性就像访问valuestack自己的属性一样方便。
对ognl的大量使用是struts2的一大特色。包括前台标签传值到action,result从action中取值等都会大量
用到ognl。而ognl中大量用到了反射,我想也许这是struts2性能不如struts1的一个原因吧。毕竟获得了
灵活而低耦合的架构的同时是要付出一定代价的。
(4)interceptor的强是无敌的强
struts2中另一个强大的特性就是interceptor拦截器了。struts2内建了大量的拦截器,拦截器使大量代码可以
重复使用,自动化了之前我们所说的琐屑的任务,从而使struts2达到了高水平的关注分离。这真是aop思想
在框架中应用的典范!
struts2三种数据转移方式
struts2提供了javabean属性,javabean对象,modeldriven对象三种方式来保存http请求中的参数。下面通过一个最常见的
登录的例子来看下这三种数据转移方式。页面代码很简单,提交表单中包含有用户名和密码,在action中得到这两个参数从而
验证用户是否登录成功。
一、javabean属性
<%@ page contenttype="text/html;charset=utf-8" %> <html> <head></head> <body> <h1>登录页</h1> <form action="/cdai/login" method="post"> <div> <label for="username">名称:</label> <input id="username" name="username" type="textfield"/> </div> <div> <label for="password">密码:</label> <input id="password" name="password" type="password"/> </div> <div> <label for="rememberme"> <input id="rememberme" name="rememberme" type="checkbox"/> 记住我 </label> <input type="submit" value="登录"></input> </div> </form> </body> </html>
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.loginrequest; import com.cdai.web.ssh.service.userservice; import com.opensymphony.xwork2.action; import com.opensymphony.xwork2.modeldriven; public class loginaction implements action { private string username; private string password; private userservice userservice; @override public string execute() { system.out.println("login action - " + request); return success; } public string getusername() { return request; } public void setusername(string username) { this.username = username; } public string getpassword() { return request; } public void setpassword(string password) { this.password = password; } }
这种方式比较简明,直接将表单中的参数保存到action中的属性中。action在验证时可能还需要将用户名和密码再封装成dto的
形式传给service层进行验证。所以为什么不更进一步,直接将用户名和密码保存到dto中。
二、javabean对象
<%@ page contenttype="text/html;charset=utf-8" %> <html> <head></head> <body> <h1>登录页</h1> <form action="/cdai/login" method="post"> <div> <label for="username">名称:</label> <input id="username" name="request.username" type="textfield"/> </div> <div> <label for="password">密码:</label> <input id="password" name="request.password" type="password"/> </div> <div> <label for="rememberme"> <input id="rememberme" name="rememberme" type="checkbox"/> 记住我 </label> <input type="submit" value="登录"></input> </div> </form> </body> </html>
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.loginrequest; import com.cdai.web.ssh.service.userservice; import com.opensymphony.xwork2.action; import com.opensymphony.xwork2.modeldriven; public class loginaction implements action { private loginrequest request; private userservice userservice; @override public string execute() { system.out.println("login action - " + request); return success; } public loginrequest getrequest() { return request; } public void setrequest(loginrequest request) { this.request = request; } }
这样就可以很方便地直接调用service层了。但是有一个小缺点就是这样加深了页面参数名的深度,只有为参数名加上request
前缀(action中的属性名)才能使struts2通过ognl将表单中的参数正确保存到request对象中。
三、modeldriven对象
<%@ page contenttype="text/html;charset=utf-8" %> <html> <head></head> <body> <h1>登录页</h1> <form action="/cdai/login" method="post"> <div> <label for="username">名称:</label> <input id="username" name="username" type="textfield"/> </div> <div> <label for="password">密码:</label> <input id="password" name="password" type="password"/> </div> <div> <label for="rememberme"> <input id="rememberme" name="rememberme" type="checkbox"/> 记住我 </label> <input type="submit" value="登录"></input> </div> </form> </body> </html>
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.loginrequest; import com.cdai.web.ssh.service.userservice; import com.opensymphony.xwork2.action; import com.opensymphony.xwork2.modeldriven; public class loginaction implements action, modeldriven<loginrequest> { private loginrequest request = new loginrequest(); private userservice userservice; @override public string execute() { system.out.println("login action - " + request); return success; } @override public loginrequest getmodel() { return request; } }
这种方式要多实现一个modeldriven接口,将modeldriven提供的对象也保存到valuestack上,从而使前台页面可以直接通过
username和password属性名来定义表单的参数名了。
三种方式具体采用哪种不能一概而论,还是看项目的具体需求再自己定吧!