简介
Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从一开始就包含在Spring Framework
中。正式名称“Spring Web MVC”,来自其源模块(spring-webmvc
)的名称,但它通常被称为“Spring MVC”。
其中,MVC 分别代表:
- 模型(
Model
):封装了应用程序数据,通常它们将由 POJO 类组成 - 视图(
View
):负责渲染模型数据,一般来说它生成客户端浏览器可以解释输出的 HTML - 控制器(
Controller
):负责处理用户请求并构建适当的模型,并将其传递给视图进行渲染
作用
Spring MVC,与许多其他 Web 框架一样,Spring MVC 同样围绕前端页面的控制器模式(Controller
)进行设计,其中最为核心的 Servlet
—— DispatcherServlet
为来自客户端的请求处理提供了一种用于请求处理的共享算法,而实际的工作是由可自定义配置的委托组件来执行。该模型非常灵活,支持多种工作流程。
当服务器收到一个请求,建立在中央前端控制器Servlet
(DispatcherServlet
)将负责发送这个请求到合适的处理程序,使用视图来返回响应的最终结果经过渲染后再返回给用户。
工作流程
在实际开发中,我们的工作主要集中在控制器和视图页面上,但 Spring MVC 内部完成了很多工作,这些程序在项目中是如何执行的呢?
下面我们来通过一张图看一看 Spring MVC 程序的执行流程:
各步骤具体说明如下:
- ① 用户通过浏览器向
Web
应用服务器发送一个HTTP
请求,服务器收到请求后,如果匹配到DispatcherServlet
的在web.xml
指定的请求映射路径,Web 容器会将该请求转交给 Spring MVC 的前端控制器DispatcherServlet
拦截处理 - ②
DispatcherServlet
拦截到请求后将根据请求将请求的信息(包括 URL、HTTP 方法、请求报问头、请求参数、Cookie
等)及HandlerMapping
(处理器映射器)的配置找到处理请求的Handler
(处理器) - ③ 当
DispatcherServlet
根据HandlerMapping
得到对应当前请求的Handler
后,通过HandlerAdaper
(处理器适配器)对Handler
进行封装,再以统一的适配器接口调用Handler
- ④
HandlerAdaper
调用的Handler
(处理器),即程序中编写的 Controller 类,亦被称之为后端控制器 - ⑤
Handler
(Conroller
)执行完成后,会返回一个ModelAndView
对象给DispatcherServlet
,该对象中会包含视图逻辑名或包含模型数据信息和视图逻辑名 - ⑥
DispatcherServlet
会根据ModelAndView
对象选择一个合适的ViewReslover
(视图解析器) - ⑦
ViewReslover
解析后,会向DispatcherServlet
中返回具体的View
(视图) - ⑧
DispatcherServlet
对View
进行渲染(即将模型数据填充至视图中) - ⑨ 视图渲染结果会返回给客户浏览器显示,最终用户得到的可能是一个简单的
html
页面,也可能是一张图片或一个 PDF 文档等不同的媒体形式
工作原理
要了解 Spring MVC 框架的工作机理,须回答下面 3 个问题:
- ①
DispatcherServlet
是如何截获特定的 HTTP 请求并交由 Spring MVC 框架处理的呢? - ② 位于 Web(表示)层的 Spirng MVC 子容器(
WebApplicationContext
)如何与位于Service(业务)层的 Spring 父容器(ApplicationContext
)建立关联,以使 Web 层的 Bean 可以调用 Service 层的 Bean? - ③ 如何初始化 Spring MVC 的各个组件,并将它们装配到
DispatcherServlet
中?
DispatcherServlet(前端控制器)
DispatcherServlet
是 Spring MVC 的“灵魂”和“心脏”,它负责接受 HTTP 请求并协调 Spring MVC 的各个组件完成请求处理的工作。
和任何Servlet
的配置一样,可以通过<servlet-mapping>
指定其处理的 URL,所以用户必须在web.xml
中配置好DispatcherServlet
。
1 | <!--声明 Servlet 容器监听器--> |
首先,我们注册了一个ContextLoaderListener
,它是一个ServletContextListener
(即Servlet
事件监听器:专门用于监听 Web 应用程序中ServletContext
、HttpSession
和ServletRequest
等域对象的创建和销毁过程,当监听这些域对象属性的修改或感知绑定到HttpSession
域中的某个对象的状态改变时,监听器中的某个方法将立即被执行)
另外,ContextLoaderListener
加载时会查找名为contextConfigLocation
的参数,该参数在<context-param>
中配置。
由于在<context-param>
参数里通过contextConfigLocation
参数指定了 Service 层 Spring 容器的配置文件(此处加载classpath
类路径下的spring目录下的spring-service.xml
文件),所以将首先启动“ Service 层”的 Spring 容器。
上述流程,简而言之:Servlet 容器启动–> 被监听器监听到 –> 装载 Spirng 配置文件–> WebApplicationContext
对象初始化并将其赋值给 ServletContext
的WebApplicationContext.ROOT
属性 –> 通过WebApplicationContext
访问 Bean
DispatcherServlet主流程
在有篇文章详细讲过,我们继续向下解释代码。
问题1答案:
在Servlet
中,我们配置了名为dispatcher
的DispatcherServlet
,它通过contextConfigLocation
参数指定加载WEB-INF
目录下的spring-web.xml
文件,将启动“ Web 层”的 Spring MVC 容器。通过映射处理接受到的所有 HTTP 请求,即所有的 HTTP 请求都会被DispatcherServlet
拦截并处理。
注意哦
:DispatcherServlet
在初始化的过程中,会建立一个自己的 IoC 容器上下文Servlet WebApplicationContext
,会以ContextLoaderListener
建立的Root WebApplicationContext
作为自己的父级上下文。DispatcherServlet
持有的上下文默认的实现类是XmlWebApplicationContext
。(上下文理解为 Spring 容器即可)
Spring容器有父子之分,这样可以实现更好的解耦。父容器对子容器可见,子容器对父容器不可见(举个栗子:Service 层的 Bean 可以在 Controller 层中注入,反之则不行)。
目前最常见的一种场景就是在一个项目中引入 Spring 和 Spring MVC 这两个框架,那么它其实就是两个容器,Spring 是父容器,Spring MVC 是其子容器。
问题2答案:
所以,“ Web 层”的 Spring MVC 容器可以引用“ Service 层” Spring 容器的 Bean,而“ Service 层” Spring 容器却访问不到“ Web 层”的 Spring MVC 容器的 Bean。
现在剩下最后一个问题
:Spring 如何将上下文中的 Spring MVC 组件装配到DispatcherServlet
中?
我们查看DispatcherServlet
的initStrategies()
方法的代码,就知道了:
1 | /** |
initStrategies()
方法将在WebApplicationContext
初始化后自动执行,此时 Spring 容器的 Bean 已经初始化完成。该方法的工作原理是:通过反射机制查找并装配 Spring 容器中用户显式自定义的组件 Bean,如果找不到,则装配DispatcherServlet.properties
文件中默认的组件实例,如本地化解析器,主题解析器,处理器映射等等。
简单来说,当DispatcherServlet
初始化后,就会自动扫描 Spring 容器中的 Bean ,根据名称或类型匹配的机制查找自定义的组件 Bean,找不到则使用默认组件。
快速入门
在学习了 Spring MVC 框架的整体机构后,下面通过一个简单的例子讲解 Spring MVC 开发的基本步骤:
(1)配置 web.xml,定义 DispatcherServlet,截获特定的 URL 请求,指定业务层对应的 Spring配置文件。
1 | <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" |
(2)编写处理请求的控制器(处理器):
创建控制器类UserController
,该类需要实现Controller 接口
。
1 | import javax.servlet.http.HttpServletRequest; |
(3)编写视图对象
该 Jsp 页面位于Web根目录
下views
目录下的user
目录下。
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %> |
(4)配置 SpringMVC 的配置文件,使控制器、视图解析器等生效。
1 |
|
注意
:在老版本 Spring 中,必须配置处理器映射器
、处理器适配器
和视图解析器
;但 Spring4.0 后简化了配置,这些可以省略,只配置处理器即可,其他 Spring 内部自动管理。更改如下:
1 |
|
5.启动 tomcat 访问浏览器
简述一下这个流程:
① 在浏览器输入url
,DispatcherServlet
接收到映射"/user"
的请求。
② DispatcherServlet
使用处理器映射器查找负责处理该请求的处理器。
③ DispatcherServlet
将请求分发给我们定义的名为UserController
的处理器。
④ 处理器完成业务处理后,返回ModelAndView
对象,其中 View 的逻辑名为/views/user/rsSuccess.jsp
。该模型包含一个键为“user”
的 Object 对象。
⑤ DispatcherServlet
调用InternalResourceViewResolver
组件对ModelAndView
中的逻辑视图名进行解析,得到真实的/views/user/rsSuccess.jsp
视图对象。
⑥ DispatcherServlet
使用/views/user/rsSuccess.jsp
对模型中的对象进行渲染;
⑦ 返回响应页面给客户端。
Spring MVC 注解
Spring 拥有它特有的注解,下面一一介绍。
Spring 注解的类型
Spring MVC 有几个常用的注解,分别是:
@Controller
:用于指示 Spring 类的实例是一个控制器,该注解在使用时不需要再实现 Controller 接口,只需将该注解加到控制器类上,然后通过<context:component-scan/>
扫描相应类包即可。该注解一般和@RequestMapping
一起使用,因为它需要指定控制器内部对每一个请求是如何处理的。@RequestMapping
:一般和@Controller
一起使用,该注解包括以下属性:value
:默认属性,用于映射一个请求或一个方法,使用时可以标注在一个方法或一个类上。method
:用于指定该方法用于处理哪种类型的请求方式,包括 GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE- 其他属性略
@RequestBody
:格式转换注解,该注解用在方法的形参上,用于将请求体中的数据绑定到方法的形参中。@ResponseBody
:格式转换注解,该注解用在方法上,用于直接返回return
对象。
Spirng 4.3 还引入了新的注解,继续用来简化代码量:
@GetMapping
:匹配 GET 方式的请求@PostMapping
:匹配 POST 方式的请求@PutMapping
:匹配 PUT 方式的请求@DeleteMapping
:匹配 DELETE 方式的请求@PatchMapping
:匹配 PATCH 方式的请求
比如传统的@RequestMapping
注解使用方式如下(value
为默认属性,可以不写):
1 | "/user/{id}",method=RequestMethod.GET) (value= |
而使用@GetMapping
注解后的简化代码如下:
1 | "/user/{id}") (value= |
URL 中的{xxx}
占位符可以通过@PathVariable("xxx")
注解绑定到操作方法的入参中,也可以绑定到类前。
使用注解的 Spring MVC 程序
前面我们知道了使用注解可以在开发时简化代码的数量,我们将在原有例子加强部分以使用注解的方式,来再次加深我们对 Spring MVC 执行流程的理解。
(1)配置web.xml
,定义DispatcherServlet
,截获特定的 URL 请求,指定业务层对应的Spring 配置文件。注
:前面配置过故此处省略。
(2)编写处理请求的控制器(处理器):
Spring MVC 通过@Controller
注解即可将一个对象转化为处理请求的控制器,通过@RequestMapping
注解为控制器指定处理哪些 URL 的请求。
下面定义了UserController
是一个负责用户处理的控制器,实现了Controller
接口(这里我们加了@Controller
即代表实现了 Contrller 接口)
1 | import org.springframework.stereotype.Controller; |
首先使用@Controller
注解对UserController
类进行标注,使其成为一个可处理 HTTP 请求的控制器(使用该注解后需要在配置文件中扫描该 Bean,相当于前面设置的控制器)。
其次使用@RequestMapping
注解对UserController
类(相当于前面在配置处理器映射的请求)及其register()
方法进行标注,确定register()
对应的请求 URL。
register()
方法返回一个字符串"register"
,它代表一个逻辑视图名,将由解析器解析为一个具体的视图)。在本例中,它将被映射为 Web 容器根路径下的/views/user/register.jsp;
为什么设置为register
可以映射为这种路径呢?
这是因为后面的视图解析器设置了前缀为/views/user/
,后缀为.jsp
(3)编写视图对象
我们使用一个register.jsp
作为用户的注册页面,UserController
类的register()
方法处理完成后,将转向这个register.jsp
页面,其位于 Web 容器根路径的 user 目录下,代码如下:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
register.jsp
非常简单,包括一个表单,提交到/user
进行处理,下面,我们在UserController
类中加入一些新的代码:
1 |
|
createUser()
方法处的@PostMapping
注解让该方法处理 URI 为/user
且请求方法为POST
的请求。由于 Spring MVC 会自动将表单中的数据按参数名和 User 实体中属性名匹配的方式进行绑定(数据绑定中介绍),所以将参数值会被填充到User
的相应属性中。而在方法内的代码,设置了逻辑视图名为rsSuccess
,最后将user
作为模型数据暴露给视图对象。
User
对象代码如下(属性和表单 name 对应):
1 | public class User { |
视图解析器将rsSuccess
解析为rsSuccess.jsp
的视图对象,rsSuccess.jsp
可以访问到模型中的数据,页面代码如下:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %> |
4.在配置文件开启 Spring 的包扫描功能:
1 |
|
5.运行 Tomcat 并测试
再次简述一下流程:
- ①
DispatcherServlet
接收到客户端的/user
请求 - ②
DispatcherServlet
使用DefaultAnnotationHandlerMapping
查找负责处理该请求的处理器 - ③
DispatcherServlet
将请求分发给名为/user
的UserController
处理器 - ④ 处理器完成业务员处理后,返回
ModelAndView
对象,其中View
的逻辑名为/user/createSuccess
,而模型包含一个键为user
的User
对象 - ⑤
DispatcherServlet
调用InternalResoureViewResolver
组件对ModelAndView
中的逻辑视图名进行解析,得到真实的views/user/rsSuccess.jsp
视图对象 - ⑥
DispatcherServlet
使用views/user/rsSuccess.jsp
对模型中的user
模型对象进行渲染 - ⑦ 返回响应页面给客户端
注意:@RequestMapping注解
上的映射和你return
请求的文件路径有可能不是一致的,都是你自己设置的。
消息与对象间的转换
HttpMessageConverter<T> 接口?
HttpMessageConverter<T>
是 Spring 十分重要的一个接口,它负责将请求信息转换为一个对象(类型为 T),也可以将对象(类型为 T)输出为响应信息。
DispacherServlet
默认将RequestMappingHandlerAdapter
作为HandlerAdapter
的组件实现类,HttpMessageConverter
即由RequestMappingHandlerAdapter
使用,将请求信息转换为对象,或将对象转换为响应信息。
HttpMessageConverter<T> 实现类
Spring 为HttpMessageConverter<T>
提供了众多的实现类,它们共同组成了一个功能强大的、用途广泛的信息对象转换家族,比如:
StringHttpMessageConverter
:可以将请求信息转换为字符串ByteArrayHttpMessageConverter
:可以读/写二进制数据SourceHttpMessageConverter
:可以读/写javax.xml.transform.Source
类型的数据MappingJackson2HttpMessageConverter
:可以利用 Jackson 开源类包的ObjectMapper
读/写 JSON 数据。
在 Spring 中,RequestMappingHandlerAdapter
已经默认配置了以下转换器:
StringHttpMessageConverter
ByteArrayHttpMessageConverter
SourceHttpMessageConverter
FormHttpMessageConverter
如果需要装配其他类型的 Http 消息转换器(比如十分常见的 JSON 格式转换器),可以在 Spring 的 Web 容器上下文中自定义一个RequestMappingHandlerAdapter
配置,当然其配置有点繁琐。
因此一般直接通过在 Spring 配置文件中加入<mvc:annotation-driven/>
标签来实现 JSON 格式转换器的装配:1
<mvc:annotation-driven/>
这个标签非常神奇,它的实现类为org.springframework.web.servlet.config
包下的AnnotationDrivenBeanDefinitionParser
,简单来讲,它为 <annotation-driven/>
MVC 名称空间元素提供以下配置:
- 注册以下
HandlerMappings
(多种映射器):RequestMappingHandlerMapping
的排序为 0,用于将请求映射到带@RequestMapping
注释的控制器方法BeanNameUrlHandlerMapping
在排序为 2,以将 URL 路径映射到控制器 bean 名称
- 注册以下
HandlerAdapters
(多种适配器):RequestMappingHandlerAdapter
用于使用带 @RequestMapping 注解的控制器方法处理请求HttpRequestHandlerAdapter
用于使用 HttpRequestHandlers 处理请求SimpleControllerHandlerAdapter
用于使用基于接口的控制器处理请求
- 注册以下
HandlerExceptionResolvers
(多种异常处理解析器):ExceptionHandlerExceptionResolver
用于通过org.springframework.web.bind.annotation.ExceptionHandler
方法处理异常ResponseStatusExceptionResolver
用于使用org.springframework.web.bind.annotation.ResponseStatus
注释的异常DefaultHandlerExceptionResolver
用于解析已知的 Spring 异常类型
- 其他:
- 注册
org.springframework.util.AntPathMatcher
和org.springframework.web.util.UrlPathHelper
以供RequestMappingHandlerMapping
、ViewControllers
的HandlerMapping
和HandlerMapping
服务资源所使用
- 注册
对于 JSR-303 实现,会检测 javax.validation.Validator
路径是否有效,有效则会帮我们创建对应的实现类并注入。
最后帮我们检测一些列 HttpMessageConverter
的实现类们,这些主要是用作直接对请求体里面解析出来的数据进行转换。俗称 HTTP 消息转换器,与参数转换器不一样。
在 SpringMVC 5.1.1 中有以下几个检测:
除了会帮我们注入以上检测有效的 HTTP 消息转换器外,还会帮我们注入SpringMVC 自带的几个 HTTP 消息转换器,上面检测的转换器是由上到下顺序加入的,也就是说解析的时候会根据 ContentType
从上到下找合适的。
如何使用 HttpMessageConverter<T>?
我们可以通过@RequestBody
和@ResponseBody
注解来使用它,前面提到过它们的作用,我们通过一个例子来解释:
1 | import ... |
我们在前面已经通过<mvc:annotation-driven/>
标签为RequestMappingHandlerAdapter
在注册了若干个HttpMessageConverter
。handler()
方法的requestBody
入参标注了一个@RequestBody注解
,Spring MVC 将根据requestBody
的类型查找匹配的HttpMessageConverter
。
由于StringHttpMessageConverter
的泛型类型对应String
,所以StringHttpMessageConverter
将被 Spring MVC 选中,用它将请求体信息进行转换并将结果绑定到requestBody
入参中。
在handleImg()
方法中标注了一个@ResponseBody
注解。由于返回值类型为byte[]
,所以 Spring MVC 根据类型匹配的查找规则将使用ByteArrayHttpMessageConver
对返回值进行处理,将图片数据流输出到客户端。
在handleJson()
方法中标注了一个@ResponseBody
注解。由于返回值为String
,所以 Spring MVC 根据类型匹配的查找规则将使用MappingJackson2HttpMessageConverter
对返回值进行处理,将map
以json
格式返回到客户端。(注意
:要使用这种类型的转换,需要jackson-databind
转换的数据绑定包),返回结果如下:
数据绑定
在执行程序时,Spring MVC 会根据客户端请求参数的不同,将请求消息中的消息以一定的方式转换并绑定到控制器类的方法参数中。这种将请求消息数据与后台方法参数建立连接的过程,即为 Spring MVC 中的数据绑定。
工作流程
Spring MVC 通过反射机制对目标处理方法的签名进行分析,将请求消息绑定到处理方法的入参中,这样后台方法就可以正确绑定并获取客户端请求携带的参数了。
数据绑定的核心组件是DataBinder
,其运行流程如下图:
各步骤详细说明如下:
- ① Spring MVC 会将
ServletRequet
对象和处理方法的入参对象传递给DataBinder
; - ②
DataBinder
调用ConversionService
组件进行数据类型转换、数据格式化等工作,并将ServeltRequest
对象中的消息填充到参数对象中; - ③ 调用
Validator
组件对已经绑定了请求消息数据的参数对象进行数据合法性校验; - ④ 校验完成后会生成数据绑定结果
BindingResulet
对象,Spring MVC 会将BindingResult
对象中的内容赋给处理方法的相应参数。
请求消息处理方法入参
在控制器类中,通过@RequestMapping
注解将请求引导到处理方法上,使用合适的方法签名将参数绑定到入参中。
每一个请求处理方法都可以有多个不同类型的参数,我们前面就使用过自定义的User
类,除此之外,还可以使用HttpServletReqeust、HttpServletResponse、HttpSession、InputStream、Reader
等等类型,参数绑定我们等等介绍。
此处需要注意的是,有时候参数名可能和前端页面的参数名不一致,或者由于 Java 的类反射对象不记录方法入参的名称,这时需要使用@RequestParam
注解指定其对应的请求参数,该注解有几个常用属性:
value
:默认属性,指的是入参的请求参数名字。required
:是否必须,默认为true
,表示请求中必须含有该参数,否则抛出异常。
1 | "/userName") ( |
类型
默认数据类型绑定
当前端请求的参数比较简单时,可以在后台方法的形参中直接使用Spring MVC 提供的默认参数类型进行数据绑定。
HttpServletRequest
:通过 request 对象获取请求信息。HttpServletResponse
:通过 response 对象响应信息。HttpSession
:通过 session对象得到 session 中存放的对象。Model/ModelMap
:Model 是一个接口,ModelMap 是一个接口实现,作用是将 model 数据填充到 request 域。
举个例子,需要使用 HttpServletRequest 相关参数时:
1 | "/userName") ( |
基本数据类型绑定
指 Java 中几种基本数据类型及简单引用类型的绑定,如 int、String、Double 等类型。
以前面注册用户的例子来说,提交表单后可以获取到用户的 userName,那么可以这样写:
1 | "/userName") ( |
注意:
形参名必须要和表单的name属性一致,如果不一致,需要使用@RequestParme注解
,前面已经解释过。
数组类型绑定
在实际开发中,可能会遇到前端请求需要传递到后台一个或多个相同名称参数的情况(如批量删除操作),此种情况不适合基本数据类型绑定。
但是,若将所有同种类型的请求参数封装到一个数组中,后台就可以进行绑定接收了。
举个批量删除的例子:
① 模拟批量删除页面和删除成功页面
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
② 在控制器配置映射路径
1 | // 根据映射跳转到批量删除页面 |
③测试结果
集合类型绑定
指 List、Set、Map 这些集合类型的绑定。
在批量删除用户的操作中,前端请求传递的都是同名参数的用户 id,只要在后台使用同一种数组类型的参数绑定接收,就可以在方法中通过循环数组参数的方式来完成删除操作。
但如果是批量修改用户操作的话,前端请求传递过来的数据可能就会批量包含各种类型的数据,如 Integer,String 等等。
这种情况我们就可以使用集合数据绑定,即在包装类中定义一个包含用户信息类的集合,然后在接受方法中将参数类型定义为该包装类的集合。
我们通过一个批量修改用户的例子来了解一下:
① 定义一个 UserVo 类
1 | public class UserVo { |
② 模拟批量修改页面和修改成功页面
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
③ 在控制器配置映射路径
1 | // 根据映射跳转到批量修改页面 |
④ 测试结果
POJO 对象/包装的 POJO 数据类型绑定
当客户端请求传递多个不同类型的参数时,使用基本类型绑定显然是不合适的,如注册表单。
此时我们就可以定义一个 POJO 类来进行数据绑定,即将所有关联的请求参数封装到一个 POJO 对象中,然后在方法中直接使用该 POJO 作为形参来完成数据绑定。
例子在前面已经举过,就是那个User
类。
假设有个需求为用户查询订单,其中页面传递的参数可能包括:订单编号、用户名称等信息,这就包含了订单和用户连个对象的信息。
此时使用 POJO 对象绑定则订单和用户信息混合封装,显得比较混乱,那么我们就可以考虑使用包装的 POJO 类型绑定。即在一个 POJO 中包含另一个简单的 POJO,如在订单对象中包含用户对象。
自定义数据类型绑定
大部分类型使用前面各种的数据类型绑定都能满足需求,然而有些特殊类型的参数是无法在后台进行直接转换的(如日期数据),也就无法直接进行数据绑定,因此它必须先经过数据转换。
针对这种特殊的数据类型,就需要开发者使用Converter
(自定义转换器)或Formatter
(格式化)来进行数据绑定,我们在下面将会实现它。
数据类型转换
前面提到过HttpMessageConvert<T>
,它用于将请求信息转换为一个对象(类型为 T)或将对象(类型为 T)输出为响应信息,是消息与对象之间的转换。
但是,下面的ConversionService
却是对象之间的转换。
ConversionService
是 Spring 类型转换的核心接口,它位于org.springframework.core.convert
中,也是该包中的唯一一个接口。
我们可以利用org.springframework.context.support.ConversionServiceFactoryBean
在 Spring 的上下文中定义一个ConversionService
,它会被自动识别,并在 Bean 属性配置及 Spring MVC 处理方法入参绑定等场合使用它进行数据转换,代码如下:
1 | <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/> |
该FactoryBean
创建ConversionService
内置了很多转换器,可完成大多数 Java 类型的转换工作。除了包括将 String 对象转换为各种基础类型的对象外,还包括 String、Number、Array、Collection、Map、Properties及 Object 之间的转换器。当然我们也可以自定义转换器。
Spring 在org.springframework.core.convert.conveerter
包中定义了 3 种类型转换器,其中提供了一个比较重要的Converter
接口的转换器用来进行类型转换,接口定义如下:
1 | package org.springframework.core.convert.converter; |
T convert(S source);
负责将 S 类型的对象转换为 T 类型的对象,下面我们通过日期类型转换的例子来具体了解:
1.创建实现了 Converter 接口的转换器:
1 | import org.springframework.core.convert.converter.Converter; |
2.在配置文件中加入以下配置:
1 | <!--显式的注入自定义类型转换器覆盖默认实现--> |
我们使用了mvc命名空间
的 <mvc:annotation-driven/>
标签,该标签可简化 Spring MVC 的相关配置。在默认情况下,该标签会创建并注册一个默认的DefaultAnnotaationHandlerMapping
和一个RequestMappingHandlerAdapter
实例。如果上下文中存在自定义的对应组件 Bean,则 Spring 会自动利用自定义的 Bean 覆盖默认的Bean。
除此之外,<mvc:annotation-driven/>
标签还会注册一个默认的ConversionService
,即FormattingConversionServiceFactoryBean
,以满足大多数类型转换的需求。现在由于要注册一个自定义的DateConvert
,因此,需要显式定义一个ConversionService
覆盖<mvc:annotation-driven/>
中的默认实现,这是通过<mvc:annotation-driven/>
的conversion-service
属性来完成的。
在装配好DateConvert
后,就可以在任何控制器的处理方法中使用这个转换器了。
3.定义控制器测试自定义转换器:
1 |
|
4.测试结果
数据格式化(本质还是数据转换)
Spring 使用转换器来进行源类型对象到目标类型对象的转换,但是转换器并不提供输入及输出信息格式化的工作。
如果需要转换的源类型数据(一般是字符串)是从客户端界面中传递过来的,为了方便使用者观看,这些数据往往具有一定的格式,比如日期、时间、数字、货币等数据都是具有一定格式的。
在不同的本地化环境中,同一类型的数据还有相应地呈现不同的显示格式。
如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据,是 Spring 格式化框架要解决的问题,该格式框架最重要的接口为Formatter<T>
接口,定义如下:
1 | package org.springframework.format; |
该接口扩展了 2 个接口Printer<T>
和Parser<T>
接口:
1 | package org.springframework.format; |
Printer<T>
负责对象的格式化输出,而Parser<T>
负责对象的格式化输入,在接口中各定义了一个方法,print()
接口方法将类型为 T 的成员对象根据本地化的不同输出为不同的格式化的字符串;parse()
接口方法参考本地化信息将一个格式化的字符串转换为T类型的对象,即完成格式化对象的输入工作。
我们以Formatter接口
再写一个日期转换的例子:
1.创建实现了 Formatter 接口的转换器:
1 | import org.springframework.format.Formatter; |
2.在配置文件中加入以下配置:
1 | <!--显式的注入自定义类型转换器--> |
3.定义控制器测试自定义转换器:
1 |
|
4.测试结果
补充:解决中文乱码
有时候如果我们输入中文字符,会出现乱码问题,这个问题很好解决,我么只需要在web.xml
配置一个Spring 的字符编码过滤器即可解决这个问题,代码如下:
1 | <!-- 配置spring的字符编码过滤器,保证request请求的中文字符不会乱码(注意这个过滤器要放到最前面) --> |
模型数据处理
对于 MVC 框架来说,模型数据是最重要的,因为控制C
是为了产生模型数据M
,而视图V
则是为了渲染模型数据。
请求处理方法执行完成后,最终返回一个ModelAndView
对象,对于那些返回String
,View
或ModelMap
等类型的处理方法,Spring MVC 也会在内部将它们装配成一个ModelAndView
对象,该对象包含了视图逻辑名和模型对象的信息。
Spring MVC 支持的方法返回类型常见的就 3 个,分别为:
String
:仅代表一个逻辑视图名,可以跳转视图,但不能携带数据。ModelAndView
:包含模型和逻辑视图名,可以添加 Model 数据,并指定视图void
:在异步请求时使用,它只返回数据,而不会跳转视图。
由于ModelAndView
类型未能实现数据与视图之间的解耦,所以在企业开发时,方法的返回类型通常都会使用String
。
问:
既然String
类型的返回值不能携带参数,那么在方法中是如何将数据带入视图页面的呢?
答:
通过Model
参数类型,即可添加需要在视图中显示的属性,代码如下:
1 | "/user") ( |
而且String的作用不仅仅如此,它还可以进行重定向与请求转发:
① forward 请求转发
例如,用户登录失败后,需要携带消息转发到用户登录页面,代码如下:
1 |
|
② redirect 重定向
例如,在用户修改用户信息操作后,将请求重定向到用于查询用户信息的界面,代码如下:
1 |
|
注意哦
:redirect
后接/
代表其会以当前 Tomcat 虚拟目录做前缀,假设发布虚拟目录为 shop
,则返回的 URL 为:http://ip:8080/shop/user/query
。
若redirect
后不加/
,变为:1
return "redirect:user/query";
此时会以Controller
类的@RequestMapper
作为前缀,返回的 URL 为:http://ip:8080/shop/user/user/query
,此时肯定无此路径,这时可以这么写:1
return "redirect:query";
注意哦
:forward
和redirect
后的字符都将作为 URL 处理,前者与当前请求属于同一个请求,后者会发起一个新的请求。
视图解析器
视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。
视图对象可以是 JSP,还可以是 Excel、PDF、XML、JSON 等各种形式的视图。
Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),视图解析器有多种,不过所有视图解析器都实现了ViewResolver接口
,我们前面就定义过这样一段代码:
1 | <!--定义视图解析器,解析视图逻辑名--> |
上面一段代码,定义了一个视图解析器(InternalResourceViewResolver
),并设置了视图的前缀和后缀属性。
为什么设置视图的前缀和后缀属性?
主要是为了减少代码量。例如,案例中的逻辑视图名只需要设置为"register"
,而不再需要设置为"/views/user/register.jsp"
,这样在后续请求中视图解析器会自动的增加前缀和后缀。
静态资源处理
如果将DispatcherServlet
请求映射配置为/
,则 Spring MVC 将捕获 Web 容器所有的请求,包括静态资源的请求,Spring MVC 会将它们当成一个普通请求来处理,因找不到对应的处理器而导致错误。
如何让 Spring 框架能够捕获所有 URL 请求,同时又将静态资源的请求转由 Web 容器处理,是可将DispatcherServlet
的请求映射配置为/
的前提,有两个解决方案
采用<mvc:default-servlet-handler/>
在 Spring 配置文件中配置<mvc:default-servlet-handler/>
后,会在 Spring MVC 上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler
,它将充当一个检查员的角色,对进入DispatcherServlet
的 URL 进行筛选,如果发现是静态资源的请求,就将该请求转由 Web 应用服务器默认的Servlet
处理,如果不是静态资源的请求,则由DispatcherServlet
继续处理。
一般 Web 应用服务器(Tomcat、Jetty、JBoss 等等)默认的Servlet
名称都是default
,因此DefaultServletHttpRequestHandler
可以找到它。
<mvc:resources/>
<mvc:default-servlet-handler/>
将静态资源的处理经由 Spring MVC 框架交回 Web 应用服务器,而<mvc:resources/>
则更进一步,由 Spring MVC 框架自己处理静态资源,并添加一些有用的附加功能。
首先,它可以运行静态资源放置在任何地方,如WEB-INF
,类路径下等等,甚至可以将 JavaScript 等静态文件打包到 JAR 中。其location
属性指定静态资源的位置,而mapping
属性指定映射的文件。
其次,它可以对静态资源提供优化,比如通过ache-period
属性设置静态资源在客户端浏览器中的缓存有效时间。
拦截器(Interceptor)
Spring MVC 中的拦截器(Interceptor
)类似于 Servlet 中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。
例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
拦截器的定义和配置
要使用 Spring MVC 中的拦截器,就需要对拦截器类进行定义和配置。
可以通过下面两种方式定义拦截器:
- 实现
HandlerInterceptor
接口或继承该接口的实现类(如HandlerInterceptorAdapter
) - 实现
WebRequestInterceptor
接口或继承该接口的实现类
以实现HandlerInterceptor
接口方式为例,自定义拦截器类的代码如下:
1 | import org.springframework.web.servlet.HandlerInterceptor; |
可以看到,拦截器重写了 3 个方法:
boolean preHandle()
:请求到达HandlerAdapter
之前执行该方法,返回false
时请求直接返回,不会向下传递void postHandle()
:请求被HandlerAdapter
执行后执行该方法void afterCompletion()
:在响应被渲染后执行该方法
要使自定义的拦截器生效,还需要在 Spring MVC 的配置文件中配置具体的拦截路径,代码如下:
1 | <mvc:interceptors> |
注意哦
:可以配置过个拦截器,每个拦截器都可以指定一个匹配的映射路径,以限制拦截器的作用范围
过滤器 VS 拦截器
核心区别在于 Filter 基于 Servlet ,而 Interceptor 基于 Spring MVC,因此:
- 它们的运行顺序不同:Filter –> Servlet –> Interceptor –> Controller
- 配置方式不同,Filter 在
web.xml
配置,而 Interceptor 在 Spring 的配置文件或注解 配置 - Filter 不依赖 Servlet 容器,而 Interceptor 依赖 Servlet 容器
- 处理对象不同,Filter 只能处理 request 和 response 对象,而 Interceptor 基于 Spring MVC 可以处理 Request、Response、Handler、Exception、ModerAndView
文件上传
一般情况下,多数文件上传都是通过表单形式提交给后台服务器的。因此,要实现文件上传功能,就需要提供一个文件上传的表单,而该表单必须满足下面3个条件:
form
表单的method
属性设置为post
;form
表单的enctype
属性设置为multipart/form-data
;- 提供
<input type="file" name="filename"
的文件上传输入框。
1 | <form method="post" enctype="multipart/form-data"> |
当form
表单的enctype
属性为multiple/form-data
时,浏览器就会采用二进制流来处理表单数据,服务器端就会对文件上传的请求进行解析处理。
Spring MVC 中文件上传
Spring MVC 为文件上传提供了直接支持,这种支持是通过即插即用的MultipartResolver
实现的。Spring 内部使用Apache Commons FileUpload
技术实现了一个MultipartResolver
实现类:CommonsMultipartResolver
,需要依赖包commons-fileupload
和commons-io
。
在 Spring MVC 上下文中默认没有装配MultipartResolver
,因此默认情况下不能处理文件的上传工作。如果像使用Spring的文件上传功能,则需要先在上下文中配置。
1.配置 MultipartResolver
1 | <!--文件上传--> |
CommonsMultipartResolver
的属性还有许多,如maxInMemorySize
缓存中的最大尺寸 和resolveLazily
可以推迟文件解析,以便在Controller
中捕获文件大小异常。
2.编写控制器类
1 | import org.springframework.stereotype.Controller; |
Spring MVC 会将上传文件绑定到MultipartFile
提供了获取上传文件内容、文件名等方法,通过其transferTo()
方法还可将文件存储到硬件中,具体说明如下:
String getOriginalFilename()
:获取上传文件的原名。boolean isEmpty()
:判断是否有上传的文件。void transferTo(File dest)
:可以使用该方法将上传文件保存到一个指定目标文件中。byte[] getBytes()
:获取文件数据。String getContentType()
:获取文件 MIME 类型,如 image/pjpeg、text/plain 等。InputStream getInputStream()
:获取文件流。String getName()
:获取表单中文件组件的名字。long getSize()
:获取文件的字节大小,单位为 Byte。
3.编写文件上传表单页面
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
4.测试
参考
- Spring MVC 官方文档
- 陈雄华 林开雄 文建国. 精通 Spring 4.x 企业应用开发 [M]. 电子工业出版社,2017
文章信息
时间 | 说明 |
---|---|
2019-03-22 | 初稿 |