You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

07 | 解耦是永恒的主题MVC框架的发展

你好,我是四火。

欢迎进入第二章,本章我们将围绕 MVC 这个老而弥坚的架构模式展开方方面面的介绍,对于基于 Web 的全栈工程师来说,它是我们知识森林中心最茂密的一片区域,请继续打起精神,积极学习和思考。

无论是在 Web 全栈还是整个软件工程领域有很多东西在本质上是相通的。比如我们在前一章提到的“权衡”trade-off我们后面还会反复提到。MVC 作为贯穿本章的主题,今天我们就通过它来引出另一个关键词——解耦。

JSP 和 Servlet

在我们谈 MVC 之前先来讲一对好朋友JSP 和 Servlet。说它们是好朋友是因为它们经常一起出现而事实上它们还有更为紧密的联系。

1. 概念介绍

如果你有使用 Java 作为主要语言开发网站的经历那么你一定听过别人谈论JSP和Servlet。其中Servlet 指的是服务端的一种 Java 写的组件,它可以接收和处理来自浏览器的请求,并生成结果数据,通常它会是 HTML、JSON 等常见格式,写入 HTTP 响应,返回给用户。

至于 JSP它的全称叫做 Java Server Pages它允许静态的 HTML 页面插入一些类似于“<% %>”这样的标记scriptlet而在这样的标记中还能以表达式或代码片段的方式嵌入一些 Java 代码,在 Web 容器响应 HTTP 请求时,这些标记里的 Java 代码会得到执行,这些标记也会被替换成代码实际执行的结果,嵌入页面中一并返回。这样一来,原本静态的页面,就能动态执行代码,并将执行结果写入页面了。

  • 第一次运行时系统会执行编译过程并且这个过程只会执行一次JSP 会处理而生成 Servlet 的 Java 代码接着代码会被编译成字节码class文件在 Java 虚拟机上运行。
  • 之后每次就只需要执行运行过程了Servlet能够接受 HTTP 请求,并返回 HTML 文本,最终以 HTTP 响应的方式返回浏览器。

这个过程大致可以这样描述:

编译过程JSP页面 → Java文件Servlet→ class文件Servlet
运行过程HTTP请求 + class文件Servlet→ HTML文本

2. 动手验证

为了更好地理解这个过程,让我们来实际动手操作一遍。

首先,你需要安装两样东西,一样是 JDKJava Development Kit8是Java的软件开发包另一样是 Apache Tomcat 9它是一款Web容器也是一款Servlet容器因此无论是静态的 HTML 页面,还是动态的 Servlet、JSP都可以部署在上面运行。

你可以使用安装包安装,也可以使用包管理工具安装(比如 Mac 下使用 Homebrew 安装)。如果你的电脑上已经安装了,只是版本号不同,也是没有问题的。

安装完成以后,打开一个新的命令行窗口,执行一下 java --version 命令,你应该能看到类似以下信息:

java -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)

这里显示了JREJava Runtime EnvironmentJava 运行时环境)的版本号,以及虚拟机的类型和版本号。

同样地执行catalina version你也能看到Tomcat重要的环境信息

catalina version
Using CATALINA_BASE: ...
Using CATALINA_HOME: ...
Using CATALINA_TMPDIR: ...
(以下省略其它的环境变量,以及服务器、操作系统和 Java 虚拟机的版本信息)

其中CATALINA_HOME 是 Tomcat 的“家”目录,就是它安装的位置,我们在下面要使用到它。

现在启动Tomcat

catalina run

在浏览器中访问 http://localhost:8080/你应该能看到Tomcat的主页面

接着,我们在 ${CATALINA_HOME}/webapps/ROOT 下建立文件 hello_world.jsp写入

Hello world! Time: <%= new java.util.Date() %>

接着,访问 http://localhost:8080/hello_world.jsp,你将看到类似下面这样的文本:

