详解Java的Struts2框架的结构及其数据转移方式

时间:2022-09-02 22:22:03

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模式。

详解Java的Struts2框架的结构及其数据转移方式

对应之前HelloWorld的例子来看,控制器C(FilterDispatcher)也就是我们在web.xml中声明的
Struts2核心类。而模型M就是我们的NewsAction动作类。而视图V自然就是news.jsp了。模型
的概念似乎有些模糊,什么是模型呢?其实这个听起来很名词的概念在Struts2中既包含了静态
从Web前端传来的业务数据,也包含了业务逻辑的实现。

有人可能会说这种架构没什么新意嘛,MVC框架有很多,这跟其他框架有什么区别呢?让我们
站在低一级别的抽象层次上解剖Struts2,看看它有什么与众不同。

详解Java的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框架的。

详解Java的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属性

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<%@ 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>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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对象

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<%@ 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>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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对象

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<%@ 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>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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属性名来定义表单的参数名了。

三种方式具体采用哪种不能一概而论,还是看项目的具体需求再自己定吧!