表示层的困惑
《Struts2技术内幕:深入解析Struts架构设计与实现原理》第2章固本清源—Web开发浅谈,本章讨论的话题是非常重要的,因为任何细节都无法脱离基本概念而存在。如果我们要探寻Struts2的细节,就必须了解Struts2作为一个框架存在的基本意义。本节为大家介绍表示层的困惑。
除了上述这3段源代码外,我们还需要建立起JSP页面中的form请求与Servlet类的响应之间的关系。这一关系,是在web.xml中维护的,如代码清单2-9所示。
代码清单2-9 web.xml
<servlet>
<servlet-name>Register</servlet-name>
<servlet-class>example.RegistrationServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Register</servlet-name>
<url-pattern>/struts2_example/registration</url-pattern>
</servlet-mapping>
我们来看看上面的这4段代码是如何构成MVC的雏形的。
- Model(数据模型)—User.java
- View(对外交互)—registration.jsp
- Control(程序执行和控制)—RegistrationServlet.java
URL Mapping(请求转化)—web.xml
我们可以看到MVC的实现似乎并不复杂。在不借助额外的框架帮助的前提下,只要基本知晓JSP和Servlet标准(它们是使用Java进行Web开发的规范和标准),任何程序员都可以像模像样地实现MVC模式,因为从原理上讲,MVC只是一个概念,我们只需要把这个概念中的各个元素赋予相应的程序实现即可。
不过程序终究是一个动态的执行过程。一旦程序开始运行,上面的这些程序实现就会开始遭遇种种困境。这些困境主要来源于两个方面:其一,出于程序自身的可读性和可维护性考虑,需要通过重构来解决程序的复杂性困境。其二,出于业务扩展的需求,需要通过框架级别的功能增强来解决可扩展性困境。
问题1 当浏览器发送一个Http请求,Web容器是如何接收这个请求并指定相应的Java类来执行业务逻辑并返回处理结果的?
这个问题是使用Java进行Web开发的核心问题之一,我们将这个问题简称为URL
Mapping问题。这个问题的本质实际上来源于Http协议与Java程序之间的匹配和交互。Web开发经过了多年的发展,这一核心的哲学问题也经历了多次重大变革,有的崇尚由繁至简,有的则从形式多样化入手。
在上面的例子中,我们可以看到使用web.xml来表达URL
Mapping关系遇到的困境:当系统变大,这种配置上的重复操作会让web.xml变得越来越大而难以维护。不仅如此,web.xml的配置也无法为URL
Mapping建立起合适的规则引擎。
由此,解决URL
Mapping问题的核心在于建立一套由Http协议中的URL表达式到Java世界中类对象的规则匹配引擎。额外的,这种规则匹配最好比较灵活而简单又不失必要的可维护性。
问题2 Web应用是典型的“请求-响应”模式的应用,数据是如何顺利流转于浏览器和Java世界之间的?面对Http协议与Java世界数据形式的不匹配性,我们如何能够在流转时做到数据类型的自动转化?
这个问题伴随着问题1而来,数据请求与数据返回相当于是基于“请求-响应”模式的Web程序的输入和输出。数据的本质是存储于其中的信息,只不过数据在不同的地方有不同的表现形式。例如,在浏览器中,数据总是以字符串形式展现出来,表现出“弱类型”的特征;在Java世界,数据则体现为一个个结构化的Java对象,表现出“强类型”的特征。于是,就需要有一个工具能够帮助我们解决在数据流转时的数据形式的相互转化。
在上面的例子中,我们可以看到RegistrationServlet中,我们编写了额外的代码,把页面上传递过来的日期值转化为Java中的Date对象。在参数的数量和Java对象越来越复杂的情况下,这种额外的代码就会变成一种灾难,甚至成为我们开发的主要瓶颈之一。
解决数据流转问题的方案是使用表达式引擎。将表达式引擎插入到程序逻辑执行之前,我们就能从复杂的对象转化中解放出来,从而进一步简化开发流程。
问题3 Web容器是一个典型的多线程环境,针对每个Http请求,Web容器的线程池会分配一个特定的线程进行处理。那么如何保证在多线程环境下,处理请求的Java类是线程安全的对象?如何保证数据的流转和访问都是线程安全的?
这个问题与问题1一样,也是Web开发中的核心问题之一,因为它涉及Web开发中最为底层的处理机制问题。在上面的例子中,我们使用的是基于Servlet标准的方式进行编程,扩展Servlet用于处理Http请求。然而恰恰就是这种编程模型,是一种非线程安全的编程模型,因为Servlet对象是一个非线程安全的对象。也就是说,如果我们在doPost方法中访问RegistrationServlet中所定义的局部变量,就会产生线程安全问题(第4章会重点介绍线程安全问题产生的来龙去脉)。
传统的表示层框架对于这个问题的处理方式是采用规避问题的方式。既然Servlet对象不是一个线程安全的对象,那么我们就干脆禁止在Servlet对象的方法中访问Servlet对象的内部变量。这种鸵鸟算法固然是一种有效的方案,但它却不是一种合理的方案。最致命的一点是,它是一种非语法检查级别的禁止,因此也就无法从根本上杜绝程序员犯这样的错误。
另外一种解决方案就是在整个请求周期中引入ThreadLocal模式,通过ThreadLocal模式的使用,将整个过程的对象访问都线程安全化,彻底解决多线程环境下的数据访问问题(有关ThreadLocal模式的方方面面,我们在后续章节中会详细介绍)。ThreadLocal模式的引入对于Web层框架的影响是深远并且颠覆性的,因为它为框架摆脱Web容器的依赖铺平了道路,意味着我们可以通过合理的设计,在脱离Servlet等Web容器元素的环境中进行编程。
问题4 Controller层作为MVC的核心控制器,如何能够在最大程度上支持功能点上的扩展?
问题4来源于我们对程序本身的自然属性(可读性和可扩展性)的需求。这一内在需求实际上也驱动着我们着手在整个MVC的构架级别设计更为成熟有效的自扩展方案。
从一个更加宏观的角度来帮助我们理解这个问题,我们来举一个制药工厂生产药品的例子。一个工厂在进行批量生产时,总是会引入“生产线”的概念。生产线能够把整个制药过程划分成若干道工序,当原材料经过每一道工序,最终就会成为一个可出厂销售的药品。某一天,由于市场推广的原因,需要改变药品的包装,那么我们对这条生产线的要求就是它能够改变“包装”这道工序的流程,更改成新的包装。
在上面的例子中,我们可以看到并没有一个“生产线”的概念。这种情况下,我们日后对于逻辑功能的扩展就变得困难重重。虽然我们发现,RegistrationServlet或许和其他所有的Servlet有着非常类似的执行步骤:接收参数、进行类型转换、调用业务逻辑接口执行逻辑、返回处理结果。然而我们却缺乏一条可以任意配置调度的生产线将这个过程规范起来。
解决这个问题从直观上来讲似乎很容易:没有生产线,我们建一条生产线就行了。而事实上,“造轮子”实在是一件费时费力的事情,因为我们要考虑的方面实在太多。这时我们就不得不借鉴许多前辈的经验了,寻找某些事件定义的框架,遵循框架的定义规范来进行编程将是我们解决这个问题的主要途径。
问题5 View层的表现形式总是多种多样的,随着Web开发技术的不断发展,MVC如何在框架级别提供一种完全透明的方式来应对不同的视图表现形式?
这一问题是基于View(视图)技术的不断发展,造成传统的基于HTML的视图已经不能满足所有的需求而提出的。当今,越来越多新的视图技术被用于Web开发中,例如,模板技术、JSON数据流、Stream数据流、Flash展现等等。
在上面的例子中,我们可以看到负责视图层跳转的RegistrationServlet是通过硬编码方式完成程序执行跳转的。这种方式不但无法支持多种新的视图技术,同时也无法使我们从复杂的视图跳转的硬编码中释放出来。
解决这个问题的最有效途径是把不同的视图技术进行分类,针对不同的分类封装不同的视图跳转逻辑,而最重要的一步是将这两者与之前我们所提到的生产线有机结合起来。
问题6 MVC模式虽然很直观地为我们规定了表示层的各种元素,但是如何通过某种机制把这些元素有机整合在一起,从而成为一个整体呢?
这个问题非常宏观,却是我们不得不去面对的一个问题。MVC虽然在概念上被规定下来,在实现上却需要一个完整的机制来把这些元素都容纳在一起。通常情况下,我们往往把这种机制称之为配置元素。配置元素是构成程序的重要组成部分,它把各种形式的程序通过某种配置规则联系在一起。之前我们提到的URL
Mapping实际上也属于配置规则的一种,视图的跳转也是配置规则的一种。只有当这种配置规则被建立起来,MVC模式才能真正运作起来。
这一系列配置元素在框架内部往往被定义成统一的可以被框架识别的数据结构并在系统初始化的时候进行缓存。而这些被缓存了的对象,也成为主程序的控制流在MVC框架中各个元素之间进行流转的依据。
如果从元素的表现形式上来看配置元素和控制流的关系,我们实际上可以看到整合过程的两个层面:数据结构和流程控制。所谓的框架,我们也只是在这两个层面上做文章,一方面规定好这些配置元素的定义,另一方面指定程序运转的流程,从而控制和整合散落在各处的表示层元素。