第22章 迈向Spring MVC的旅程
本章内容
-
Servlet独行天下的时代
-
繁盛一时的JSP时代
-
Servlet与JSP的联盟
-
数英雄人物,还看今朝
“子曰:温故而知新”,如果简单回顾一下整个Java平台上的Web开发历程,将极大有助于我们理解当前各个Web开发框架存在的背景和先进性,最主要的是有助于我们平滑过渡到SpringMVC的世界中去。所以,不妨让我们从最初的Servlet独行天下的时代说起。
22.1 Servlet独行天下的时代
话说Servlet是当年Java平台上第一个用于Web开发的技术。相对于CGI(Common Gateway Interface)时代来说,Servlet的提出是一个很大的进步,它运行于Web容器(Web Container)之内,提供了Session和对象生命周期管理等功能。最主要的,Servlet依然是Java类,从中直接访问Java平台的各种服务,并使用相应的API支持是很自然的事情,比如调用业务对象,使用JDBC进行数据访问等。
Servlet 本身并非万能的,它的强项在于无缝地衔接业务对象与 Web 层对象之间的调用,以及二进制显示内容的输出等。 但在当时开发人员只有 Servlet 这一种技术可以选择的时候,或许是不得不,也或许是盲从,又或许根本就是图一时的省事儿,单一的 Servlet 被赋予了过多的使命,从而导致了开发过程中出现的一系列的弊病。最常见的,就是称为“神奇 Servlet(Magic Servlet)”的普遍存在。在“神奇 Servlet”中,开发人员会将各种逻辑混杂于一处,包括流程控制逻辑、视图显示逻辑、业务逻辑、数据访问逻辑等,这就造成了后期系统的难以维护等一系列问题。 我们不妨看一段模拟当年的 Web 应用中的 Servlet 实现代码,见下方代码清单。
代码的可读性极差,这一点先放一边不说,单就“数据访问逻辑和业务处理逻辑与对应的视图渲染逻辑相互混杂”这一点来说,就已经让今天的我们觉得该Servlet的实现是如此的不堪入目了。没有将相应的关注点进行明确的分离,直接导致相应的逻辑无法重用,进而造成后期系统难以维护。那么,我们有没有办法来重构这段代码,以使得它结构良好,易于维护呢?
实际上,我们只要将相应的逻辑以独立的形式剥离出来,避免这些逻辑之间的混杂,就应该能够得到一个结构清晰的应用。对于使用JDBC原始API进行数据访问的代码逻辑来说,有了之前Spring数据访问相关内容作为基础,对其进行重构应该是一件易事。假设你已经能够将这部分逻辑剥离到相应的数据访问对象,并提供了相应的MockServletService封装一系列的数据访问逻辑、事务管理逻辑等,那么,重构后的MockMagicServlet的代码看起来如下方代码清单所示。
哇噢,清爽多了,不是吗?不过,我们依然没有摆脱那些烦人的out.println,而且,对于还处于懵懂状态的Servlet开发时代来说,这些out.println可不只是烦人而已。
-
“神奇Servlet”的存在,并不意味着一个Web应用程序中只存在一个Servlet实现。实际上,Servlet时代之初,我们更多是使用“一个Servlet对应处理一个Web请求”的方式。对于简单的Web应用来说,这种用于生成视图的out.println语句,分散的程度看起来还不算夸张。可是,随着应用规模的扩展,试想一下,开发和维护这些out.println的工作量将是何等的恐怖?
-
我们的代码示例只是给出了一个简单的视图渲染逻辑。可是,随着页面逻辑的膨胀,要维护这么一堆几乎无法“纵观全局”的out.println,我们这些开发人员该怎么办呢?暂且不谈开发人员大都不熟悉(X)HTML等页面标记语言,就算熟知,要在浩瀚的out.println中寻找要更改的位置,且保证更改过程中不会造成之前的显示逻辑的破坏,又是谈何容易啊!
-
因为视图逻辑是以Java代码的形式写死到Servlet中的,所以如果视图逻辑需要变动的话,我们就得更改Servlet的代码并重新编译。开发人员或许会说视图逻辑不归我们管,那是美工和前台开发人员的工作,可是,写死到Java代码中的视图逻辑,你又能逃脱了干系不成?
令人遗憾的是,仅靠Servlet一人之力,我们无法解决视图逻辑与Servlet紧密耦合的问题。好消息是,这个时候有了JSP前来救驾。
提示:请稍微关注以上的Servlet代码。当然,这样说,并不是因为它臻于完美,而是因为不管这之后的Web应用的开发如何演化,都将以该Servlet为基础进行,不信的话,继续往下看。
22.2 繁盛一时的JSP时代
为了能够将Servlet中的视图渲染逻辑以独立的单元抽取出来,我们通常使用“模板化”(templating)的方法。JSP的提出,成为Java平台上开发Web应用程序事实上的模板化视图标准。
有了JSP的加入,我们就可以将原先不得不在Servlet中通过out.println语句输出的视图渲染逻辑,抽取到.jsp后缀名的模板文件中。这样,我们就有了如下方代码清单所示的视图模板定义。
现在,由JSP专职负责视图的渲染工作,我们的MockMagicServlet得以进一步的解脱。现在,MockMagicServlet的主要实现逻辑看起来如下方代码清单所示。
啊哈,看起来我们离成功仅是一步之遥啦!但是,不好意思,在此之前,我们还需要经历一段曲折的日子。
实际上,如果当初我们的开发人员或者是“技术布道者”能够严格地界定JSP的基本使命的话,我们早就迈入了WebMVC的世界了。不过,如果我们更愿意“怨天尤人”,也可以把责任推卸给JSP本身。我们都知道,JSP与其他模板技术有一个主要的区别,那就是,它最终是编译为Servlet来运行的,这一层关系使得JSP拥有比其他通用模板技术更大的能量。
-
我们可以直接在JSP中编写Java代码,通过Scriptlet,只要愿意,任何的应用程序逻辑几乎都能够融入JSP广阔的“胸襟”。我还清晰地记得当初自己写下的第一个Web应用程序是什么样子,整个应用几乎都是JSP文件组成,几乎什么逻辑全部通过Scriptlet编写到JSP文件中,甚至于数据库连接代码。我想,这只是一个缩影,实际上,那几年无论是技术社区还是技术书籍,倡导的也都是类似理念。比如介绍JSP的书籍,即使不应该放到JSP内的逻辑,也全都写入JSP的实例中,使得你不上当都难。可是现在回头看看,如果说我们之前创造了“神奇Servlet”,那么,现在,我们创造的则是神奇JSP了。相应的弊病,就不多说了。
-
使用Servlet处理Web请求,我们需要在web.xml文件中,注册相应的请求URL与具体的处理Servlet之间的一个映射关系。之前说过,最初阶段,我们是一个Web请求对应一个Servlet进行处理的,所以web.xml中就过多地充斥着与下方代码清单中类似的映射配置信息。
使用JSP的话,则可以省却这些烦琐,直接通过链接就可以,无需任何配置。这也助长了“超频”使用JSP的“歪风”,直接使用JSP替代Servlet处理Web请求。对于简单的应用,或许几个页面的流程关系还理得清楚,一旦应用规模上来了,分散于各个JSP文件中的流程控制信息,无异于一张杂乱无章的网,令人理不清,道不明,更不用说易于管理和维护了。而且,将原本由Servlet处理的Web流程逻辑纳入JSP的职权,我们又进一步帮助了神奇JSP的尽快诞生。
本该行使简单的视图渲染功能的JSP,现在已经完全替代了Servlet而一统天下了,也就是说,我们现在不需要MockMagicServlet了,取而代之的,就是我们原先设想用于剥离试图渲染逻辑的JSP。而现在,它自身已经不只是一个视图模板了(见下方代码清单)。
包揽全部职能的JSP代码示例:
可是,历史是相似的。当初为了解决“神奇Servlet”的问题,而被迫分离应用的各种关注点,我们是否也注意到了现在的JSP又以相似的步伐重蹈“神奇Servlet”的覆辙呢?答案是肯定的,不光我们注意到了,SUN公司也注意到了,所以,这也促使了JSP Model 1的诞生(见图22-1)。
在JSP独大的世界中引入JavaBean,通过JavaBean对相关业务逻辑的封装,我们完成了初步的关注点分离。不过,JSP Model 1的提出并没有进一步地限定JSP的基本职责,本该一心关注视图渲染逻辑的JSP,现在依然紧攥着本该是Servlet强项的Web流程管理功能不放。看来,革命尚未成功,我们依然需要努力啊!
注意:单独使用JSP阶段的Web开发,还存在许多的弊端,你可以从Rod Johnson的ExpertOne-on-One J2EE Design and Development一书中了解更多详情。
另外,JSP Model 1也有其先进性,我们可以用它快速的构建Web应用程序的原型,但是,切记,不要以这种架构用于实际的生产环境。
22.3 Servlet与JSP的联盟
JSP的不良诱感使得我们走上了歧路,本已经近在咫尺的良好架构,在经历了一段尘封的岁月之后,又重现光芒。让我们回到JSP时代的开始,在那里,我们让JSP做为视图模板而存在。不管它有多大的能耐,我们只让它负责视图的渲染工作,这样,对于JSP来说,只需要页面开发人员或者说表现层(presentation layer)开发人员来负贵和管理即可。而已经剥离了视图渲染逻辑Servlet,现在只负责请求处理流程的控制以及与业务层的交互,这部分工作则由Java开发人员来负责。至此,我们不仅将各个关注点清晰地分离出来,而且也分离了Java开发人员与前台开发人员之间的职责。而后者对于一个复杂并且需要多人协作的团队来说,是至关重要的。
通过结合Servlet和JSP,并且引入封装业务层逻辑的JavaBean,我们得到了JSP Model 1的升级版架构,即JSP Model 2,见图22-2。
在Model 2中,与我们重构过程结果所展示的一样,由Servlet来管理处理流程,由JSP来负责视图的渲染,并由JavaBean封装业务逻辑并负贵与数据层进行交互。JSP Model 2的提出可以说是Java平台Web应用开发过程中一个里程碑,它促进了Web开发领域至今一直沿用的MVC设计模式的广泛应用。
虽然JSP Model 2已经具备了使用MVC模式实现的Web应用架构的雏形,但并非严格意义上的MVC。为了搞清楚其间的差别,我们先来简单回顾一下MVC模式以及模式中涉及的几个组件,见图22-3。
MVC(Model-View-Controller,模型-视图控制器)在当今Java界尤其是Web开发领域已经是耳熟能详的一个名词了。如图22-3所示,最初意义上的MVC模式中,各个组件的作用如下所述。
- 控制器负责接收视图发送的请求并进行处理 ,它会根据请求条件通知模型进行应用程序状态的更新,之后选择合适的视图显示给用户。
- 模型通常封装了应用的逻辑以及数据状态。 当控制器通知模型进行状态更新的时候,模型封装的相应逻辑将被调用。执行完成后,模型通常会通过事件机制通知视图状态更新完毕,从而视图可以显示最新的数据状态。
- 视图是面向用户的接口。 当用户通过视图发起某种请求的时候,视图将这些请求转发给控制器进行处理。处理流程流经控制器和模型之后,最终视图将接收到模型的状态更新通知,然后视图将结合模型数据,更新自身的显示。
可见,最初意义上的MVC模式,在视图与模型间的数据同步工作是采用从模型Push到视图的形式完成的。而对于Web应用来说,局限于所用的协议以及使用场景,无法实现从模型Push数据到视图这样的功能。所以,我们只能对MVC中的组件的最初作用定义做适当的调整,由控制器与模型进行交互,在原来通知模型更新应用程序状态的基础上,还要获取模型更新的结果数据,然后将更新的模型数据一并转发给视图。也就是说,我们现在改由控制器从模型中Pull数据给视图,这种意义上的MVC称为Web MVC,也就是现在大多说Web开发框架所使用的架构模式。
实际上,JSP Model 2已经十分接近WebMVC架构了,但是,在真正步入WebMVC应用框架时代之前,我们还是来看一下JSP Model 2在具体的应用过程中存在哪些问题,我们又是如何解决这些问题,并进而促成WebMVC应用程序框架的广泛应用的吧!
从JSP Model 2架构的示意图(图22-3)上,我们可以看到,Servlet是作为控制器的角色存在的,但是,该架构示意图并没有进一步规定,具体应用中到底是只需要一个控制器,还是使用多个控制器,这就造成如下两种情况。
- Web应用程序中使用多个Servlet作为控制器。 这实际上也是从最初Servlet步入Java平台Web开发领域后使用最多的模式,即一个Servlet对应一个Web请求的处理。以这种方式进行的开发实践表明,我们需要为每个请求处理流程都定义一个Servlet,并借助Web容器的URL映射匹配能力来解决Web请求到具体的处理Servlet的映射。自然,我们就需要在web.xml配置文件中,为每个Servlet都提供定义并添加URL映射。随着应用规模的增加,web.xml的体积将愈加庞大。最主要的,系统中的所有Web请求的处理流程将各自分散管理,没有一种集中管理的方式。这将不利于整个系统的开发和维护工作。所以,随着开发理念的更新,这种方式逐步淡化出了我们的视野,并更多侧重于下面这种单一Servlet作为整个Web应用程序控制器的实践方式。
- Web应用程序中使用单一Servlet作为集中控制器。 现在,所有的Web处理请求全部经由Web应用程序中定义的这个单一的Servlet控制器来进行。相对于原先的情况,请求的处理现在有了一个集中管理的位置,而且,也不用顾虑web.xml文件内容是否会因Web请求流程的增多而膨胀。不过,却遇到了新的问题,我们避免了web.xml文件的膨胀,却将这种膨胀变相地带到了Servlet控制器类中,如下所述。 * 因为现在所有的Web请求都映射到了集中的Servlet控制器来处理,所以,控制器类需要自己来根据Web请求的URL信息进行分析,以判断处理流程的流向。 显然,无法再借助Web容器的URL映射匹配能力来完成这个工作了。早期来说,这些逻辑都是硬编码到Servlet控制器中的。这些逻辑往往不能重用,而且最主要的,一旦写死,要调整URL映射的处理,就得修改Servlet控制器的代码并重新编译,灵活性和可扩展性根本无从谈起。 * 分析完URL映射关系之后,Servlet控制器就可以根据结果来选择执行哪些处理流程,硬编码的问题再次出现了。那个时候的控制器Servlet类,大都是将处理流程和处理逻辑硬编码到自身,无论是流程分支的调整,还是具体每个分支的处理逻辑的调整,都不可避免地要对Servlet控制器的实现代码进行一番“或大或小”的手术。当然,下一个应用开始之后,这些处理流程的转发逻辑以及其他通用逻辑,是无法复用到下一个应用程序中的。
不过,情况并没有我们所看到的那么坏,引入合适的设计模式,可以避免早期单一Servlet控制器所面临的问题。所以,这种方式依然是我们比较倾向使用的控制器实践方式。
如我们所看到的,制约JSP Model 2发展的,就是 将流程控制等通用相关逻辑进行硬编码的实践方式 ,这直接导致了JSP Model 2架构的不可重用性。每次启动新的Web应用程序的开发工作,通常又得从头编写Servlet控制器的URL分析,以及流程控制等Web层通用逻辑。这自然就促使我们寻找途径,去除架构中控制逻辑的硬编码,并尽可能地复用Web应用程序开发过程中的一些通用逻辑。而在JSP Model 2架构基础,上发展起来的各种Web MVC应用框架,恰好顺应了历史的需求。现在,我们步入了各种Web应用程序框架盛行的时代。
22.4 数英雄人物,还看今朝
Web框架存在的意义在于,它们为Web应用程序的开发提供了一套可复用的基础设施,这样开发人员只需要关注特定于每个应用的逻辑开发工作,而不需要每次都重复那些可以统一处理的通用逻辑。当前的Web开发框架有如下两种类型。
-
请求驱动的Web框架(request-driven framework)。 又称为request/response框架(request/response framework)。顾名思义,这种框架是基于Servlet的请求/响应(request/response)处理模型构建的。这种类型的开发框架大都以WebMVC模式为指导,在JSPModel2架构基础上“进化”而来。比如,几乎是整个Java平台Web开发框架事实标准的Struts框架,优雅轻便的WebWork等,以及我们稍后即将为你介绍的SpringMVC框架,都属于这种请求驱动的Web开发框架。
-
事件驱动Web开发框架(event-drivenweb framework)。 或许它的另一个名字基于组件的Web开发框架(component-based framework)更好理解一些。这种框架采用与Swing等GUI开发框架类似的思想,将视图组件化,由视图中的相应组件触发事件,进而驱动整个处理流程。最初的Tapestry(http://tapestryapache.org/)框架以及现在的JSF(Java Server Faces)框架都属于这一类。
Spring MVC属于请求驱动的Web框架,所以,我们要对这种类型的框架给予更多的笔墨。至于事件驱动的Web开发框架,如果你感兴趣,不妨自己做进一步深入的了解。
对于请求驱动的Web开发框架来说,它们大多是在JSP Model 2的基础上发展而来的。我们一定有一个问题,那就是,这些Web开发框架是如何解决JSP Model 2在实践中所存在的那些问题的?
如前所述,在JSP Model 2中,我们更加倾向于使用单一Servlet作控制器的实践方式。实际上,现在的请求驱动的Web框架也大都如此,但是为了避免之前提到的一些问题,这些框架通常会结合Front Controller以及Page Controller模式,对单一Servlet控制器做进一步的改进,对原先过于耦合的各种控制器逻辑进行逐步的分离。
具体来说,就是由原来的单一Servlet作为整个应用程序的Front Controller。该Servlet接收到具体的Web处理请求之后,会参照预先可配置的映射信息,将待处理的Web处理请求转发给次一级的控制器(sub-controller)来处理,整个情形看起来如图22-4所示。
在控制器Servlet接收到Web处理请求之后,它会对Web请求的URL信息进行分析,然后根据分析结果,并参照外部可配置的流程规则,将当前Web请求转发给次一级的控制器类进行处理。现在,作为Front Controller的Servlet和次级控制器类共同组成了整个应用程序的控制器。原先单一的控制器Servlet通过将流程控制信息外部化,并分离具体的Web请求处理逻辑给次级控制器类进行处理的方式,瘦身为灵活而可复用的担当Front Contoller的Servlet。有了这样的关注点分离之后,我们就极大地提高了整个Web应用中控制器逻辑的可复用性。
如果我们之前接触过Struts框架,对照一下以上的结构,将有助于更深入地理解该框架中各个组件所扮演的角色:
-
ActionServlet是整个框架的Front Controller,负责分发具体的Web请求;
-
Action是次级控制器,ActionServlet分发的具体Web请求将由被选中的Action来处理;
-
为了避免控制流程的硬编码,ActionServlet将从struts-config.xml中读取请求的URL与具体的Action之间的映射关系,进而正确地转发Web请求给相应的Action进行处理。
当然,各个Web框架中实现相应角色的具体实现类或许不同,但所要达到的目的是一样的。而且,有些框架还在以上实现结构的基础上,进一步细化了框架中可能复用的逻辑。在介绍完SpringMVC之后,你将发现一个设计更加完善的Web开发框架。
实际上,Spring MVC可以说是集之前各个Web框架的优点于一身,并且还有进一步的发展。相信当你更多地了解SpringMVC之后,也会深深地迷恋于此。话不多说,还是赶快开始我们的SpringMVC之旅吧!
22.5 小结
本章我们主要回顾了Java平台上以往的Web开发历程。我们从Servlet出现之初独当一面的时候开始,进而经历了JSP繁盛一时的时代,然后就是Servlet和JSP结盟,最终演化到现在Web开发框架盛行的年代。
有这些背景作为基础,接下来,我们将正式进入SpringMVC的领地,看一下SpringMVC框架到底有何独到之处。