(五)Spring MVC

简介

  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 为来自客户端的请求处理提供了一种用于请求处理的共享算法,而实际的工作是由可自定义配置的委托组件来执行。该模型非常灵活,支持多种工作流程。
  当服务器收到一个请求,建立在中央前端控制器ServletDispatcherServlet)将负责发送这个请求到合适的处理程序,使用视图来返回响应的最终结果经过渲染后再返回给用户。

工作流程

  在实际开发中,我们的工作主要集中在控制器和视图页面上,但 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 类,亦被称之为后端控制器
  • HandlerConroller)执行完成后,会返回一个ModelAndView对象给DispatcherServlet,该对象中会包含视图逻辑名或包含模型数据信息和视图逻辑名
  • DispatcherServlet会根据ModelAndView对象选择一个合适的ViewReslover(视图解析器)
  • ViewReslover解析后,会向DispatcherServlet中返回具体的View(视图)
  • DispatcherServletView进行渲染(即将模型数据填充至视图中)
  • ⑨ 视图渲染结果会返回给客户浏览器显示,最终用户得到的可能是一个简单的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--声明 Servlet 容器监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!--监听到 Servlet 容器启动时装载 Root WebApplicationContext 的 Spring 配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-service.xml</param-value>
</context-param>

<!--配置 Spring Mvc 的前端控制器,request 请求会先经过这个控制器-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--该 Servlet 初始化时加载该 Servlet 专属的 WebApplicationContext 的 Spring 配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!--默认匹配所有请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>

  首先,我们注册了一个ContextLoaderListener,它是一个ServletContextListener(即Servlet事件监听器:专门用于监听 Web 应用程序中ServletContextHttpSessionServletRequest等域对象的创建和销毁过程,当监听这些域对象属性的修改或感知绑定到HttpSession域中的某个对象的状态改变时,监听器中的某个方法将立即被执行)
  另外,ContextLoaderListener加载时会查找名为contextConfigLocation的参数,该参数在<context-param>中配置。
  由于在<context-param>参数里通过contextConfigLocation参数指定了 Service 层 Spring 容器的配置文件(此处加载classpath类路径下的spring目录下的spring-service.xml文件),所以将首先启动“ Service 层”的 Spring 容器

  上述流程,简而言之:Servlet 容器启动–> 被监听器监听到 –> 装载 Spirng 配置文件–> WebApplicationContext 对象初始化并将其赋值给 ServletContextWebApplicationContext.ROOT属性 –> 通过WebApplicationContext访问 Bean

  DispatcherServlet主流程有篇文章详细讲过,我们继续向下解释代码。

  问题1答案:Servlet中,我们配置了名为dispatcherDispatcherServlet,它通过contextConfigLocation参数指定加载WEB-INF目录下的spring-web.xml文件,将启动“ Web 层”的 Spring MVC 容器。通过映射处理接受到的所有 HTTP 请求,即所有的 HTTP 请求都会被DispatcherServlet拦截并处理

注意哦DispatcherServlet在初始化的过程中,会建立一个自己的 IoC 容器上下文Servlet WebApplicationContext,会以ContextLoaderListener建立的Root WebApplicationContext作为自己的父级上下文。DispatcherServlet持有的上下文默认的实现类是XmlWebApplicationContext。(上下文理解为 Spring 容器即可)

img

  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中?
  我们查看DispatcherServletinitStrategies()方法的代码,就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
// 初始化上传文件解析器
initMultipartResolver(context);
// 初始化本地化解析器
initLocaleResolver(context);
// 初始化主题解析器
initThemeResolver(context);
// 初始化处理器映射器
initHandlerMappings(context);
// 初始化处理器适配器
initHandlerAdapters(context);
// 初始化处理器异常解析器
initHandlerExceptionResolvers(context);
// 初始化请求到视图名翻译器
initRequestToViewNameTranslator(context);
// 初始化视图解析器
initViewResolvers(context);
// 检索和保存 FlashMap 实例的策略界面
initFlashMapManager(context);
}

  initStrategies()方法将在WebApplicationContext初始化后自动执行,此时 Spring 容器的 Bean 已经初始化完成。该方法的工作原理是:通过反射机制查找并装配 Spring 容器中用户显式自定义的组件 Bean,如果找不到,则装配DispatcherServlet.properties文件中默认的组件实例,如本地化解析器,主题解析器,处理器映射等等。
  简单来说,当DispatcherServlet初始化后,就会自动扫描 Spring 容器中的 Bean ,根据名称或类型匹配的机制查找自定义的组件 Bean,找不到则使用默认组件。

