0x00 一些基础的概念
这一节讲一些基础的概念,这个概念可能和Servlet和Struts2无关,但是是后面内容的重要铺垫
对象的构成要素
public class Human { // 签名区
private String name; // 属性
public String printName() { // 方法
System.out.println(this.name);
return this.name
}
}
构成一个对象的基本要素:
- 签名(Signature):对象的核心语义概括
- 属性(Property):对象的内部特征和状态的描述
- 方法(Method):对象的行为特征的描述
对象的运作模式
属性对象模式
也称为JavaBean模式,作为数据存储和数据传输的载体
平常见到的PO(Persistent Object)持久化对象,BO(Business Object)业务对象,VO(Value Object)值对象,DTO(Data Transfer Object)数据传输对象都只不过是对基本的、运行在JavaBean模式下对象的有效扩展或增强,它们被用于不同的业务场景和编程层次,有些时候甚至是同一对象在不同层次上的不同名称
当一个对象运作在属性对象模式时,其本质是对象当JavaBean特性
JavaBean对象的产生主要是为了强调对象的内在特征和状态,同时构造一个数据存储和数据传输的载体
行为对象模式
public Return someMethod(Param param1, Param param2) {}
上面函数定义中
- 方法签名(Signature):行为动作的逻辑语义概括,就是上面的
someMethod
- 参数(Parameter):行为动作的逻辑请求输入,就是上面的
Param param1, Param param2
- 返回值(Return):行为动作的处理响应结果输出
方法的定义是一种触发式的逻辑定义,对象中的方法定义是进行请求响应的天然载体
属性-行为对象模式
同时有属性定义和方法定义的对象,也是最普遍的运行模式
框架的本质
if (str == null || str.length() == 0) {
// 逻辑代码
}
上面是一段判断输入是否为空或null的例子
我们把它换成如下
public abstract class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
}
if (StringUtils.isEmpty(string) {
// 这里添加逻辑
}
这样子封装可以带来两个好处
一是可读性:isEmpty比原来的代码更直观
二是当isEmpty这个函数定义不需要频繁变动时候的可扩展性:当需要判断列表为空等新功能时祝需要修改一次,否则需要在每个调用处修改
对于一些特定问题,经验丰富的程序员在经过无数次尝试后,总结出来的处理特定问题的特定方法,也就是某段逻辑的最佳实践,最终以JAR包形式蕴含在框架中
框架是一个JAR包的集合,当我们加载一个框架,就是加载了很多个JAR包到CLASSPATH,实际上是获得了JAR对JDK的功能扩展
所以框架是一组程序的集合,是解决某个领域的问题的一系列最佳实践,本质是对JDK的功能扩展
Web开发的基本模式
宏观上来说,Web开发模式中最重要的一条是分层开发模式,除非Web项目非常小
- 表示层(Presentation Layer):负责处理与洁面交互相关的功能
- 业务层(Business Layer):负责复杂的业务逻辑计算和判断
- 持久层(Persistent Layer):负责将业务逻辑数据进行持久化存储
常听的分析开发模式的最佳实践是MVC
- M(Model):数据模型
- V(View):视图展示
- C(Control):控制器
MVC中的数据流和控制流
我们常说程序就是数据结构+算法
那么MVC中的数据结构+算法是什么
- 数据流:描述程序运行过程中数据的流转方式及其行为状态
- 控制流:控制程序逻辑执行的先后顺序
在请求-响应过程中,数据流实际上表现为数据内容,其核心包括数据请求和数据响应;而控制流实际上表现为方法进行逻辑处理的过程,包含程序的执行方向
真正贯穿MVC框架并且将MVC的各个模块黏合在一起的是数据,数据作为黏合剂,构成了模块与模块间的互动载体,把MVC真正融合在一起
Model层实际上是一个动态元素,它作为数据载体流转于程序之间,并在不同的程序模块中表现出不同的行为状态,这就是形成数据流的本质
而整个流程之所以能运转良好,得益于一个核心元素的掌握,就是MVC核心控制器的Control
- 控制层负责请求数据的接收
- 控制层负责业务逻辑的处理
- 控制层负责响应数据的收集
- 控制层负责响应流程的控制
控制流实际上是数据流融入控制层之后形成的逻辑处理和程序跳转结果
那么View好像没有什么用,虽然是流程中的一部分,但是即不代表数据流和也不代表控制流,更像是数据流在控制流处理下输出的结果
但是View是页面和Java世界交互流转的部分,毕竟B/S构架下浏览器和Java后台是两个不同的程序,View这一层需要分离
HTTP的请求响应在Java程序中的实现模式
请求-响应是一种概念非常宽泛的人机交互模型,是人与计算机进行沟通的一种最基本的行为方式
在HTTP中请求响应的三要素
- 沟通协议:HTTP协议
- 请求内容:HTTP请求
- 响应内容:HTTP响应
Java对请求响应的实现模式
参数-返回值模式(Param-Return)模式
public Return someMethod(Param param1, Param param2) {}
上面函数定义中
- 方法签名(someMethod):请求-响应沟通协议
- 方法参数(Param param1, Param param2):请求-响应的请求内容
- 方法返回值(Return):请求-响应的响应内容
对象的方法定义与请求-响应模式的流程是相通的,从而使得对象的方法成为请求-响应模式在Java世界中的一种抽象
参数-参数(Param-Param)模式
以Servlet为例
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
- 方法签名(doGet):请求-响应沟通协议
- 方法参数(HttpServletRequest req):请求-响应的请求内容
- 方法返回值(HttpServletResponse resp):请求-响应的响应内容
POJO模式
public class UserController {
private String username;
private String password;
public String login() {
return "success";
}
}
如果说参数-返回值模式是Java世界中的一种直观抽象,那么POJO模式就是一种比较晦涩的请求-响应模式实现方式了
比如在上面这个类中,响应的处理流程,处理机制和处理结果,如POJO实例的内部属性的状态有关,甚至无法直接看出请求-响应的沟通协议、请求内容和响应内容,但是我们确知道它能实现请求-响应模型
各种模式之间的区别
+-------------+----------------+-------------+---_------------+
| | 参数-返回值模式 | 参数-参数模式 | POJO模式 |
+-------------+----------------+-------------+----------------+
| 请求内容 | 方法参数 | 方法参数 | 类的属性变量 |
+-------------+----------------+--------------+---------------+
| 请求处理载体 | 响应方法 | 响应方法 | 响应方法 |
+-------------+----------------+--------------+---------------+
| 响应内容 | 返回值 | 方法参数 | 类的属性变量 |
+-------------+----------------+--------------+---------------+
| 响应跳转处理 | 返回值或框架组件 | 方法参数 | 返回值或框架组建 |
+-------------+----------------+--------------+---------------+
好了,讲完这些我们来看看Servlet有什么不足,Struts2又是怎么改进的
0x01 Servlet在Web开发模式中的不足
Servlet实现MVC
Model: User.java
public class User {
private String username;
private String password;
public User() {}
// setter,getter方法
}
View: login.jsp
<form method="post" action="/login">
username: <input type="text" name="username"/>
password: <input type="text" name="password"/>
<input type="submit" name="submit"/>
</form>
Control: LoginServlet.java
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = new User();
user.setUsername(username);
user.setPassword(password);
try {
// 业务逻辑代码
UserService userService = new UserService();
userService.login(user);
req.getRequestDispatcher("/index.jsp")
} catch (Exception e) {
request.getRequestDispatcher("/fail.jsp")
}
}
}
除了MVC外,还需要建立JSP的form请求与Servlet类的响应关系,也就是维护web.xml
<servlet>
<servlet-name>loginServlet</servlet-name>
<servlet-class>LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>loginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
可以看到除了MVC,我们还需要一个URL Mapping(请求转化),也就是web.xml
Servlet实现的MVC的不足
仅依靠JSP和Servlet标准,我们就可以实现MVC,但是程序是动态演变的,程序的维护时间越长,就会开始遭遇困境,困境主要来自两个方面
- 程序自身的可读性和可维护性
- 出于业务扩展的需求,需要通过框架级别的功能增强来解决可扩展性困境
比如存在以下的问题
- 用web.xml的配置来维护URL Mapping问题,配置的重复操作会让web.xml变得越来越大而越来越难以维护,web.xml有URL Mapping外的配置,不容易建立URL表达式到Java世界类对象的规则匹配引擎
- HTTP的请求响应参数是”弱类型”,大多数用字符串展现,但是Java世界是”强类型”的,需要帮助我们解决在数据流转时候的数据转化,常用的方法是表达式引擎
- Web容器是一个多线程环境,针对每个HTTP请求,Web容器的线程池会分配一个特定的线程进行处理,如果保证数据访问和流转是线程安全的,常用的方法是ThreadLocal
- Controller作为MVC的核心控制器,如何最大程度上支持功能点扩展,上面Servlet中的代码没有类似生产线的概念,直来直去,没有抽象出常规HTTP请求的执行流程
- View层表现形式多样,HTML、JSON、redirect、forward等跳转的表现形式不同,输入的返回需要硬编码解决,需要提供一种透明的应对不同视图的表现形式,有效方式是对表现形式进行分类,封装后和上面的流水线结合
- MVC各层元素如何有机整合,数据结构和流程控制如何完成
0x02 Struts2如何改进
Struts2怎么实现MVC
Model: User.java
public class User {
private String name;
private String password;
public User() {}
// setter,getter方法
}
View: login.jsp
<form method="post" action="/login">
username: <input type="text" name="user.name"/>
password: <input type="text" name="user.password"/>
<input type="submit" name="submit"/>
</form>
Control: LoginServlet.java
public class UserAction implements Action {
private User user;
private String execute() {
// 可以直接在这里使用user对象,因为它已经被作为参数传入了
return "success";
}
// setter,getter
}
struts2中web.xml的配置
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
struts.xml的配置
<struts>
<package name="test" extends="struts-default">
<interceptors>
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"></interceptor>
</interceptors>
<action name="login" class="action.LoginAction" method="execute">
<result name="success">/index.jsp</result>
</action>
</package>
</struts>
URL Mapping的改进
用struts.xml中的Action配置改进了servlet和url-pattern在web.xml中的配置,通过Action的name和class和method的区别容易建立起url和Java世界Action类method方法之间联系
强弱类型转换的表达式引擎OGNL
View层的数据模型将遵循HTTP协议,它没有数据类型的概念,多为字符串
Controller层的数据模型遵循Java的语法和数据结构,所有数据载体在Java世界中可以表现为丰富的数据结构和数据类型,可以自定义喜欢的类,在类与类之间进行继承、嵌套,通常把这种模型叫做对象树,数据在传递时,将以对象树形式进行
数据在不同的MVC层次上,扮演的角色和表现形式不同,是由于HTTP协议与Java面向对象之间的不匹配造成的
为了处理这个数据流转的问题,比如Java世界用Hibernate或者MyBatis这样的持久层框架来处理Java对象与关系型数据库的匹配,在Struts2中,引入来OGNL表达式引擎来处理View层和Controller层的数据匹配关系
ThreadLocal保证数据访问的安全
Web容器默认采用单Servlet实例多线程的方式处理HTTP请求,这种处理方式能够减少新建Servlet实例的开销,从而缩短对HTTP请求的响应,但这样的处理方式会导致变量访问的线程安全问题
public class LoginServlet extends HttpServlet {
private int counter = 0;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(this + ":" + Thread.currentThread());
for (int i = 0; i < 5; i++) {
System.out.println("Counter = " + counter);
try {
Thread.sleep((long) Math.random() * 1000);
counter++
} catch () {}
}
}
}
并发运行时counter的值被三个线程修改,会导致非期望的输出
Struts2中使用ThreadLocal来处理这个问题,ThreadLocal和synchronized的区别是,ThreadLocal是用空间换时间,synchronized是用时间换空间
Thread.java
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
查看ThreadLocalMap的实现可以得知
- ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量,使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key进行存储
- 线程中ThreadLocalMap的变量的值是在ThreadLocal对象进行set或者get操作时创建的
- 创建ThreadLocalMap之前,会检查是否存在,不存在则创建,存在则使用
ThreadLocal从两个方面完成来数据访问隔离
- 纵向隔离:线程与线程之间的数据访问隔离,这一点由线程的数据结构保证,线程访问的是各自的ThreadLocalMap
- 横向隔离:同一个线程中,不同的ThreadLocal实例操作的对象之间相互隔离,由ThreadLocalMap存储时采用ThreadLoca实例作为key来保证
在一个HTTP请求-响应过程中,ThreadLocal的操作维持于整个Thread的生命周期,不管在Web开发模型的哪一个层次(表示层、业务层或者持久层),解决了各个层次变量共享的问题,可以对执行逻辑和执行数据进行有效解耦
ThreadLocal模式的实现关键在于创建一个任何地方都可以访问到的ThreadLocal实例,Struts2中用以下方式实现
- 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境
- 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)
Controller如何抽象出常规HTTP请求的生产线
我们之前说到,MVC的控制器有以下4个控制流程,这也是常规HTTP请求的生产线
- 控制层负责请求数据的接收
- 控制层负责业务逻辑的处理
- 控制层负责响应数据的收集
- 控制层负责响应流程的控制
在一个servlet中
以下代码是处理请求参数的接收
String name = request.getParameter("name");
Data age = new SimpleDateFormat("yyyy-MM-dd").parse(request.getParameter("age"));
User user = new User(name, age);
以下代码是负责业务逻辑的处理
userService.login(user)
以下代码是负责响应数据的收集
request.setAttribute("user", user);
以下代码是负责响应流程的控制
RequestDispatcher dispatcher = request.getRequestDispatcher("/index");
dispatcher.forward(request, response);
当请求-响应的接口有1000个,这样的四段式也就要重复1000遍,这里面的重中之重是处理业务逻辑这一步
控制层的核心指责是处理业务逻辑,而对于一个开发框架而言,控制层应该更加关注其核心职责,其他的辅助逻辑则由框架帮忙来完成
Struts2中,比如我们在上面举的Struts2的MVC实现的例子
- Action的execute处理页面逻辑,作为重中之重控制
- 请求参数接收和响应数据收集交给Action的属性定义和Interceptor配合完成
- 响应流程的控制通过result配置简化
View不同表现形式的封装与流水线的结合
Struts2中通过对Result的元素的抽取,隔离出Result的生成方式,可以根据View的不同表现形式定制Result
MVC各层的整合
我们上面说到真正贯穿MVC框架并且将MVC的各个模块黏合在一起的是数据,数据作为黏合剂,构成了模块与模块间的互动载体,把MVC真正融合在一起
明白了Struts2是如何处理数据的,我们就明白了如何整合MVC各层
我们对数据的关注点主要有两个:数据存储和数据传输,Model层是以”属性对象模式”进行建模的,Model扮演的是一个载体的角色
载体必须具备一定的数据结构,这里用三种数据结构进行比较
Map结构
比如ServletRequest使用Map作为数据载体
public Map getParameterMap();
Map用key-value作为数据存储结构非常方便,简单而有效
但是存在一些致命问题
- Map作为一个原始的数据结构,弱化了Java作为一个强类型语言的功能
- Map中的健值作为数据存储的依据,使得程序的可读性大大降低(因为它可以任意添加、修改和删除)
- Map进行数据交互无法实现必要的类型转化
FormBean结构
针对map结构的问题,很多Web框架提出了使用FormBean作为数据交互载体的方案,Struts1.X的FormBean如下
public class UserForm extends ActionForm {
private String userName;
private String password;
// 数据校验方法,主要用于数据格式层面,非空、类型判断等
public AccountErrors validate(ActionMapping mapping, HttpServletRequest request) {
return null;
}
// setter getter
}
除此之外,需要在struts-config.xml中声明一下这个FormBean
<form-beans>
<form-bean name="UserForm" type="example.UserForm" />
</form-beans>
控制层的代码如下
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) {
UserForm userForm = (UserForm) form;
String userName = userForm.getUserName();
String userPwd = userForm.getUserPwd();
return mapping.findForward(forward);
}
FormBean也有很多问题
- FormBean被强制和框架的功能耦合在一起:FormBean从ActionForm中继承了数据校验机制,FormBean和Struts1.X的标签耦合在一起,使得程序失去灵活性和扩展性
- FormBean在参数传递非常复杂的情况下几乎无法工作,复杂时一个页面的多个元素需要被归并到多个Java实体中,在FormBean一个映射中大量关联
POJO结构
public class User {
private String name;
private String password;
// getter,setter
}
public class UserAction implements Action {
private User user;
public String execute() {
// 这里使用user对象
return "success";
}
// getter,setter
}
<forms method="post" action="/login">
username: <input type="text" name="user.name"/>
password: <input type="text" name="user.password"/>
<input type="submit" name="submit"/>
</form>
- 作为JavaBean,POJO是一个具备语义的强类型,不仅能够享受编译器的类型检查,还能够自由定义我们所需要表达的定义
- POJO不依赖于任何框架,可以在程序的任何一个层次(如业务逻辑层,甚至是持久层)被复用
- POJO突破了FormBean对于页面元素唯一对应的限制,我们可以将一个页面中的元素自由映射到多个POJO中去
可以看到上面的POJO例子,User无需在继承任何框架相关的类,Action引用User作为参数时,直接将其作为Action的局部变量使用即可,也可以增加多个局部变量来映射不同的页面元素
可以发现,POJO结构的处理也就是我们上面提到的HTTP的请求响应在Java程序中的实现模式的POJO模型,这也就是为什么说明白了Struts2如何处理数据,就知道如何整合MVC各层的