1.1跟踪Spring MVC的请求
每当用户在Web浏览器中点击链接或提交表单的时候,请求就开始工作了。对请求的工作描述就像是快递投送员。与邮局投递员或FedEx投送员一样,请求会将信息从一个地方带到另一个地方。请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。图5.1展示了请求使用Spring MVC所经历的所有站点。
在请求离开浏览器时 ,会带有用户所请求内容的信息,至少会包含请求的URL。但是还可能带有其他的信息,例如用户提交的表单信息。请求旅程的第一站是Spring的DispatcherServlet。与大多数基于Java的Web框架一样,Spring MVC所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查
询一个或多个处理器映射(handler mapping) 来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器 。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理。)
控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回DispatcherServlet 。这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver)来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。
既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP) ,在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码) 。可以看到,请求要经过很多的步骤,最终才能形成返回给客户端的响应。大多数的步骤都是在Spring框架内部完成的,也就是图5.1所示的组件中。尽管本章的主要内容都关注于如何编写控制器,但在此之前
我们首先看一下如何搭建Spring MVC的基础组件。
1.2搭建Spring MVC
配置DispatcherServlet
DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。当然,这是配置DispatcherServlet的方法之一。但是,借助于Servlet 3规范和Spring 3.1的功能增强,这种方式已经不是唯一的方案了,这也不是我们本章所使用的配置方法。我们会使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下的程序清单展示了所需的Java类
Spring MVC配置的替代方案
在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和Context-LoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我们自己来完成这项任务了。如下是一个基本的web.xml文件,它按照传统的方式搭建了DispatcherServlet和ContextLoaderListener。
在web.xml中搭建Spring MVC
ContextLoaderListener和DispatcherServlet各自都会加载一个Spring应用上下文。上下文参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。如程序清单7.3所示,根上下文会从“/WEB-INF/spring/root-context.xml”中加载bean定义。DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。在程序清单7.3中,Servlet的名字是appServlet,因此DispatcherServlet会从“/WEB-INF/appServlet-context.xml”文件中加载其应用上下文。
如果你希望指定DispatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数。例如,如下的配置中,DispatcherServlet会从“/WEB-INF/spring/appServlet/servlet-context.xml”加载它的bean:
1.3处理multipart形式的数据
multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了multipart的请求体:
尽管multipart请求看起来很复杂,但在Spring MVC中处理它们却很容易。在编写控制器方法处理文件上传之前,我们必须要配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。
配置multipart解析器
DispatcherServlet并没有实现任何解析multipart请求数据的功能。它将该任务委托给了Spring中MultipartResolver策略接口的实现,通过这个实现类来解析multipart请求中的内容。从Spring 3.1开始,Spring内置了两个MultipartResolver的实现供我们选择:CommonsMultipartResolver:使用Jakarta CommonsFileUpload解析multipart请求;StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1)。一般来讲,在这两者之间,StandardServletMultipartResolver可能会是优选的方案。它使用Servlet所提供的功能支持,并不需要依赖任何其他的项目。如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没有使用Spring 3.1或更高版本,那么可能就需要CommonsMultipartResolver了。
1.4跨重定向请求传递数据
在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。在第5章,在控制器方法返回的视图名称中,我们借助了“redirect:”前缀的力量。当控制器方法返回的String值以“redirect:”开头的话,那么这个String不是用来查找视图的,而是用来指导浏览器进行重定向的路径。我们可以回头看一下程序清单5.17,可以看到processRegistration()方法返回的“redirect:String”如下所示:
“redirect:”前缀能够让重定向功能变得非常简单。你可能会想Spring很难再让重定向功能变得更简单了。但是,请稍等:Spring为重定向功能还提供了一些其他的辅助功能。具体来讲,正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。但是,如图7.1所示,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:
1.使用URL模板以路径变量和/或查询参数的形式传递数据;
2.通过flash属性发送数据。
首先,我们看一下Spring如何帮助我们通过路径变量和/或查询参数的形式传递数据。
通过URL模板进行重定向
通过路径变量和查询参数传递数据看起来非常简单。例如,在程序清单5.19中,我们以路径变量的形式传递了新创建Spitter的username。但是按照现在的写法,username的值是直接连接到重定向String上的。这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。
除了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。例如,在程序清单5.19中,processRegistration()方法的最后一行可以改写为如下的形式
现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任何想要的内容作为username,并会将其附加到路径上
除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查询参数。作为样例,假设除了username以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()方法可以改写为如下的形式:
所返回的重定向String并没有太大的变化。但是,因为模型中的spitterId属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。如果username属性的值是habuma并且spitterId属性的值是42,那么结果得到的重定向URL路径将会是“/spitter/habuma?spitterId=42”。
通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方式,但它也有一定的限制。它只能用来发送简单的值,如String和数字的值。在URL中,并没有办法发送更为复杂的值,但这正是flash属性能够提供帮助的领域。
使用flash属性
假设我们不想在重定向中发送username或ID了,而是要发送实际的Spitter对象。如果我们只发送ID的话,那么处理重定向的方法还需要从数据库中查找才能得到Spitter对象。但是,在重定向之前,我们其实已经得到了Spitter对象。为什么不将其发送给处理重定向的方法,并将其展现出来呢
实际上,Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。按照定义,flash属性会一直携带这些数据直到下一次请求,然后才会消失。
Spring提供了通过RedirectAttributes设置flash属性的方法,这是Spring 3.1引入的Model的一个子接口。RedirectAttributes提供了Model的所有功能,除此之外,还有几个方法是用来设置flash属性的。具体来讲,RedirectAttributes提供了一组addFlashAttribute()方法来添加flash属性。重新看一下processRegistration()方法,我们可以使用addFlashAttribute()将Spitter对象添加到模型中
在这里,我们调用了addFlashAttribute()方法,并将spitter作为key,Spitter对象作为值。另外,我们还可以不设置key参数,让key根据值的类型自行推断得出
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问Spitter对象了,就像获取其他的模型对象一样。图7.2阐述了它是如何运行的。