快速入门

  在学习了 Spring MVC 框架的整体机构后,下面通过一个简单的例子讲解 Spring MVC 开发的基本步骤:

(1)配置 web.xml,定义 DispatcherServlet,截获特定的 URL 请求,指定业务层对应的 Spring配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1" metadata-complete="true">
<!--配置 springmvc 的前端控制器,request 请求会先经过这个控制器-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--初始化时加载配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--数字代表优先级,容器启动时立即加载该 servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!--默认匹配所有请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

(2)编写处理请求的控制器(处理器):

  创建控制器类UserController ,该类需要实现Controller 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 控制器类
public class UserController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
// 创建 ModelAndView 对象
ModelAndView mav = new ModelAndView();
// 向对象中添加数据
mav.addObject("userName","tom");
// 设置逻辑视图名
mav.setViewName("/views/user/rsSuccess.jsp");
// 返回
return mav;
}
}

(3)编写视图对象
  该 Jsp 页面位于Web根目录views目录下的user目录下。

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %>
<html>
<head>
<title>注册成功</title>
</head>
<body>
恭喜你注册成功,${userName},你好啊
</body>
</html>

(4)配置 SpringMVC 的配置文件,使控制器、视图解析器等生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置处理器 Handler,映射 "/firstController" 请求-->
<bean name="/user" class="cn.wk.chapter17.controller.UserController"/>

<!--处理器映射器,将处理器 Handler 的 name 作为 url 查找-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<!--处理器适配器,配置对处理器中的 HandlerRequest() 方法调用-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
</beans>

注意:在老版本 Spring 中,必须配置处理器映射器处理器适配器视图解析器;但 Spring4.0 后简化了配置,这些可以省略,只配置处理器即可,其他 Spring 内部自动管理。更改如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置处理器 Handler,映射 "/user" 请求-->
<bean name="/user" class="cn.wk.chapter17.controller.UserController"/>
</beans>

5.启动 tomcat 访问浏览器

结果

  简述一下这个流程:
① 在浏览器输入urlDispatcherServlet接收到映射"/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
2
3
4
@RequestMapping(value="/user/{id}",method=RequestMethod.GET)
public String selectUserById(@PathVariable("id") String id){
...
}

  而使用@GetMapping注解后的简化代码如下:

1
2
3
4
@GetMapping(value="/user/{id}")
public String selectUserById(@PathVariable("id") String id){
...
}

  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
2
3
4
5
6
7
8
9
10
11
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
// 控制器类
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/register")
public String register(){
return "register";
}
}

  首先使用@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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>注册页面</title>
</head>
<body>
<%--将表单提交给/user控制器中--%>
<form method="post" action="${pageContext.request.contextPath}/user">
用户名:<input type="text" name="userName"><br>
密码:<input type="password" name="password"><br>
姓名:<input type="text" name="name"><br>
<input type="submit" value="提交"><br>
</form>
</body>
</html>

  register.jsp非常简单,包括一个表单,提交到/user进行处理,下面,我们在UserController类中加入一些新的代码:

1
2
3
4
5
6
7
8
9
@PostMapping
public ModelAndView createUser(User user){
ModelAndView mav = new ModelAndView();
// 设置视图名
mav.setViewName("rsSuccess");
// 加入 user 对象
mav.addObject("user",user);
return mav;
}

  createUser()方法处的@PostMapping注解让该方法处理 URI 为/user且请求方法为POST的请求。由于 Spring MVC 会自动将表单中的数据按参数名和 User 实体中属性名匹配的方式进行绑定(数据绑定中介绍),所以将参数值会被填充到User的相应属性中。而在方法内的代码,设置了逻辑视图名为rsSuccess,最后将user作为模型数据暴露给视图对象。
  User对象代码如下(属性和表单 name 对应):

1
2
3
4
5
6
7
8
public class User {
private Integer id;
private String userName;
private String password;
private String name;

// get 和 set 方法省略
}

  视图解析器将rsSuccess解析为rsSuccess.jsp的视图对象,rsSuccess.jsp可以访问到模型中的数据,页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %>
<html>
<head>
<title>注册成功</title>
</head>
<body>
恭喜您注册成功,${user.userName},您好啊</br>
您的密码为${user.password},请您记住哦!</br>
您的姓名为${user.name}
</body>
</html>

