服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|

服务器之家 - 编程语言 - JAVA教程 - 详解Java的Struts2框架的结构及其数据转移方式

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

2020-03-21 13:33cdai JAVA教程

这篇文章主要介绍了详解Java的Struts2框架的结构及其数据转移方式,Struts框架是Java的SSH三大web开发框架之一,需要的朋友可以参考下

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属性名来定义表单的参数名了。

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

延伸 · 阅读

精彩推荐