Hello world! Time: Sat Jul 27 20:39:19 PDT 2019

代码被顺利执行了。可是根据我们学到的原理我们应该能找到这个JSP文件生成的Java和class文件它们应该藏在某处。没错现在进入如下目录${CATALINA_HOME}/work/Catalina/localhost/ROOT/org/apache/jsp你可以看到这样几个文件

index_jsp.java
hello_005fworld_jsp.java
index_jsp.class
hello_005fworld_jsp.class

你看前两个Java文件就是根据JSP生成的Servlet的源代码后两个就是这个Servlet编译后的字节码。以index开头的文件就是Tomcat启动时你最初看到的主页面而以hello开头的这两个文件则完全来自于我们创建的hello_world.jsp。

现在你可以打开 hello_005fworld_jsp.java如果你有Java基础那么你应该可以看得懂其中的代码。代码中公有类 hello_005fworld_jsp 继承自 HttpJspBase 类,而如果你查看 Tomcat的API文档你就会知道原来它进一步继承自HttpServlet类也就是说这个自动生成的 Java 文件,就是 Servlet。

在117行附近你可以找到我们写在 JSP 页面中的内容,它们以流的方式被写入了 HTTP 响应:

out.write("Hello world! Time: ");
out.print( new java.util.Date() );
out.write('\n');

通过自己动手,我想你现在应该更加理解 JSP 的工作原理了。你看JSP和Servlet并不是完全独立的“两个人”JSP实际工作的时候是以Servlet的形式存在的,也就是说,前者其实是可以转化成后者的。

3. 深入理解

那么问题来了我们为什么不直接使用Servlet而要设计出JSP这样的技术让其在实际运行中转化成Servlet来执行呢

最重要的原因,从编程范型的角度来看JSP页面的代码多是基于声明式Declarative而Servlet的代码则多是基于命令式Imperative,这两种技术适合不同的场景。这两个概念,最初来源于编程范型的分类,声明式编程,是去描述物件的性质,而非给出指令,而命令式编程则恰恰相反。

比方说典型的JSP页面代码中只有少数一些scriptlet大部分还是HTML等格式的文本而HTML文本会告诉浏览器这里显示一个按钮那里显示一个文本输入框随着程序员对代码的阅读可以形象地在脑海里勾勒出这个页面的样子这也是声明式代码的一大特点。全栈工程师经常接触到的HTML、XML、JSON和CSS等都是声明式代码。你可能注意到了这些代码都不是使用编程语言写的而是使用标记语言写的但是编程语言其实也有声明式的比如Prolog。

再来说命令式代码在Servlet中它会一条一条语句告诉计算机下一步该做什么这个过程就是命令式的。我们绝大多数的代码都是命令式的。声明式代码是告诉计算机“什么样”而不关注“怎么做”命令式代码则是告诉计算机“怎么做”而不关注“什么样”。

为什么需要两种方式?因为人的思维是很奇特的,对于某些问题,使用声明式会更符合直觉,更形象,因而更接近于人类的语言;而另一些问题,则使用命令式,更符合行为步骤的思考模式,更严谨,也更能够预知机器会怎样执行

计算机生来就是遵循命令执行的因此声明式的JSP页面会被转化成一行行命令式的Servlet代码交给计算机执行。可是你可以想象一下如果HTML那样适合声明式表述的代码程序员使用命令式来手写会是怎样的一场噩梦——代码将会变成无趣且易错的一行行字符串拼接。

MVC 的演进

我想你一定听过MVC这种经典的架构模式它早在20世纪70年代就被发明出来了直到现在互联网上的大多数网站都是遵从MVC实现的这足以见其旺盛的生命力。MVC模式包含这样三层

  • 控制器Controller恰如其名主要负责请求的处理、校验和转发。
  • 视图View将内容数据以界面的方式呈现给用户也捕获和响应用户的操作。
  • 模型Model数据和业务逻辑真正的集散地。