4.在配置文件开启 Spring 的包扫描功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启自动扫描-->
<context:component-scan base-package="cn.wk.chapter17.controller"/>
<!--定义视图名称解析器,解析视图逻辑名-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--设置前缀,简化视图逻辑名-->
<property name="prefix" value="/views/user/"/>
<!--设置后缀,简化视图逻辑名-->
<property name="suffix" value=".jsp"/>
</bean>
</beans>

5.运行 Tomcat 并测试

注册页面

输入数据

返回的结果

  再次简述一下流程:

  • DispatcherServlet接收到客户端的/user请求
  • DispatcherServlet使用DefaultAnnotationHandlerMapping查找负责处理该请求的处理器
  • DispatcherServlet将请求分发给名为/userUserController处理器
  • ④ 处理器完成业务员处理后,返回ModelAndView对象,其中View的逻辑名为/user/createSuccess,而模型包含一个键为userUser对象
  • 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.AntPathMatcherorg.springframework.web.util.UrlPathHelper以供RequestMappingHandlerMappingViewControllersHandlerMappingHandlerMapping 服务资源所使用

  对于 JSR-303 实现,会检测 javax.validation.Validator 路径是否有效,有效则会帮我们创建对应的实现类并注入。
  最后帮我们检测一些列 HttpMessageConverter的实现类们,这些主要是用作直接对请求体里面解析出来的数据进行转换。俗称 HTTP 消息转换器,与参数转换器不一样。

  在 SpringMVC 5.1.1 中有以下几个检测:

img

  除了会帮我们注入以上检测有效的 HTTP 消息转换器外,还会帮我们注入SpringMVC 自带的几个 HTTP 消息转换器,上面检测的转换器是由上到下顺序加入的,也就是说解析的时候会根据 ContentType 从上到下找合适的。

如何使用 HttpMessageConverter<T>?

  我们可以通过@RequestBody@ResponseBody注解来使用它,前面提到过它们的作用,我们通过一个例子来解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import ...
@Controller
@RequestMapping("/user")
public class UserController{

@RequestMapping("/handle")
public String handle(@RequestBody String requestBody) {
System.out.println(requestBody);
return "success";
}

@RequestMapping("/handleImg/imageId")
@ResponseBody
public byte[] handleImg(@PathVariable("imageId") String imageId) throws IOException {
System.out.println("加载" + imageId + "图片");
Resource res = new ClassPathResource("/image.jpg");
byte[] fileData = FileCopyUtils.copyToByteArray(res.getInputStream());
return fileData;
}

@RequestMapping("/handleJson")
@ResponseBody
public Map<String, User> handleJson() {
User user1 = new User();
user1.setName("张三");
user1.setPassword("987654");

User user2 = new User();
user2.setName("王五");
user2.setPassword("123456");

Map<String, User> map = new HashMap();
map.put("1", user1);
map.put("2", user2);
return map;
}
}

  我们在前面已经通过<mvc:annotation-driven/>标签为RequestMappingHandlerAdapter在注册了若干个HttpMessageConverterhandler()方法的requestBody入参标注了一个@RequestBody注解,Spring MVC 将根据requestBody的类型查找匹配的HttpMessageConverter
  由于StringHttpMessageConverter的泛型类型对应String,所以StringHttpMessageConverter将被 Spring MVC 选中,用它将请求体信息进行转换并将结果绑定到requestBody入参中。
  在handleImg()方法中标注了一个@ResponseBody注解。由于返回值类型为byte[],所以 Spring MVC 根据类型匹配的查找规则将使用ByteArrayHttpMessageConver对返回值进行处理,将图片数据流输出到客户端。
  在handleJson()方法中标注了一个@ResponseBody注解。由于返回值为String,所以 Spring MVC 根据类型匹配的查找规则将使用MappingJackson2HttpMessageConverter对返回值进行处理,将mapjson格式返回到客户端。(注意:要使用这种类型的转换,需要jackson-databind转换的数据绑定包),返回结果如下:

Json 格式

数据绑定

  在执行程序时,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
2
3
4
5
6
@RequestMapping("/userName")
public String getUserName(@RequestParme("userName")String str){
System.out.println("str");// 这样 str 的输出结果就为表单中的 userName

// 省略其他代码
}

类型

