MVC 模式
MVC 是 Model、View 和 Controller 的缩写,分别代表 Web 应用程序中的 3 种职责。
- 模型:用于存储数据以及处理用户请求的业务逻辑。
- 视图:向控制器提交数据,显示模型中的数据。
- 控制器:根据视图提出的请求判断将请求和数据交给哪个模型处理,将处理后的有关结果交给哪个视图更新显示。
基于 Servlet 的 MVC 模式的具体实现如下:
- 模型:一个或多个 JavaBean 对象,用于存储数据(实体模型,由 JavaBean 类创建)和处理业务逻辑(业务模型,由一般的 Java 类创建)。
- 视图:一个或多个 JSP 页面,向控制器提交数据和为模型提供数据显示,JSP 页面主要使用 HTML 标记和 JavaBean 标记来显示数据。
- 控制器:一个或多个 Servlet 对象,根据视图提交的请求进行控制,即将请求转发给处理业务逻辑的 JavaBean,并将处理结果存放到实体模型 JavaBean 中,输出给视图显示。
基于 Servlet 的 MVC 模式的流程如图所示:
Spring MVC 处理用户请求流程
Spring MVC 框架是高度可配置的,包含多种视图技术,例如 JSP 技术、Velocity、Tiles、iText 和 POI。
Spring MVC 框架并不关心使用的视图技术,也不会强迫开发者只使用 JSP 技术,但教程中使用的视图是 JSP,本节主要介绍 Spring MVC 框架处理用户请求的完整流程和处理中包含的 4 个接口。
Spring MVC 工作流程
Spring MVC 框架主要由 DispatcherServlet
、处理器映射、控制器、视图解析器、视图组成,其工作原理如图所示。
从图可总结出 Spring MVC 的工作流程如下:
- 客户端请求提交到 DispatcherServlet。
- 由 DispatcherServlet 控制器寻找一个或多个 HandlerMapping,找到处理请求的 Controller。
- DispatcherServlet 将请求提交到 Controller。
- Controller 调用业务逻辑处理后返回 ModelAndView。
- DispatcherServlet 寻找一个或多个 ViewResolver 视图解析器,找到 ModelAndView 指定的视图。
- 视图负责将结果显示到客户端。
Spring MVC 接口
在图中包含 4 个 Spring MVC 接口,即 DispatcherServlet
、HandlerMapping
、Controller
和 ViewResolver
。
Spring MVC 所有的请求都经过 DispatcherServlet
来统一分发,在 DispatcherServlet
将请求分发给 Controller
之前需要借助 Spring MVC 提供的 HandlerMapping
定位到具体的 Controller
。
HandlerMapping
接口负责完成客户请求到 Controller
映射。
Controller
接口将处理用户请求,这和 Java Servlet 扮演的角色是一致的。一旦 Controller
处理完用户请求,将返回 ModelAndView
对象给 DispatcherServlet
前端控制器,ModelAndView
中包含了模型(Model)和视图(View)。
从宏观角度考虑,DispatcherServlet
是整个 Web 应用的控制器;从微观考虑,Controller
是单个 Http 请求处理过程中的控制器,而 ModelAndView
是 Http 请求过程中返回的模型(Model)和视图(View)。
ViewResolver
接口(视图解析器)在 Web 应用中负责查找 View
对象,从而将相应结果渲染给客户。
Spring MVC 视图解析器
Spring 视图解析器是 Spring MVC 中的重要组成部分,用户可以在配置文件中定义 Spring MVC 的一个视图解析器(ViewResolver),示例代码如下:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" >
<!--前缀-->
<property name="prefix" value="/WEB-INF/jsp/"/>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>
上述视图解析器配置了前缀和后缀两个属性,因此 RegisterController
和 LoginController
控制器类的视图路径仅需提供 register
和 login
,视图解析器将会自动添加前缀和后缀。
InternalResourceViewResolver
是 URLBasedViewResolver
的子类,所以 URLBasedViewResolver
支持的特性它都支持。在实际应用中 InternalResourceViewResolver
也是使用的最广泛的一个视图解析器。
单从字面意思来看,我们可以把 InternalResourceViewResolver
解释为内部资源视图解析器,这就是 InternalResourceViewResolver
的一个特性。InternalResourceViewResolver
会把返回的视图名称都解析为 InternalResourceView
对象,InternalResourceView
会把 Controller
处理器方法返回的模型属性都存放到对应的 request
属性中,然后通过 RequestDispatcher
在服务器端把请求 forword 重定向到目标 URL。
比如在 InternalResourceViewResolver
中定义了 prefix=/WEB-INF/
,suffix=.jsp
,然后请求的 Controller 处理器方法返回的视图名称为 login
,那么这个时候 InternalResourceViewResolver
就会把 login
解析为一个 InternalResourceView
对象,先把返回的模型属性都存放到对应的 HttpServletRequest
属性中,然后利用 RequestDispatcher
在服务器端把请求 forword
到 /WEB-INF/login.jsp
。
这就是 InternalResourceViewResolver
一个非常重要的特性,我们都知道存放在 /WEB-INF/
下面的内容是不能直接通过 request
请求的方式请求到的,为了安全性考虑,我们通常会把 jsp
文件放在 WEB-INF
目录下,而 InternalResourceView
在服务器端跳转的方式可以很好的解决这个问题。
@Controller 和 @RequestMapping
在第一个Spring MVC应用中创建了两个传统风格的控制器,它们是实现 Controller 接口的类。传统风格的控制器不仅需要在配置文件中部署映射,而且只能编写一个处理方法,不够灵活。使用基于注解的控制器具有以下两个优点:
- 在基于注解的控制器类中可以编写多个处理方法,进而可以处理多个请求(动作),这就允许将相关的操作编写在同一个控制器类中,从而减少控制器类的数量,方便以后的维护。
- 基于注解的控制器不需要在配置文件中部署映射,仅需要使用 RequestMapping 注释类型注解一个方法进行请求处理。
在 Spring MVC 中最重要的两个注解类型是 Controller 和 RequestMapping。
Controller 注解
在 Spring MVC 中使用 org.springframework.stereotype.Controller
注解类型声明某类的实例是一个控制器。
在 Spring MVC 中使用扫描机制找到应用中所有基于注解的控制器类,所以,为了让控制器类被 Spring MVC 框架扫描到,需要在配置文件中声明 spring-context
,并使用 <context:component-scan/>
元素指定控制器类的基本包(请确保所有控制器类都在基本包及其子包下)。
RequestMapping 注解
在基于注解的控制器类中可以为每个请求编写对应的处理方法。那么如何将请求与处理方法一一对应呢?
需要使用 org.springframework.web.bind.annotation.RequestMapping
注解类型将请求与处理方法一一对应。
为了方便维护程序,建议开发者采用类级别注解,将相关处理放在同一个控制器类中。例如,对商品的增、删、改、查处理方法都可以放在 GoodsOperate 控制类中。
编写请求处理方法
在控制类中每个请求处理方法可以有多个不同类型的参数,以及一个多种类型的返回结果。
请求处理方法中常出现的参数类型
如果需要在请求处理方法中使用 Servlet API 类型,那么可以将这些类型作为请求处理方法的参数类型。Servlet API 参数类型的示例代码如下:
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/login")
public String login(HttpSession session,HttpServletRequest request) {
session.setAttribute("skey", "session范围的值");
session.setAttribute("rkey", "request范围的值");
return "login";
}
}
除了 Servlet API 参数类型以外,还有输入输出流、表单实体类、注解类型、与 Spring 框架相关的类型等。
其中特别重要的类型是 org.springframework.ui.Model 类型,该类型是一个包含 Map 的 Spring 框架类型。在每次调用请求处理方法时 Spring MVC 都将创建 org.springframework.ui.Model 对象。Model 参数类型的示例代码如下:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/register")
public String register(Model model) {
/* 在视图中可以使用EL表达式${success}取出model中的值 */
model.addAttribute("success", "注册成功");
return "register";
}
}
请求处理方法常见的返回类型
最常见的返回类型就是代表逻辑视图名称的 String 类型,例如前面教程中的请求处理方法。除了 String 类型以外,还有 ModelAndView、Model、View 以及其他任意的 Java 类型。
Spring MVC 获取参数
通过一个实体 Bean 来接收请求参数,适用于 get 和 post 提交请求方式。
需要注意的是,Bean 的属性名称必须与请求参数名称相同。
通过处理方法的形参接收请求参数
通过处理方法的形参接收请求参数也就是直接把表单参数写在控制器类相应方法的形参中,即形参名称与请求参数名称完全相同。该接收参数方式适用于 get 和 post 提交请求方式。
通过 HttpServletRequest 接收请求参数
通过 HttpServletRequest
接收请求参数适用于 get 和 post 提交请求方式
通过 @PathVariable 接收 URL 中请求参数
通过 @PathVariable
获取 URL 中的参数
通过 @RequestParam 接收请求参数
通过 @RequestParam
接收请求参数适用于 get 和 post 提交请求方式
通过 @RequestParam 接收请求参数与“通过处理方法的形参接收请求参数”部分的区别如下:当请求参数与接收参数名不一致时,“通过处理方法的形参接收请求参数”不会报 404 错误,而“通过 @RequestParam 接收请求参数”会报 404 错误。
通过 @ModelAttribute 接收请求参数
当 @ModelAttribute
注解放在处理方法的形参上时,用于将多个请求参数封装到一个实体对象,从而简化数据绑定流程,而且自动暴露为模型数据,在视图页面展示时使用。
而“通过实体 Bean 接收请求参数”中只是将多个请求参数封装到一个实体对象,并不能暴露为模型数据(需要使用 model.addAttribute
语句才能暴露为模型数据)。
通过 @ModelAttribute
注解接收请求参数适用于 get 和 post 提交请求方式
Spring MVC 转发与重定向
重定向是将用户从当前处理请求定向到另一个视图(例如 JSP)或处理请求,以前的请求(request)中存放的信息全部失效,并进入一个新的 request 作用域;转发是将用户对当前处理的请求转发给另一个视图或处理请求,以前的 request 中存放的信息不会失效。
转发是服务器行为,重定向是客户端行为。
1)转发过程
客户浏览器发送 http 请求,Web 服务器接受此请求,调用内部的一个方法在容器内部完成请求处理和转发动作,将目标资源发送给客户;在这里转发的路径必须是同一个 Web 容器下的 URL,其不能转向到其他的 Web 路径上,中间传递的是自己的容器内的 request。
在客户浏览器的地址栏中显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。
2)重定向过程
客户浏览器发送 http 请求,Web 服务器接受后发送 302 状态码响应及对应新的 location 给客户浏览器,客户浏览器发现是 302 响应,则自动再发送一个新的 http 请求,请求 URL 是新的 location 地址,服务器根据此请求寻找资源并发送给客户。
在这里 location 可以重定向到任意 URL,既然是浏览器重新发出了请求,那么就没有什么 request 传递的概念了。在客户浏览器的地址栏中显示的是其重定向的路径,客户可以观察到地址的变化。重定向行为是浏览器做了至少两次的访问请求。
在 Spring MVC 框架中,控制器类中处理方法的 return 语句默认就是转发实现,只不过实现的是转发到视图。示例代码如下:
@RequestMapping("/register")
public String register() {
return "register"; //转发到register.jsp
}
在 Spring MVC 框架中,重定向与转发的示例代码如下:
package org.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/login")
public String login() {
// 转发到一个请求方法(同一个控制器类可以省略/index/)
return "forward:/index/isLogin";
}
@RequestMapping("/isLogin")
public String isLogin() {
// 重定向到一个请求方法
return "redirect:/index/isRegister";
}
@RequestMapping("/isRegister")
public String isRegister() {
// 转发到一个视图
return "register";
}
}
在 Spring MVC 框架中,不管是重定向或转发,都需要符合视图解析器的配置,如果直接转发到一个不需要 DispatcherServlet 的资源,例如:
return "forward:/html/my.html";
则需要使用 mvc:resources 配置
<mvc:resources location="/html/" mapping="/html/**" />
@ModelAttribute 的使用
通过 org.springframework.web.bind.annotation.ModelAttribute
注解类型可经常实现以下两个功能
1)绑定请求参数到实体对象(表单的命令对象)
@RequestMapping("/register")
public String register(@ModelAttribute("user") UserForm user) {
if ("zhangsan".equals(user.gerUsername()) && "123456".equals(user.getPassword())) {
logger.info("成功");
return "login";
} else {
logger.info("失败");
return "register";
}
在上述代码中 @ModelAttribute(“user”)UserForm user 语句的功能有两个:
- 将请求参数的输入封装到 user 对象中。
- 创建 User 实例。
以“user”为键值存储在 Model 对象中,和 model.addAttribute("user",user)”
语句的功能一样。如果没有指定键值,即 @ModelAttribute UserForm user
,那么在创建 UserForm 实例时以 userForm 为键值存储在 Model 对象中,和 model.addAtttribute("userForm", user)
语句的功能一样。
2)注解一个非请求处理方法
被 @ModelAttribute 注解的方法将在每次调用该控制器类的请求处理方法前被调用。这种特性可以用来控制登录权限,当然控制登录权限的方法有很多,例如拦截器、过滤器等。
使用该特性控制登录权限,创建 BaseController,代码如下所示:
package org.example.controller;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.ModelAttribute;
public class BaseController {
@ModelAttribute
public void isLogin(HttpSession session) throws Exception {
if (session.getAttribute("user") == null) {
throw new Exception("没有权限");
}
}
}
创建 ModelAttributeController ,代码如下所示:
package org.example.controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/attribute")
public class ModelAttributeController {
@RequestMapping("/add")
public String add() {
return "addSuccess";
}
@RequestMapping("/update")
public String update() {
return "updateSuccess";
}
@RequestMapping("/delete")
public String delete() {
return "deleteSuccess";
}
}
在上述 ModelAttributeController
类中的 add
、update
、delete
请求处理方法执行时,首先执行父类 BaseController
中的 isLogin
方法判断登录权限,可以通过地址 http://localhost:8080/admin/add
测试登录权限。
JSON 数据交互
Spring MVC 在数据绑定的过程中需要对传递数据的格式和类型进行转换,它既可以转换 String 等类型的数据,也可以转换 JSON 等其他类型的数据。
JSON 概述
JSON(JavaScript Object Notation, JS 对象标记)是一种轻量级的数据交换格式。与 XML 一样,JSON 也是基于纯文本的数据格式。它有对象结构和数组结构两种数据结构。
1)对象结构
对象结构以“{”开始、以“}”结束,中间部分由 0 个或多个以英文“,”分隔的 key/value 对构成,key 和 value 之间以英文“:”分隔。对象结构的语法结构如下:
{
key1:value1,
key2:value2,
...
}
其中,key 必须为 String 类型,value 可以是 String、Number、Object、Array 等数据类型。例如,一个 person 对象包含姓名、密码、年龄等信息,使用 JSON 的表示形式如下:
{
"pname":"张三",
"password":"123456",
"page":40
}
2)数组结构
数组结构以“[”开始、以“]”结束,中间部分由 0 个或多个以英文“,”分隔的值的列表组成。数组结构的语法结构如下:
[
value1,
value2,
...
]
上述两种(对象、数组)数据结构也可以分别组合构成更加复杂的数据结构。例如,一个 student 对象包含 sno、sname、hobby 和 college 对象,其 JSON 的表示形式如下:
{
"sno":"201802228888",
"sname":"张三",
"hobby":["篮球","足球"],
"college":{
"cname":"清华大学",
"city":"北京"
}
}
JSON 数据转换
为实现浏览器与控制器类之间的 JSON 数据交互,Spring MVC
提供了 MappingJackson2HttpMessageConverter
实现类默认处理 JSON 格式请求响应。该实现类利用 Jackson 开源包读写 JSON 数据,将 Java 对象转换为 JSON 对象和 XML 文档,同时也可以将 JSON 对象和 XML 文档转换为 Java 对象。
在使用注解开发时需要用到两个重要的 JSON 格式转换注解,分别是 @RequestBody
和 @ResponseBody
。
@RequestBody
:用于将请求体中的数据绑定到方法的形参中,该注解应用在方法的形参上。@ResponseBody
:用于直接返回 return 对象,该注解应用在方法上。
方法中的 @RequestBody 注解用于将前端请求体中的 JSON 格式数据绑定到形参 user 上,@ResponseBody 注解用于直接返回 Person 对象(当返回 POJO 对象时默认转换为 JSON 格式数据进行响应)。
拦截器的配置和使用
Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。
拦截器的定义
在 Spring MVC 框架中定义一个拦截器需要对拦截器进行定义和配置,定义一个拦截器可以通过两种方式:一种是通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类来定义;另一种是通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义。
package org.example.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class TestInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("afterCompletion方法在控制器的处理请求方法执行完成后执行," +
"即视图渲染结束之后执行");
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle方法在控制器的处理请求方法调用之后,解析视图之前执行");
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle方法在控制器的处理请求方法调用之后,解析视图之前执行");
return false;
}
}
在上述拦截器的定义中实现了 HandlerInterceptor 接口,并实现了接口中的 3 个方法。有关这 3 个方法的描述如下。
- preHandle 方法:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。
- postHandle 方法:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。
- afterCompletion 方法:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。
拦截器的配置
让自定义的拦截器生效需要在 Spring MVC 的配置文件中进行配置,配置示例代码如下:
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 配置一个全局拦截器,拦截所有请求 -->
<bean class="interceptor.TestInterceptor" />
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/**" />
<!-- 配置不需要拦截作用的路径 -->
<mvc:exclude-mapping path="" />
<!-- 定义<mvc:interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
<bean class="interceptor.Interceptor1" />
</mvc:interceptor>
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/gotoTest" />
<!-- 定义在<mvc: interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
<bean class="interceptor.Interceptor2" />
</mvc:interceptor>
</mvc:interceptors>
在上述示例代码中,<mvc:interceptors>
元素用于配置一组拦截器,其子元素 <bean>
定义的是全局拦截器,即拦截所有的请求。
<mvc:interceptor>
元素中定义的是指定路径的拦截器,其子元素 <mvc:mapping>
用于配置拦截器作用的路径,该路径在其属性 path
中定义。
如上述示例代码中,path
的属性值“/**”
表示拦截所有路径,“/gotoTest”
表示拦截所有以“/gotoTest”
结尾的路径。如果在请求路径中包含不需要拦截的内容,可以通过 <mvc:exclude-mapping>
子元素进行配置。
需要注意的是,<mvc:interceptor>
元素的子元素必须按照 <mvc:mapping.../>
、<mvc:exclude-mapping.../>
、<bean.../>
的顺序配置。
拦截器的执行流程
单个拦截器的执行流程
在配置文件中如果只定义了一个拦截器,程序将首先执行拦截器类中的 preHandle 方法,如果该方法返回 true,程序将继续执行控制器中处理请求的方法,否则中断执行。如果 preHandle 方法返回 true,并且控制器中处理请求的方法执行后、返回视图前将执行 postHandle 方法,返回视图后才执行 afterCompletion 方法。
多个拦截器的执行流程
在 Web 应用中通常需要有多个拦截器同时工作,这时它们的 preHandle 方法将按照配置文件中拦截器的配置顺序执行,而它们的 postHandle 方法和 afterCompletion 方法则按照配置顺序的反序执行。
Spring MVC 文件上传
Spring MVC 框架的文件上传是基于 commons-fileupload 组件的文件上传,只不过 Spring MVC 框架在原有文件上传组件上做了进一步封装,简化了文件上传的代码实现,取消了不同上传组件上的编程差异。
基于表单的文件上传
标签 <input type="file"/>
会在浏览器中显示一个输入框和一个按钮,输入框可供用户填写本地文件的文件名和路径名,按钮可以让浏览器打开一个文件选择框供用户选择文件。
文件上传的表单例子如下:
<form method="post" action="upload" enctype="multipart/form-data">
<input type="file" name="myfile"/>
</form>
对于基于表单的文件上传,不要忘记使用 enctype 属性,并将它的值设置为 multipart/form-data,同时将表单的提交方式设置为 post。为什么要这样呢?下面从 enctype 属性说起。
表单的 enctype 属性指定的是表单数据的编码方式,该属性有以下 3 个值。
- application/x-www-form-urlencoded:这是默认的编码方式,它只处理表单域里的 value 属性值。
- multipart/form-data:该编码方式以二进制流的方式来处理表单数据,并将文件域指定文件的内容封装到请求参数里。
- text/plain:该编码方式只有当表单的 action 属性为“mailto:”URL 的形式时才使用,主要适用于直接通过表单发送邮件的方式。
由上面 3 个属性的解释可知,在基于表单上传文件时 enctype 的属性值应为 multipart/form-data。
MultipartFile 接口
在 Spring MVC 框架中上传文件时将文件相关信息及操作封装到 MultipartFile 对象中,因此开发者只需要使用 MultipartFile 类型声明模型类的一个属性即可对被上传文件进行操作。该接口具有如下方法。
在上传文件时需要在配置文件中使用 Spring 的 org.springframework.web.multipart.commons.CommonsMultipartResolver
类配置 MultipartResolver
用于文件上传。