你可能会想这不够全面啊这三层之间的交互和数据流动在哪里别急MVC在历史上经历了多次演进这三层再加上用户它们之间的交互模型是逐渐变化的。哪怕在今天不同的MVC框架的实现在这一点上也是有区别的。

1. JSP Model 1

JSP Model 1 是整个演化过程中最古老的一种请求处理的整个过程包括参数验证、数据访问、业务处理到页面渲染或者响应构造全部都放在JSP页面里面完成。JSP页面既当爹又当妈静态页面和嵌入动态表达式的特性使得它可以很好地容纳声明式代码而JSP的scriptlet又完全支持多行Java代码的写入因此它又可以很好地容纳命令式代码。

2. JSP Model 2

在 Model 1 中,你可以对 JSP页面上的内容进行模块和职责的划分但是由于它们都在一个页面上物理层面上可以说是完全耦合在一起因此模块化和单一职责无从谈起。和 Model 1 相比Model 2 做了明显的改进。

  • JSP只用来做一件事那就是页面渲染换言之JSP从全能先生转变成了单一职责的页面模板
  • 引入JavaBean的概念它将数据库访问等获取数据对象的行为封装了起来成为业务数据的唯一来源
  • 请求处理和派发的活交到纯Servlet手里它成为了MVC的“大脑”它知道创建哪个JavaBean准备好业务数据也知道将请求引导到哪个JSP页面去做渲染。

通过这种方式你可以看到原本全能的JSP被解耦开了分成了三层这三层其实就是MVC的View、Model和Controller。于是殊途同归MVC又一次进入了人们的视野今天的MVC框架千差万别原理上却和这个版本基本一致。

上面提到了一个概念JavaBean随之还有一个常见的概念POJO这是在Java领域中经常听到的两个名词但有时它们被混用。在此我想对这两个概念做一个简短的说明。

  • JavaBean其实指的是一类特殊的封装对象这里的“Bean”其实指的就是可重用的封装对象。它的特点是可序列化包含一个无参构造器以及遵循统一的getter和setter这样的简单命名规则的存取方法。
  • POJO即Plain Old Java Object还是最擅长创建软件概念的Martin Fowler的杰作。它指的就是一个普通和简单的Java对象没有特殊限制也不和其它类有关联它不能继承自其它类不能实现任何接口也不能被任何注解修饰

所以二者是两个类似的概念通常认为它们之间具备包含关系即JavaBean可以视作POJO的一种。但它们二者也有一些共性比如它们都是可以承载实际数据状态都定义了较为简单的方法概念上对它们的限制只停留在外在表现即内部实现可以不“plain”可以很复杂比如JavaBean经常在内部实现中读写数据库

3. MVC的一般化

JSP Model 2 已经具备了MVC的基本形态但是它却对技术栈有着明确限制——Servlet、JSP和JavaBean。今天我们见到的MVC已经和实现技术无关了并且在MVC三层大体职责确定的基础上其中的交互和数据流动却是有许多不同的实现方式的。

不同的MVC框架下实现的MVC架构不同有时即便是同一个框架不同的版本之间其MVC架构都有差异比如ASP.NET MVC在这里我只介绍最典型的两种情况如果你在学习的过程中见到其它类型请不要惊讶重要的是理解其中的原理。

第一种:

上图是第一种典型情况这种情况下用户请求发送给Controller而Controller是大总管需要主动调用Model层的接口去取得实际需要的数据对象之后将数据对象发送给需要渲染的ViewView渲染之后返回页面给用户。

在这种情况下Controller往往会比较大因为它要知道需要调用哪个Model的接口获取数据对象还需要知道要把数据对象发送给哪个View去渲染View和Model都比较简单纯粹它们都只需要被动地根据Controller的要求完成它们自己的任务就好了。

第二种:

上图是第二种典型情况请和第一种比较注意到了区别没有这种情况在更新操作中比较常见Controller调用Model的接口发起数据更新操作接着就直接转向最终的View去了View会调用Model去取得经过Controller更新操作以后的最新对象渲染并返回给用户。

在这种情况下Controller相对就会比较简单而这里写操作是由Controller发起的读操作是由View发起的二者的业务对象模型可以不相同非常适合需要CQRSCommand Query Responsibility Segregation命令查询职责分离的场景我在 [第 08 讲] 中会进一步介绍 CQRS。

4. MVC的变体

MVC的故事还没完当它的核心三层和它们的基本职责发生变化这样的架构模式就不再是严格意义上的MVC了。这里我介绍两种MVC的变体MVP和MVVM。

MVP包含的三层为Model、View和Presenter它往往被用在用户的界面设计当中和MVC比起来Controller被Presenter替代了。

  • Model的职责没有太大的变化依然是业务数据的唯一来源。
  • View变成了纯粹的被动视图它被动地响应用户的操作来触发事件并将其转交给Presenter反过来它的视图界面被动地由Presenter来发起更新。
  • Presenter变成了View和Model之间的协调者Middle-man它是真正调度逻辑的持有者会根据事件对Model进行状态更新又在Model层发生改变时相应地更新View。

MVVM是在MVP的基础上将职责最多的Presenter替换成了ViewModel它实际是一个数据对象的转换器将从Model中取得的数据简化转换为View可以识别的形式返回给View。View和ViewModel实行双向绑定成为命运共同体即View的变化会自动反馈到ViewModel中反之亦然。关于数据双向绑定的知识我还会在 [第 10 讲] 中详解。

总结思考

今天我们学习了 JSP 和 Servlet 这两个同源技术的本质,它们是分别通过声明式和命令式两种编程范型来解决同一问题的体现,接着围绕解耦这一核心,了解了 MVC 的几种形式和变体。

  • JSP Model 1请求处理的整个过程全部都耦合在JSP页面里面完成
  • JSP Model 2MVC 分别通过 JavaBean、JSP 和 Servlet 解耦成三层;
  • MVC 的常见形式一:数据由 Controller 调用 Model 来准备,并传递给 View 层;
  • MVC 的常见形式二Controller 发起对数据的修改,在 View 中查询修改后的数据并展示,二者分别调用 Model
  • MVPPresenter 扮演协调者,对 Model 和 View 实施状态的更新;
  • MVVMView 和 ViewModel 实行数据的双向绑定,以自动同步状态。

好,现在提两个问题,检验一下今天的学习成果:

  • 我们介绍了JSP页面和Servlet在编程范型上的不同这两个技术有着不同的使用场景你能举出例子来说明吗
  • 在介绍MVC的一般化时我介绍了两种典型的MVC各层调用和数据流向的实现你工作或学习中使用过哪一种还是都没使用过而是第三种

MVC是本章的核心内容在这之后的几讲中我会对MVC逐层分解今天的内容先打个基础希望你能真正地理解和消化这将有助于之后的学习。欢迎你在留言区和我讨论

扩展阅读

  • 【基础】专栏文章中的例子有时会涉及到 Java 代码,如果你对 Java 很不熟悉,可以参考廖雪峰 Java 教程中“快速入门”的部分,它很短小,但是覆盖了专栏需要的 Java 基础知识。
  • 【基础】W3Cschool上的 Servlet教程JSP教程,如果你对这二者完全不了解,那我推荐你阅读。在较为系统的教程中,这两个算较为简洁的,如果觉得内容较多,可以挑选其中的几个核心章节阅读。
  • 如果你顺利地将文中介绍的Tomcat启动起来了并且用的也是9.x版本那么你可以直接访问 http://localhost:8080/examples/里面有Tomcat自带的很多典型和带有源码的例子有JSP的例子也有Servlet的例子还有WebSocket的例子由于我们前一章已经学过了WebSocket这里你应该可以较为顺利地学习