默认数据类型绑定

  当前端请求的参数比较简单时,可以在后台方法的形参中直接使用Spring MVC 提供的默认参数类型进行数据绑定。

  • HttpServletRequest:通过 request 对象获取请求信息。
  • HttpServletResponse:通过 response 对象响应信息。
  • HttpSession:通过 session对象得到 session 中存放的对象。
  • Model/ModelMap:Model 是一个接口,ModelMap 是一个接口实现,作用是将 model 数据填充到 request 域。

  举个例子,需要使用 HttpServletRequest 相关参数时:

1
2
3
4
5
6
@RequestMapping("/userName")
public String getUserName(HttpServletRequest request){
String userName = request.getParameter("userName");

// 省略其他代码
}

基本数据类型绑定

  指 Java 中几种基本数据类型及简单引用类型的绑定,如 int、String、Double 等类型。
  以前面注册用户的例子来说,提交表单后可以获取到用户的 userName,那么可以这样写:

1
2
3
4
5
6
@RequestMapping("/userName")
public String getUserName(String userName){
System.out.println("userName");

//省略其他代码
}

注意:形参名必须要和表单的name属性一致,如果不一致,需要使用@RequestParme注解,前面已经解释过。

数组类型绑定

  在实际开发中,可能会遇到前端请求需要传递到后台一个或多个相同名称参数的情况(如批量删除操作),此种情况不适合基本数据类型绑定。
  但是,若将所有同种类型的请求参数封装到一个数组中,后台就可以进行绑定接收了。
  举个批量删除的例子:
  ① 模拟批量删除页面和删除成功页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<title>批量删除用户</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="container">
<form method="get" action="${pageContext.request.contextPath}/user/deleteUsers">
<table class="table table-bordered">
<tr class="active">
<td>id</td>
<td>用户名</td>
<td>密码</td>
<td>姓名</td>
</tr>
<tr class="info">
<td><input name="id" class="checkbox" type="checkbox" value="1"></td>
<td>淹死的鱼</td>
<td>123455</td>
<td>jack</td>
</tr>
<tr class="success">
<td><input name="id" class="checkbox" type="checkbox" value="2"></td>
<td>不会飞的鸟</td>
<td>123345</td>
<td>tom</td>
</tr>
<tr class="warning">
<td><input name="id" class="checkbox" type="checkbox" value="3"></td>
<td>不会化的冰</td>
<td>123345</td>
<td>lucy</td>
</tr>
</table>
<button class="btn btn-danger" type="submit">删除选中用户</button>
</form>
</div>
</body>
</html>

<--删除成功跳转页面-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>删除成功</title>
</head>
<body>
恭喜你删除成功
</body>
</html>

  ② 在控制器配置映射路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根据映射跳转到批量删除页面
@RequestMapping("/toUsers")
public String toUser(){
return "Users";
}

// 选中复选框后提交得到数据并跳转到删除成功页面
@GetMapping("/deleteUsers")
public String deleteUsers(Integer[] id){
if(id!=null){
for(Integer i : id){
System.out.println("删除id为"+i+"的用户成功");
}
}
return "deleteSuccess";
}

  ③测试结果

批量删除页面

删除成功页面

控制台打印结果

集合类型绑定

  指 List、Set、Map 这些集合类型的绑定。
  在批量删除用户的操作中,前端请求传递的都是同名参数的用户 id,只要在后台使用同一种数组类型的参数绑定接收,就可以在方法中通过循环数组参数的方式来完成删除操作。
  但如果是批量修改用户操作的话,前端请求传递过来的数据可能就会批量包含各种类型的数据,如 Integer,String 等等。
  这种情况我们就可以使用集合数据绑定,即在包装类中定义一个包含用户信息类的集合,然后在接受方法中将参数类型定义为该包装类的集合。

  我们通过一个批量修改用户的例子来了解一下:
  ① 定义一个 UserVo 类

1
2
3
4
5
6
7
8
9
10
11
public class UserVo {
private List<User> users;

public List<User> getUsers() {
return users;
}

public void setUsers(List<User> users) {
this.users = users;
}
}

  ② 模拟批量修改页面和修改成功页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<title>批量删除用户</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="container">
