(五)Spring MVC

什么是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的作用

  Spring MVC,与许多其他 Web 框架一样,Spring MVC 同样围绕前端页面的控制器模式 (Controller) 进行设计,其中最为核心的 Servlet —— DispatcherServlet 为来自客户端的请求处理提供了一种用于请求处理的共享算法,而实际的工作是由可自定义配置的委托组件来执行。该模型非常灵活,支持多种工作流程。
  当服务器收到一个请求,建立在中央前端控制器Servlet(DispatcherServlet)将负责发送这个请求到合适的处理程序,使用视图来返回响应的最终结果经过渲染后再返回给用户。Spring MVC 是 Spring 产品组合的一部分,它享有 Spring IoC容器紧密结合Spring松耦合等特点,因此它有Spring的所有优点。

Spring MVC的工作流程

  在实际开发中,我们的工作主要集中在控制器和视图页面上,但Spring MVC内部完成了很多工作,这些程序在项目中是如何执行的呢?下面我们来通过一张图看一看Spring MVC程序的执行流程:


执行流程

①用户通过浏览器向Web应用服务器发送一个HTTP请求,服务器收到请求后,如果匹配到DispatcherServlet的请求映射路径(在web.xml指定的),Web容器会将该请求转交给Spring MVC的前端控制器DispatcherServlet拦截处理;

②DispatcherServlet拦截到请求后,会调用HandlerMapping(处理器映射器);

③处理器映射器根据请求将请求的信息(包括URL、HPPT方法、请求报问头、请求参数、Cookie等)找到具体的处理器,生成处理器对象及处理器拦截器(如果有有则生成)一并返回给DispatcherServlet;

④DispatcherServlet会通过返回信息选择合适的HandlerAdaper(处理器适配器)

⑤HandlerAdaper会调用Handler(处理器),这里的处理器指的是程序中编写的Controller类,也被称之为后端控制器

⑥Conroller执行完成后,会返回一个ModelAndView对象,该对象中会包含视图名或包含模型和视图名;

⑦HandlerAdaper将ModelAndView对象返回给DispatcherServlet;

⑧DispatcherServlet会根据ModelAndView对象选择一个合适的ViewReslover(视图解析器);

⑨ViewReslover解析后,会向DispatcherServlet中返回具体的View(视图);
⑩DispatcherServlet对View进行渲染(即将模型数据填充至视图中)

⑪视图渲染结果会返回给客户浏览器显示,最终用户得到的可能是一个简单的html页面,也可能是一张图片或一个PDF文档等不同的媒体形式。

Spring MVC的工作原理

  要了解Spring MVC框架的工作机理,须回答下面3个问题:
(1) DispatcherServlet如何截获特定的HTTP请求并交由Spring MVC框架处理?

(2) 位于Web(表示)层的Spirng MVC 子容器(WebApplicationContext)如何与位于Service(业务)层的Spring父容器(ApplicationContext)建立关联,以使Web层的Bean可以调用Service层的Bean?

(3) 如何初始化Spring MVC的各个组件,并将它们装配到DispatcherServlet中?