<form method="get" action="${pageContext.request.contextPath}/user/changeUserVO">
<table class="table table-bordered">
<tr class="active">
<td>id</td>
<td>用户名</td>
<td>密码</td>
<td>姓名</td>
</tr>
<tr class="info">
<td><input name="users[0].id" class="checkbox" type="checkbox" value="1"></td>
<td><input name="users[0].userName" class="text" type="text" value="淹死的鱼"></td>
<td><input name="users[0].password" class="text" type="text" value="123456"></td>
<td><input name="users[0].name" class="text" type="text" value="tom"></td>
</tr>
<tr class="success">
<td><input name="users[1].id" class="checkbox" type="checkbox" value="2"></td>
<td><input name="users[1].userName" class="text" type="text" value="不会飞的鸟"></td>
<td><input name="users[1].password" class="text" type="text" value="123456"></td>
<td><input name="users[1].name" class="text" type="text" value="jack"></td>
</tr>
<tr class="warning">
<td><input name="users[2].id" class="checkbox" type="checkbox" value="3"></td>
<td><input name="users[2].userName" class="text" type="text" value="不会化的冰"></td>
<td><input name="users[2].password" class="text" type="text" value="123456"></td>
<td><input name="users[2].name" class="text" type="text" value="tom"></td>
</tr>
</table>
<button class="btn btn-danger" type="submit">修改选中用户</button>
</form>
</div>
</body>
</html>

<--修改成功页面-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>批量修改成功</title>
</head>
<body>
恭喜你修改成功
</body>
</html>

  ③ 在控制器配置映射路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 根据映射跳转到批量修改页面
@RequestMapping("/toChangeUsers")
public String toChangeUsers() {
return "changeUsers";
}

// 修改后提交数据并跳转到修改成功页面
@GetMapping("/changeUserVO")
public String ChangeUsers(UserVo userList) {
List<User> users = userList.getUsers();
if (userList != null) {
for (User user : users) {
if (user.getId() != null){
System.out.println(user.getUserName()+user.getPassword()+user.getName());
}
}
}
return "cgSuccess";
}

  ④ 测试结果

批量修改界面

修改后提交

修改成功界面

控制台打印结果

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
2
3
4
5
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
@Nullable
T convert(S source);
}

  T convert(S source);负责将 S 类型的对象转换为 T 类型的对象,下面我们通过日期类型转换的例子来具体了解:

1.创建实现了 Converter 接口的转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.core.convert.converter.Converter;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateConvert implements Converter<String, Date> {
private String pattern = "yyyy-MM-dd HH:mm:ss";
private SimpleDateFormat sdf;

@Override
public Date convert(String source) {
// 1.用给定的模式和默认语言环境的日期格式符号构造 SimpleDateFormat
sdf = new SimpleDateFormat(pattern);
try {
// 2. 解析source字符串的文本,根据指定的设定生成 Date返回。
return sdf.parse(source);
}catch (Exception e){
// 3.异常抛出
throw new IllegalArgumentException("日期格式无效");
}
}
}

2.在配置文件中加入以下配置

1
2
3
4
5
6
7
8
9
10
11
<!--显式的注入自定义类型转换器覆盖默认实现-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--自定义类型转换器配置-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="cn.wk.chapter17.convert.DateConvert"/>
</set>
</property>
</bean>

  我们使用了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
2
3
4
5
6
7
8
@Controller
public class DateController {
@RequestMapping("/stringToDate")
public String toDate(Date date){
System.out.println("日期为"+date);
return "success";//跳转到成功界面
}
}

4.测试结果

页面

控制台打印结果

数据格式化(本质还是数据转换)

  Spring 使用转换器来进行源类型对象到目标类型对象的转换,但是转换器并不提供输入及输出信息格式化的工作。
  如果需要转换的源类型数据(一般是字符串)是从客户端界面中传递过来的,为了方便使用者观看,这些数据往往具有一定的格式,比如日期、时间、数字、货币等数据都是具有一定格式的。
  在不同的本地化环境中,同一类型的数据还有相应地呈现不同的显示格式。
  如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据,是 Spring 格式化框架要解决的问题,该格式框架最重要的接口为Formatter<T>接口,定义如下:

1
2
3
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

  该接口扩展了 2 个接口Printer<T>Parser<T>接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.springframework.format;
import java.util.Locale;
@FunctionalInterface
public interface Printer<T> {
String print(T object, Locale locale);
}
package org.springframework.format;
import java.text.ParseException;
import java.util.Locale;
@FunctionalInterface
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}

  Printer<T>负责对象的格式化输出,而Parser<T>负责对象的格式化输入,在接口中各定义了一个方法,print()接口方法将类型为 T 的成员对象根据本地化的不同输出为不同的格式化的字符串;parse()接口方法参考本地化信息将一个格式化的字符串转换为T类型的对象,即完成格式化对象的输入工作。
  我们以Formatter接口再写一个日期转换的例子:

1.创建实现了 Formatter 接口的转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.format.Formatter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormat implements Formatter<Date> {
// 定义日期格式
private String pattern = "yyyy-MM-dd HH:mm:ss";
private SimpleDateFormat sdf;
@Override
public Date parse(String s, Locale locale) throws ParseException {
// 解析 s 字符串的文本,根据指定的设定生成 Date 返回
sdf = new SimpleDateFormat(pattern);
return sdf.parse(s);
}

@Override
public String print(Date date, Locale locale) {
// 将给定的 Date 格式化为日期/时间字符串,并将结果添加到给定的 StringBuffer。
return new SimpleDateFormat().format(date);
}
}

2.在配置文件中加入以下配置

1
2
3
4
5
6
7
8
9
10
11
<!--显式的注入自定义类型转换器-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--自定义类型转换器配置-->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="cn.wk.chapter17.convert.DateFormat"/>
</set>
</property>
</bean>

3.定义控制器测试自定义转换器:

1
2
3
4
5
6
7
8
@Controller
public class DateController {
@RequestMapping("/stringToDate")
public String toDate(Date date){
System.out.println("日期为"+date);
return "success";//跳转到成功界面
}
}

4.测试结果

页面

控制台打印结果

补充:解决中文乱码

  有时候如果我们输入中文字符,会出现乱码问题,这个问题很好解决,我么只需要在web.xml配置一个Spring 的字符编码过滤器即可解决这个问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 配置spring的字符编码过滤器,保证request请求的中文字符不会乱码(注意这个过滤器要放到最前面) -->
<filter>
<filter-name>CharacterEncoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncoding</filter-name>
<!-- 设置这个字符编码过滤器作用于每一个请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>

模型数据处理

  对于 MVC 框架来说,模型数据是最重要的,因为控制C是为了产生模型数据M,而视图V则是为了渲染模型数据。
  请求处理方法执行完成后,最终返回一个ModelAndView对象,对于那些返回StringViewModelMap等类型的处理方法,Spring MVC 也会在内部将它们装配成一个ModelAndView对象,该对象包含了视图逻辑名和模型对象的信息。
  Spring MVC 支持的方法返回类型常见的就 3 个,分别为:

  • String:仅代表一个逻辑视图名,可以跳转视图,但不能携带数据。
  • ModelAndView:包含模型和逻辑视图名,可以添加 Model 数据,并指定视图
  • void:在异步请求时使用,它只返回数据,而不会跳转视图。

  由于ModelAndView类型未能实现数据与视图之间的解耦,所以在企业开发时,方法的返回类型通常都会使用String
  问:既然String类型的返回值不能携带参数,那么在方法中是如何将数据带入视图页面的呢?
  答:通过Model参数类型,即可添加需要在视图中显示的属性,代码如下:

1
2
3
4
5
@RequestMapping("/user")
public String handleRequest(HttpServletRequest request,HttpServletResponse response,Model mode,User user){
model.addAttribute("user",user);
return "views/user/rsSuccess";
}

  而且String的作用不仅仅如此,它还可以进行重定向与请求转发:

① forward 请求转发

  例如,用户登录失败后,需要携带消息转发到用户登录页面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class UserController{
@RequestMapping("/login.do")
public String update(String username, String password, Model model){
****业务代码省略****
try {
// 登录成功,重定向到主页
return "redirect:/index";
} catch (AuthenticationException e) {
// 登录失败,转发到登录页面
model.addAttribute("msg", "用户名或密码错误,请重新输入");
return "forward:/login";
}
}
}

② redirect 重定向

  例如,在用户修改用户信息操作后,将请求重定向到用于查询用户信息的界面,代码如下:

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapper("/user")
public class UserController{
@RequestMapping("/update")
public String update(User user){
****业务代码省略****
return "redirect:/user/query";
}
}

注意哦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";

注意哦forwardredirect后的字符都将作为 URL 处理,前者与当前请求属于同一个请求,后者会发起一个新的请求。

视图解析器

  视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。
  视图对象可以是 JSP,还可以是 Excel、PDF、XML、JSON 等各种形式的视图。
  Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),视图解析器有多种,不过所有视图解析器都实现了ViewResolver接口,我们前面就定义过这样一段代码:

1
2
3
4
5
6
7
<!--定义视图解析器,解析视图逻辑名-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--设置前缀,简化视图逻辑名-->
<property name="prefix" value="/views/user/"/>
<!--设置后缀,简化视图逻辑名-->
<property name="suffix" value=".jsp"/>
</bean>

  上面一段代码,定义了一个视图解析器(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return false;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
}

  可以看到,拦截器重写了 3 个方法:

  • boolean preHandle():请求到达HandlerAdapter之前执行该方法,返回false时请求直接返回,不会向下传递
  • void postHandle():请求被HandlerAdapter执行后执行该方法
  • void afterCompletion():在响应被渲染后执行该方法

  要使自定义的拦截器生效,还需要在 Spring MVC 的配置文件中配置具体的拦截路径,代码如下:

1
2
3
4
5
6
<mvc:interceptors>
<mvc:interceptor>
<mapping path="/secure/*"/>
<bean class="cn.wk.spring.interceptor.UserHandlerInterceptor" />
</mvc:interceptor>
</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
2
3
4
5
<form method="post" enctype="multipart/form-data">
<%--multiple属性为html5新特性,支持多文件上传--%>
<input type="file" name="filename" multiple="multiple">
<input type="submit" value="文件上传">
</form>

  当form表单的enctype属性为multiple/form-data时,浏览器就会采用二进制流来处理表单数据,服务器端就会对文件上传的请求进行解析处理。

Spring MVC 中文件上传

  Spring MVC 为文件上传提供了直接支持,这种支持是通过即插即用的MultipartResolver实现的。Spring 内部使用Apache Commons FileUpload技术实现了一个MultipartResolver实现类:CommonsMultipartResolver,需要依赖包commons-fileuploadcommons-io
  在 Spring MVC 上下文中默认没有装配MultipartResolver,因此默认情况下不能处理文件的上传工作。如果像使用Spring的文件上传功能,则需要先在上下文中配置。

1.配置 MultipartResolver

1
2
3
4
5
6
7
8
9
<!--文件上传-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--设置上传的默认的编码格式-->
<property name="defaultEncoding" value="UTF-8"/>
<!--设置上传文件大小上限为 5MB-->
<property name="maxUploadSize" value="5242880"/>
<!--设置上传文件的临时路径,文件上传完成临时路径中的文件会被清除-->
<property name="uploadTempDir" value="views"/>
</bean>

  CommonsMultipartResolver的属性还有许多,如maxInMemorySize缓存中的最大尺寸 和resolveLazily可以推迟文件解析,以便在Controller中捕获文件大小异常。
2.编写控制器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;

@Controller
public class UploadPhoto {
// 访问文件上传页面
@RequestMapping("/uploadPhoto")
public String uploadPhoto(){
return "uploadPhoto";
}

@RequestMapping("/upload")
public String uploadPhoto(String username, List<MultipartFile> files, HttpServletRequest request){
if (!files.isEmpty()){
for(MultipartFile file : files){
// 获取上传文件的原名称
String originalName = file.getOriginalFilename();
// 设置上传文件的地址保存目录
String savePath = request.getServletContext().getRealPath("/upload/");
File filePath = new File(savePath);
// 如果地址保存目录不存在,则先创建目录
if(!filePath.exists()){
filePath.mkdirs();
}
// 使用 UUID 重新命名上传文件的名称
String newFileName = username + UUID.randomUUID() + originalName;
try {
// 使用 MultipartFile 的 transferTo 方法将文件上传到指定目录
file.transferTo(new File(savePath + newFileName));
System.out.println(savePath + newFileName);
} catch (IOException e) {
e.printStackTrace();
return "fail";
}
}
return "success";
}else
return "fail";
}
}

  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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>图片上传界面</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="${pageContext.request.contextPath}/upload">
<input type="text" name="username" value="请输入你的用户名"></br>
<input type="file" name="files" multiple="multiple"></br>
<input type="submit" value="上传图片">
</form>
</body>
</html>

//上传成功显示success页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>成功操作</title>
</head>
<body>
恭喜你上传成功
</body>
</html>

4.测试

选择图片

上传完成跳转到成功页面

上传的目录

参考

  • Spring MVC 官方文档
  • 陈雄华 林开雄 文建国. 精通 Spring 4.x 企业应用开发 [M]. 电子工业出版社,2017

文章信息

时间 说明
2019-03-22 初稿
0%