DispatcherServlet(前端控制器)介绍

  DispatcherServlet是Spring MVC的“灵魂”和“心脏”,它负责接受HTTP请求并协调Spring MVC的各个组件完成请求处理的工作。和任何Servlet的配置一样,可以通过<servlet-mapping>指定其处理的URL,所以用户必须在web.xml中配置好DispatcherServlet。

    <!--声明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>

    <!--配置springmvc的前端控制器,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>

  在<context-param>参数里通过contextConfigLocation参数指定Service层Spring容器的配置文件(此处加载classpath类路径下的spring目录下的spring-service.xml文件)。ContextLoaderListener是一个ServletContextListener(Servlet事件监听器,专门用于监听Web应用程序中ServletContext、HttpSession和ServletRequest等域对象的创建和销毁过程,监听这些域对象属性的修改以及感知绑定到HttpSession域中的某个对象的状态,当被监听对象发生上述事件后,监听器某个方法将立即被执行。),它通过contextConfigLocation参数所指定的Spring配置文件启动“Service层”的Spring容器,我们开头已经注册了这个监听器。
  简述上面的流程:Servlet容器启动——被监听器监听到——装载Spirng配置文件——WebApplicationContext对象初始化并将其赋值给ServletContext的WebApplicationContext.ROOT属性——通过WebApplicationContext访问Bean

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

  问题1答案:在<servlet>中,我们配置了名为dispatcher的DispatcherServlet,它加载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和SpringMVC这两个框架,那么它其实就是两个容器,Spring是父容器,SpringMVC是其子容器。
  问题2答案:所以,“Web层”的Spring MVC容器可以引用“Service层”Spring容器的Bean,而“Service层”Spring容器却访问不到“Web层”的Spring MVC容器的Bean。
  现在剩下最后一个问题:Spring 如何将上下文中的Spring MVC组件装配到DispatcherServlet中?我们查看DispatcherServlet的initStrategies()方法的代码,就知道了:

/**
     * 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);//初始化视图解析器
        initFlashMapManager(context);//检索和保存FlashMap实例的策略界面
    }

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

一个简单的入门程序

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

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

<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 接口

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目录下。

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

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

<?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内部自动管理。更改如下:

<?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:默认属性为value:用于映射一个请求或一个方法,使用时可以标注在一个方法或一个类上。method属性:用于指定该方法用于处理哪种类型的请求方式,包括GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE。其他属性不常用,此处不介绍。该注解一般和@Controller一起使用。
  • @RequestBody:格式转换注解,该注解用在方法的形参上,用于将请求体中的数据绑定到方法的形参中。
  • @ResponseBody:格式转换注解,该注解用在方法上,用于直接返回return对象。

  Spirng4.3还引入了新的注解,继续用来简化代码量:

  • @GetMapping:匹配GET方式的请求
  • @PostMapping:匹配POST方式的请求
  • @PutMapping:匹配PUT方式的请求
  • @DeleteMapping:匹配DELETE方式的请求
  • @PatchMapping:匹配PATCH方式的请求

  比如传统的@RequestMapping注解使用方式如下(value为默认属性,可以不写):

@RequestMapping(value="/user/{id}",method=RequestMethod.GET)
public String selectUserById(String id){
...
}

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

@GetMapping(value="/user/{id}")
public String selectUserById(String id){
...
}

  URL中的{xxx}占位符可以通过@PathVariable("xxx")注解绑定到操作方法的入参中,也可以绑定到类前。

  使用注解后就不用在控制类上实现接口了,也不用在配置文件配置映射路径和类了,是不是方便了很多呢?

使用注解的Spring MVC 程序

  前面我们知道了使用注解可以在开发时简化代码的数量,我们将在原有例子加强部分以使用注解的方式,来再次加深我们对Spring MVC执行流程的理解。

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

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

  Spring MVC通过@Controller注解即可将一个POJO对象转化为处理请求的控制器,通过@RequestMapping注解为控制器指定处理哪些URL的请求。UserController是一个负责用户处理的控制器,实现了Controller接口(这里我们加了@Controller即代表实现了Contrller接口)

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目录下,代码如下:

<%@ 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 类中加入下面代码:

    @PostMapping
    public ModelAndView createUser(User user){
        ModelAndView mav = new ModelAndView();
        //设置视图名
        mav.setViewName("rsSuccess");
        //加入user对象
        mav.addObject("user",user);
        return mav;
    }

  createUser()方法处的@PostMapping注解让createUser()处理URI为/user且请求方法为POST的请求。Spring MVC自动将表单中的数据按参数名和User属性名匹配的方式进行绑定(数据绑定中会讲到),将参数值填充到User的相应属性中。逻辑视图名为rsSuccess,然后user作为模型数据暴露给视图对象。User对象代码如下(属性和表单name对应):

public class User {
    private Integer id;
    private String userName;
    private String password;
    private String name;

    ****get和set方法省略****
}

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

<%@ 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的包扫描功能:

<?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>接口?

  HttpMessageConverter<T>是Spring十分重要的一个接口,它负责将请求信息转换为一个对象(类型为T),将对象(类型为T)输出为响应信息。
  DispacherServlet默认已经安装了RequestMappingHandlerAdapter作为HandlerAdapter的组件实现类,HttpMessageConverter即由RequestMappingHandlerAdapter使用,将请求信息转换为对象,或将对象转换为响应信息。

HttpMessageConverter<T>的实现类

  Spring为HttpMessageConverter<T>提供了众多的实现类,他们组成了一个功能强大的、用途广泛的HttpMessageConverter<T>家族,如:

  • StringHttpMessageConverter:可以将请求信息转换为字符串
  • ByteArrayHttpMessageConverter:可以读/写二进制数据
  • SourceHttpMessageConverter:可以读/写javax.xml.transform.Source类型的数据
  • MappingJackson2HttpMessageConverter:可以利用Jackson开源类包的ObjectMapper读/写JSON数据。
      在Spring中,RequestMappingHandlerAdapter已经默认配置了以下转换器:StringHttpMessageConverterByteArrayHttpMessageConverterSourceHttpMessageConverterFormHttpMessageConverter如果需要装配其他类型的Http消息转换器(比如十分常见的JSON格式转换器),可以在Spring的Web容器上下文中自定义一个RequestMappingHandlerAdapter配置,当然其配置有点繁琐,所以现在我们直接通过在Spring配置文件中加入<mvc:annotation-driven/>标签来实现JSON格式转换器的装配:
 <mvc:annotation-driven/>

这个标签非常神奇,它的实现类为org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser,让我们简单了解下它的功能:
  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 中有以下几个检测:

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

如何使用HttpMessageConverter<T>

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

import ...
@Controller
@RequestMapping("/user")
public class UserController{

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

    @ResponseBody
    @RequestMapping("/handleImg/imageId")
    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;
    }
    @ResponseBody
    @RequestMapping("/handleJson")
    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对返回值进行处理,将map以json格式返回到客户端。(注意:要使用这种类型的转换,需要3个jar包:jackson-annoations转换注解包、jackson-core转换核心包、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,表示请求中必须含有该参数,否则抛出异常。
@RequestMapping("/userName")
public String getUserName(@RequestParme("userName")String str){
    System.out.println("str");//这样str的输出结果就为表单中的userName
    
    ***省略代码****
}

数据绑定的类型

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

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

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

@RequestMapping("/userName")
public String getUserName(HttpServletRequest request){
    String userName = request.getParameter("userName");
      
    ***省略代码****
}

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

@RequestMapping("/userName")
public String getUserName(String userName){
    System.out.println("userName");
      
    ***省略代码****
}

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

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

<%@ 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>

  ②.在控制器配置映射路径

 //根据映射跳转到批量删除页面
    @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";
    }

  ③测试结果

批量删除页面

删除成功页面

控制台打印结果

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

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

import java.util.List;

public class UserVo {
    private List<User> users;

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

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

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

<%@ 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>

  ③.在控制器配置映射路径

//根据映射跳转到批量修改页面
    @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";
    }

  ④测试结果

批量修改界面

修改后提交

修改成功界面

控制台打印结果

(5)POJO对象/包装的POJO数据类型绑定
  当客户端请求传递多个不同类型的参数时,使用基本类型绑定显然是不合适的,如注册表单,此时我们就可以定义一个POJO类来进行数据绑定,即将所有关联的请求参数封装到一个POJO对象中,然后在方法中直接使用该POJO作为形参来完成数据绑定。例子在前面已经举过,就是那个User类。
  假设有个需求为用户查询订单,其中页面传递的参数可能包括:订单编号、用户名称等信息,这就包含了订单和用户连个对象的信息。此时使用POJO对象绑定则订单和用户信息混合封装,显得比较混乱,那么我们就可以考虑使用包装的POJO类型绑定。即在一个POJO中包含另一个简单的POJO,如在订单对象中包含用户对象。
(6)自定义数据类型绑定
  大部分类型使用前面各种的数据类型绑定都能满足需求,然而有些特殊类型的参数是无法在后台进行直接转换的(如日期数据),也就无法直接进行数据绑定,因此它必须先经过数据转换。
  针对这种特殊的数据类型,就需要开发者使用Converter(自定义转换器)或Formatter(格式化)来进行数据绑定,我们在下面将会实现它。

数据类型转换

   前面提到过HttpMessageConvert<T>,它用于将请求信息转换为一个对象(类型为T)或将对象(类型为T)输出为响应信息,消息的转换,而下面要讲的ConversionService却是对象之间的转换。
   ConversionService是Spring类型转换的核心接口,它位于org.springframework.core.convert中,也是该包中的唯一一个接口。
  我们可以利用org.springframework.context.support.ConversionServiceFactoryBean在Spring的上下文中定义一个ConversionService,它会被自动识别,并在Bean属性配置及Spring MVC处理方法入参绑定等场合使用它进行数据转换,代码如下:

<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接口的转换器用来进行类型转换,接口定义如下:

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
    @Nullable
    T convert(S source);
}

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

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

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.在配置文件中加入以下配置

    <!--显式的注入自定义类型转换器覆盖默认实现-->
    <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.定义控制器测试自定义转换器:

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

4.测试结果

页面

控制台打印结果

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

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

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

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

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接口的转换器:

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.在配置文件中加入以下配置

<!--显式的注入自定义类型转换器-->
    <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.定义控制器测试自定义转换器:

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

4.测试结果

页面

控制台打印结果

处理模型数据

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

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

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

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

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

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

@RequestMapping("/update")
public String update(HttpServletRequest request,HttpServletResponse response,Model mode){
    ****业务代码省略****
    return "redirect:queryUser";
}

②forward请求转发。例如,用户执行修改操作时,转发到用户修改页面,代码如下:

@RequestMapping("/edit")
public String edit(HttpServletRequest request,HttpServletResponse response,Model mode){
    ****业务代码省略****
    return "forward:editUser";
}

补充:解决中文乱码

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

<!-- 配置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>

视图解析器

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

<!--定义视图解析器,解析视图逻辑名-->
    <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",在访问时视图解析器会自动的增加前缀和后缀。

资源处理

  由于早期的Spring MVC不能很好的处理静态资源,所以在web.xml中配置DispatcherServlet的请求映射时,往往采用*.do、*.xhtml等方式。这就决定了请求URL必须是一个带后缀的URL,而无法采用真正REST风格的URL。

拦截器(Interceptor)

  Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

拦截器的定义和配置

  要使用Spring MVC中的拦截器,就需要对拦截器类进行定义和配置。通常拦截器类可以通过两种方式来定义。

  • 通过实现HandlerInterceptor接口,或继承HandlerInterceptor接口的实现类(如HandlerInterceptorAdapter)来定义
  • 通过实现WebRequestInterceptor接口,或继承WebRequestInterceptor接口的实现类来定义。

以实现HandlerInterceptor接口方式为例,自定义拦截器类的代码如下:

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 {

    }
}

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


文件上传

  一般情况下,多数文件上传都是通过表单形式提交给后台服务器的,因此,要实现文件上传功能,就需要提供一个文件上传的表单,而该表单必须满足下面3个条件:

  • form表单的method属性设置为post
  • form表单的enctype属性设置为multipart/form-data
  • 提供<input type="file" name="filename"的文件上传输入框。
<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

<!--文件上传-->
    <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.编写控制器类

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.编写文件上传表单页面

<%@ 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 企业应用开发》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容