手码两万余字,SpringMVC 包教包会

1. SpringMVC 简介

1.1 Spring Web MVC是什么

Spring Web MVC 是一种基于 Java 的实现了 Web MVC 设计模式的请求驱动类型的轻量级 Web 框架,即便用了 MVC 架构模式的思想,将 web 层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助咱们简化开发,Spring Web MVC 也是要简化咱们平常 Web 开发的。在 传统的 Jsp/Servlet 技术体系中,若是要开发接口,一个接口对应一个 Servlet,会致使咱们开发出许多 Servlet,使用 SpringMVC 能够有效的简化这一步骤。php

Spring Web MVC 也是服务到工做者模式的实现,但进行可优化。前端控制器是 DispatcherServlet;应用控制器能够拆为处理器映射器(Handler Mapping)进行处理器管理和视图解析器(View Resolver)进行视图管理;页面控制器/动做/处理器为 Controller 接口(仅包含 ModelAndView handleRequest(request, response) 方法,也有人称做 Handler)的实现(也能够是任何的 POJO 类);支持本地化(Locale)解析、主题(Theme)解析及文件上传等;提供了很是灵活的数据验证、格式化和数据绑定机制;提供了强大的约定大于配置(惯例优先原则)的契约式编程支持。css

1.2 Spring Web MVC能帮咱们作什么

  • 让咱们能很是简单的设计出干净的 Web 层和薄薄的 Web 层;
  • 进行更简洁的 Web 层的开发;
  • 天生与 Spring 框架集成(如 IoC 容器、AOP 等);
  • 提供强大的约定大于配置的契约式编程支持;
  • 能简单的进行 Web 层的单元测试;
  • 支持灵活的 URL 到页面控制器的映射;
  • 很是容易与其余视图技术集成,如 Velocity、FreeMarker 等等,由于模型数据不放在特定的 API 里,而是放在一个 Model 里(Map 数据结构实现,所以很容易被其余框架使用);
  • 很是灵活的数据验证、格式化和数据绑定机制,能使用任何对象进行数据绑定,没必要实现特定框架的 API;
  • 提供一套强大的 JSP 标签库,简化 JSP 开发;
  • 支持灵活的本地化、主题等解析;
  • 更加简单的异常处理;
  • 对静态资源的支持;
  • 支持 RESTful 风格

2. HelloWorld

接下来,经过一个简单的例子来感觉一下 SpringMVC。html

1.利用 Maven 建立一个 web 工程(参考 Maven 教程)。 2.在 pom.xml 文件中,添加 spring-webmvc 的依赖:前端

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.3</version>
    </dependency>
</dependencies>
复制代码

添加了 spring-webmvc 依赖以后,其余的 spring-web、spring-aop、spring-context 等等就所有都加入进来了。java

3.准备一个 Controller,即一个处理浏览器请求的接口。git

public class MyController implements Controller {
    /** * 这就是一个请求处理接口 * @param req 这就是前端发送来的请求 * @param resp 这就是服务端给前端的响应 * @return 返回值是一个 ModelAndView,Model 至关因而咱们的数据模型,View 是咱们的视图 * @throws Exception */
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("name", "javaboy");
        return mv;
    }
}
复制代码

这里咱们咱们建立出来的 Controller 就是前端请求处理接口。程序员

4.建立视图web

这里咱们就采用 jsp 做为视图,在 webapp 目录下建立 hello.jsp 文件,内容以下:面试

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>hello ${name}!</h1>
</body>
</html>
复制代码

5.在 resources 目录下,建立一个名为 spring-servlet.xml 的 springmvc 的配置文件,这里,咱们先写一个简单的 demo ,所以能够先不用添加 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">

    <bean class="org.javaboy.helloworld.MyController" name="/hello"/>
    <!--这个是处理器映射器,这种方式,请求地址其实就是一个 Bean 的名字,而后根据这个 bean 的名字查找对应的处理器-->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
        <property name="beanName" value="/hello"/>
    </bean>
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>
    
    <!--视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>
复制代码

6.加载 springmvc 配置文件

在 web 项目启动时,加载 springmvc 配置文件,这个配置是在 web.xml 中完成的。

<?xml version="1.0" encoding="UTF-8"?>
<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_4_0.xsd" version="4.0">
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-servlet.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
复制代码

全部请求都将自动拦截下来,拦截下来后,请求交给 DispatcherServlet 去处理,在加载 DispatcherServlet 时,还须要指定配置文件路径。这里有一个默认的规则,若是配置文件放在 webapp/WEB-INF/ 目录下,而且配置文件的名字等于 DispatcherServlet 的名字+ -servlet(即这里的配置文件路径是 webapp/WEB-INF/springmvc-servlet.xml),若是是这样的话,能够不用添加 init-param 参数,即不用手动配置 springmvc 的配置文件,框架会自动加载。

7.配置并启动项目(参考 Maven 教程)

8.项目启动成功后,浏览器输入 http://localhost:8080/hello 就能够看到以下页面:

3. SpringMVC 工做流程

面试时,关于 SpringMVC 的问题,超过 99% 都是这个问题。

4. SpringMVC 中的组件

1.DispatcherServlet:前端控制器

用户请求到达前端控制器,它就至关于 mvc 模式中的c,DispatcherServlet 是整个流程控制的中心,至关因而 SpringMVC 的大脑,由它调用其它组件处理用户的请求,DispatcherServlet 的存在下降了组件之间的耦合性。

2.HandlerMapping:处理器映射器

HandlerMapping 负责根据用户请求找到 Handler 即处理器(也就是咱们所说的 Controller),SpringMVC 提供了不一样的映射器实现不一样的映射方式,例如:配置文件方式,实现接口方式,注解方式等,在实际开发中,咱们经常使用的方式是注解方式。

3.Handler:处理器

Handler 是继 DispatcherServlet 前端控制器的后端控制器,在DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。因为 Handler 涉及到具体的用户业务请求,因此通常状况须要程序员根据业务需求开发 Handler。(这里所说的 Handler 就是指咱们的 Controller)

4.HandlAdapter:处理器适配器

经过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,经过扩展适配器能够对更多类型的处理器进行执行。

5.ViewResolver:视图解析器

ViewResolver 负责将处理结果生成 View 视图,ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果经过页面展现给用户。 SpringMVC 框架提供了不少的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等。通常状况下须要经过页面标签或页面模版技术将模型数据经过页面展现给用户,须要由程序员根据业务需求开发具体的页面。

5. DispatcherServlet

5.1 DispatcherServlet做用

DispatcherServlet 是前端控制器设计模式的实现,提供 Spring Web MVC 的集中访问点,并且负责职责的分派,并且与 Spring IoC 容器无缝集成,从而能够得到 Spring 的全部好处。DispatcherServlet 主要用做职责调度工做,自己主要用于控制流程,主要职责以下:

  1. 文件上传解析,若是请求类型是 multipart 将经过 MultipartResolver 进行文件上传解析;
  2. 经过 HandlerMapping,将请求映射处处理器(返回一个 HandlerExecutionChain,它包括一个处理器、多个 HandlerInterceptor 拦截器);
  3. 经过 HandlerAdapter 支持多种类型的处理器(HandlerExecutionChain 中的处理器);
  4. 经过 ViewResolver 解析逻辑视图名到具体视图实现;
  5. 本地化解析;
  6. 渲染具体的视图等;
  7. 若是执行过程当中遇到异常将交给 HandlerExceptionResolver 来解析

5.2 DispathcherServlet配置详解

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
复制代码
  • load-on-startup:表示启动容器时初始化该 Servlet;
  • url-pattern:表示哪些请求交给 Spring Web MVC 处理, "/" 是用来定义默认 servlet 映射的。也能够如 *.html 表示拦截全部以 html 为扩展名的请求
  • contextConfigLocation:表示 SpringMVC 配置文件的路径

其余的参数配置:

参数 描述
contextClass 实现WebApplicationContext接口的类,当前的servlet用它来建立上下文。若是这个参数没有指定, 默认使用XmlWebApplicationContext。
contextConfigLocation 传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。这个字符串能够被分红多个字符串(使用逗号做为分隔符) 来支持多个上下文(在多上下文的状况下,若是同一个bean被定义两次,后面一个优先)。
namespace WebApplicationContext命名空间。默认值是[server-name]-servlet。

5.3 Spring 配置

以前的案例中,只有 SpringMVC,没有 Spring,Web 项目也是能够运行的。在实际开发中,Spring 和 SpringMVC 是分开配置的,因此咱们对上面的项目继续进行完善,添加 Spring 相关配置。

首先,项目添加一个 service 包,提供一个 HelloService 类,以下:

@Service
public class HelloService {
    public String hello(String name) {
        return "hello " + name;
    }
}
复制代码

如今,假设我须要将 HelloService 注入到 Spring 容器中并使用它,这个是属于 Spring 层的 Bean,因此咱们通常将除了 Controller 以外的全部 Bean 注册到 Spring 容器中,而将 Controller 注册到 SpringMVC 容器中,如今,在 resources 目录下添加 applicationContext.xml 做为 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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.javaboy" use-default-filters="true">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
</beans>
复制代码

可是,这个配置文件,默认状况下,并不会被自动加载,因此,须要咱们在 web.xml 中对其进行配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
复制代码

首先经过 context-param 指定 Spring 配置文件的位置,这个配置文件也有一些默认规则,它的配置文件名默认就叫 applicationContext.xml ,而且,若是你将这个配置文件放在 WEB-INF 目录下,那么这里就能够不用指定配置文件位置了,只须要指定监听器就能够了。这段配置是 Spring 集成 Web 环境的通用配置;通常用于加载除 Web 层的 Bean(如DAO、Service 等),以便于与其余任何Web框架集成。

  • contextConfigLocation:表示用于加载 Bean 的配置文件;
  • contextClass:表示用于加载 Bean 的 ApplicationContext 实现类,默认 WebApplicationContext。

配置完成以后,还须要修改 MyController,在 MyController 中注入 HelloSerivce:

@org.springframework.stereotype.Controller("/hello")
public class MyController implements Controller {
    @Autowired
    HelloService helloService;
    /** * 这就是一个请求处理接口 * @param req 这就是前端发送来的请求 * @param resp 这就是服务端给前端的响应 * @return 返回值是一个 ModelAndView,Model 至关因而咱们的数据模型,View 是咱们的视图 * @throws Exception */
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        System.out.println(helloService.hello("javaboy"));
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("name", "javaboy");
        return mv;
    }
}
复制代码

注意

为了在 SpringMVC 容器中可以扫描到 MyController ,这里给 MyController 添加了 @Controller 注解,同时,因为咱们目前采用的 HandlerMapping 是 BeanNameUrlHandlerMapping(意味着请求地址就是处理器 Bean 的名字),因此,还须要手动指定 MyController 的名字。

最后,修改 SpringMVC 的配置文件,将 Bean 配置为扫描形式:

<context:component-scan base-package="org.javaboy.helloworld" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--这个是处理器映射器,这种方式,请求地址其实就是一个 Bean 的名字,而后根据这个 bean 的名字查找对应的处理器-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
    <property name="beanName" value="/hello"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
    <property name="prefix" value="/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>
复制代码

配置完成后,再次启动项目,Spring 容器也将会被建立。访问 /hello 接口,HelloService 中的 hello 方法就会自动被调用。

5.4 两个容器

当 Spring 和 SpringMVC 同时出现,咱们的项目中将存在两个容器,一个是 Spring 容器,另外一个是 SpringMVC 容器,Spring 容器经过 ContextLoaderListener 来加载,SpringMVC 容器则经过 DispatcherServlet 来加载,这两个容器不同:

从图中能够看出:

  • ContextLoaderListener 初始化的上下文加载的 Bean 是对于整个应用程序共享的,无论是使用什么表现层技术,通常如 DAO 层、Service 层 Bean;
  • DispatcherServlet 初始化的上下文加载的 Bean 是只对 Spring Web MVC 有效的 Bean,如 Controller、HandlerMapping、HandlerAdapter 等等,该初始化上下文应该只加载 Web相关组件。
  1. 为何不在 Spring 容器中扫描全部 Bean?

这个是不可能的。由于请求达到服务端后,找 DispatcherServlet 去处理,只会去 SpringMVC 容器中找,这就意味着 Controller 必须在 SpringMVC 容器中扫描。

2.为何不在 SpringMVC 容器中扫描全部 Bean?

这个是能够的,能够在 SpringMVC 容器中扫描全部 Bean。不写在一块儿,有两个方面的缘由:

  1. 为了方便配置文件的管理
  2. 在 Spring+SpringMVC+Hibernate 组合中,实际上也不支持这种写法

6. 处理器详解

6.1 HandlerMapping

注意,下文所说的处理器即咱们平时所见到的 Controller

HandlerMapping ,中文译做处理器映射器,在 SpringMVC 中,系统提供了不少 HandlerMapping:

HandlerMapping 是负责根据 request 请求找到对应的 Handler 处理器及 Interceptor 拦截器,将它们封装在 HandlerExecutionChain 对象中返回给前端控制器。

  • BeanNameUrlHandlerMapping

BeanNameUrl 处理器映射器,根据请求的 url 与 Spring 容器中定义的 bean 的 name 进行匹配,从而从 Spring 容器中找到 bean 实例,就是说,请求的 Url 地址就是处理器 Bean 的名字。

这个 HandlerMapping 配置以下:

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
    <property name="beanName" value="/hello"/>
</bean>
复制代码
  • SimpleUrlHandlerMapping

SimpleUrlHandlerMapping 是 BeanNameUrlHandlerMapping 的加强版本,它能够将 url 和处理器 bean 的 id 进行统一映射配置:

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello">myController</prop>
            <prop key="/hello2">myController2</prop>
        </props>
    </property>
</bean>
复制代码

注意,在 props 中,能够配置多个请求路径和处理器实例的映射关系。

6.2 HandlerAdapter

HandlerAdapter,中文译做处理器适配器。

HandlerAdapter 会根据适配器接口对后端控制器进行包装(适配),包装后便可对处理器进行执行,经过扩展处理器适配器能够执行多种类型的处理器,这里使用了适配器设计模式。

在 SpringMVC 中,HandlerAdapter 也有诸多实现类:

  • SimpleControllerHandlerAdapter

SimpleControllerHandlerAdapter 简单控制器处理器适配器,全部实现了 org.springframework.web.servlet.mvc.Controller 接口的 Bean 经过此适配器进行适配、执行,也就是说,若是咱们开发的接口是经过实现 Controller 接口来完成的(不是经过注解开发的接口),那么 HandlerAdapter 必须是 SimpleControllerHandlerAdapter。

<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
复制代码
  • HttpRequestHandlerAdapter

HttpRequestHandlerAdapter,http 请求处理器适配器,全部实现了 org.springframework.web.HttpRequestHandler 接口的 Bean 经过此适配器进行适配、执行。

例如存在以下接口:

@Controller
public class MyController2 implements HttpRequestHandler {
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("-----MyController2-----");
    }
}
复制代码
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello2">myController2</prop>
        </props>
    </property>
</bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" id="handlerAdapter"/>
复制代码

6.3 最佳实践

各类状况都大概了解了,咱们看下项目中的具体实践。

  • 组件自动扫描

web 开发中,咱们基本上再也不经过 XML 或者 Java 配置来建立一个 Bean 的实例,而是直接经过组件扫描来实现 Bean 的配置,若是要扫描多个包,多个包之间用 , 隔开便可:

<context:component-scan base-package="org.sang"/>
复制代码
  • HandlerMapping

正常状况下,咱们在项目中使用的是 RequestMappingHandlerMapping,这个是根据处理器中的注解,来匹配请求(即 @RequestMapping 注解中的 url 属性)。由于在上面咱们都是经过实现类来开发接口的,至关于仍是一个类一个接口,因此,咱们能够经过 RequestMappingHandlerMapping 来作处理器映射器,这样咱们能够在一个类中开发出多个接口。

  • HandlerAdapter

对于上面提到的经过 @RequestMapping 注解所定义出来的接口方法,这些方法的调用都是要经过 RequestMappingHandlerAdapter 这个适配器来实现。

例如咱们开发一个接口:

@Controller
public class MyController3 {
    @RequestMapping("/hello3")
    public ModelAndView hello() {
        return new ModelAndView("hello3");
    }
}
复制代码

要可以访问到这个接口,咱们须要 RequestMappingHandlerMapping 才能定位到须要执行的方法,须要 RequestMappingHandlerAdapter,才能执行定位到的方法,修改 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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.javaboy.helloworld"/>

    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" id="handlerMapping"/>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" id="handlerAdapter"/>
    <!--视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>
复制代码

而后,启动项目,访问 /hello3 接口,就能够看到相应的页面了。

  • 继续优化

因为开发中,咱们经常使用的是 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter ,这两个有一个简化的写法,以下:

<mvc:annotation-driven>
复制代码

能够用这一行配置,代替 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 的两行配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="org.javaboy.helloworld"/>

    <mvc:annotation-driven/>
    <!--视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>
复制代码

访问效果和上一步的效果同样。这是咱们实际开发中,最终配置的形态。

7.1 @RequestMapping

这个注解用来标记一个接口,这算是咱们在接口开发中,使用最多的注解之一。

7.1.1 请求 URL

标记请求 URL 很简单,只须要在相应的方法上添加该注解便可:

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}
复制代码

这里 @RequestMapping("/hello") 表示当请求地址为 /hello 的时候,这个方法会被触发。其中,地址能够是多个,就是能够多个地址映射到同一个方法。

@Controller
public class HelloController {
    @RequestMapping({"/hello","/hello2"})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}
复制代码

这个配置,表示 /hello 和 /hello2 均可以访问到该方法。

7.1.2 请求窄化

同一个项目中,会存在多个接口,例如订单相关的接口都是 /order/xxx 格式的,用户相关的接口都是 /user/xxx 格式的。为了方便处理,这里的前缀(就是 /order、/user)能够统一在 Controller 上面处理。

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping({"/hello","/hello2"})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}
复制代码

当类上加了 @RequestMapping 注解以后,此时,要想访问到 hello ,地址就应该是 /user/hello 或者 /user/hello2

7.1.3 请求方法限定

默认状况下,使用 @RequestMapping 注解定义好的方法,能够被 GET 请求访问到,也能够被 POST 请求访问到,可是 DELETE 请求以及 PUT 请求不能够访问到。

固然,咱们也能够指定具体的访问方法:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}
复制代码

经过 @RequestMapping 注解,指定了该接口只能被 GET 请求访问到,此时,该接口就不能够被 POST 以及请求请求访问到了。强行访问会报以下错误:

固然,限定的方法也能够有多个:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping(value = "/hello",method = {RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}
复制代码

此时,这个接口就能够被 GET、POST、PUT、以及 DELETE 访问到了。可是,因为 JSP 支支持 GET、POST 以及 HEAD ,因此这个测试,不能使用 JSP 作页面模板。能够讲视图换成其余的,或者返回 JSON,这里就不影响了。

7.2 Controller 方法的返回值

7.2.1 返回 ModelAndView

若是是先后端不分的开发,大部分状况下,咱们返回 ModelAndView,即数据模型+视图:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping("/hello")
    public ModelAndView hello() {
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("username", "javaboy");
        return mv;
    }
}
复制代码

Model 中,放咱们的数据,而后在 ModelAndView 中指定视图名称。

7.2.2 返回 Void

没有返回值。没有返回值,并不必定真的没有返回值,只是方法的返回值为 void,咱们能够经过其余方式给前端返回。实际上,这种方式也能够理解为 Servlet 中的那一套方案。

注意,因为默认的 Maven 项目没有 Servlet,所以这里须要额外添加一个依赖:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
复制代码
  • 经过 HttpServletRequest 作服务端跳转
@RequestMapping("/hello2")
public void hello2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.getRequestDispatcher("/jsp/hello.jsp").forward(req,resp);//服务器端跳转
}
复制代码
  • 经过 HttpServletResponse 作重定向
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.sendRedirect("/hello.jsp");
}
复制代码

也能够本身手动指定响应头去实现重定向:

@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.setStatus(302);
    resp.addHeader("Location", "/jsp/hello.jsp");
}
复制代码
  • 经过 HttpServletResponse 给出响应
@RequestMapping("/hello4")
public void hello4(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.setContentType("text/html;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("hello javaboy!");
    out.flush();
    out.close();
}
复制代码

这种方式,既能够返回 JSON,也能够返回普通字符串。

7.2.3 返回字符串

  • 返回逻辑视图名

前面的 ModelAndView 能够拆分为两部分,Model 和 View,在 SpringMVC 中,Model 咱们能够直接在参数中指定,而后返回值是逻辑视图名:

@RequestMapping("/hello5")
public String hello5(Model model) {
    model.addAttribute("username", "javaboy");//这是数据模型
    return "hello";//表示去查找一个名为 hello 的视图
}
复制代码
  • 服务端跳转
@RequestMapping("/hello5")
public String hello5() {
    return "forward:/jsp/hello.jsp";
}
复制代码

forward 后面跟上跳转的路径。

  • 客户端跳转
@RequestMapping("/hello5")
public String hello5() {
    return "redirect:/user/hello";
}
复制代码

这种,本质上就是浏览器重定向。

  • 真的返回一个字符串

上面三个返回的字符串,都是由特殊含义的,若是必定要返回一个字符串,须要额外添加一个注意:@ResponseBody ,这个注解表示当前方法的返回值就是要展现出来返回值,没有特殊含义。

@RequestMapping("/hello5")
@ResponseBody
public String hello5() {
    return "redirect:/user/hello";
}
复制代码

上面代码表示就是想返回一段内容为 redirect:/user/hello 的字符串,他没有特殊含义。注意,这里若是单纯的返回一个中文字符串,是会乱码的,能够在 @RequestMapping 中添加 produces 属性来解决:

@RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8")
@ResponseBody
public String hello5() {
    return "Java 语言程序设计";
}
复制代码

7.3 参数绑定

7.3.1 默认支持的参数类型

默认支持的参数类型,就是能够直接写在 @RequestMapping 所注解的方法中的参数类型,一共有四类:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Model/ModelMap

这几个例子能够参考上一小节。

在请求的方法中,默认的参数就是这几个,若是在方法中,恰好须要这几个参数,那么就能够把这几个参数加入到方法中。

7.3.2 简单数据类型

Integer、Boolean、Double 等等简单数据类型也都是支持的。例如添加一本书:

首先,在 /jsp/ 目录下建立 add book.jsp 做为图书添加页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>书名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>做者:</td>
            <td><input type="text" name="author"></td>
        </tr>
        <tr>
            <td>价格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic">是
                <input type="radio" value="false" name="ispublic">否
            </td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="添加">
           </td>
        </tr>
    </table>
</form>
</body>
</html>
复制代码

建立控制器,控制器提供两个功能,一个是访问 jsp 页面,另外一个是提供添加接口:

@Controller
public class BookController {
    @RequestMapping("/book")
    public String addBook() {
        return "addbook";
    }

    @RequestMapping(value = "/doAdd",method = RequestMethod.POST)
    @ResponseBody
    public void doAdd(String name,String author,Double price,Boolean ispublic) {
        System.out.println(name);
        System.out.println(author);
        System.out.println(price);
        System.out.println(ispublic);
    }
}
复制代码

注意,因为 doAdd 方法确实不想返回任何值,因此须要给该方法添加 @ResponseBody 注解,表示这个方法到此为止,不用再去查找相关视图了。另外, POST 请求传上来的中文会乱码,因此,咱们在 web.xml 中再额外添加一个编码过滤器:

<filter>
    <filter-name>encoding</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>
    <init-param>
        <param-name>forceRequestEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encoding</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
复制代码

最后,浏览器中输入 http://localhost:8080/book ,就能够执行添加操做,服务端会打印出来相应的日志。

在上面的绑定中,有一个要求,表单中字段的 name 属性要和接口中的变量名一一对应,才能映射成功,不然服务端接收不到前端传来的数据。有一些特殊状况,咱们的服务端的接口变量名可能和前端不一致,这个时候咱们能够经过 @RequestParam 注解来解决。

  • @RequestParam

这个注解的的功能主要有三方面:

  1. 给变量取别名
  2. 设置变量是否必填
  3. 给变量设置默认值

以下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam("name") String bookname, String author, Double price, Boolean ispublic) {
    System.out.println(bookname);
    System.out.println(author);
    System.out.println(price);
    System.out.println(ispublic);
}
复制代码

注解中的 “name” 表示给 bookname 这个变量取的别名,也就是说,bookname 将接收前端传来的 name 这个变量的值。在这个注解中,还能够添加 required 属性和 defaultValue 属性,以下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam(value = "name",required = true,defaultValue = "三国演义") String bookname, String author, Double price, Boolean ispublic) {
    System.out.println(bookname);
    System.out.println(author);
    System.out.println(price);
    System.out.println(ispublic);
}
复制代码

required 属性默认为 true,即只要添加了 @RequestParam 注解,这个参数默认就是必填的,若是不填,请求没法提交,会报 400 错误,若是这个参数不是必填项,能够手动把 required 属性设置为 false。可是,若是同时设置了 defaultValue,这个时候,前端不传该参数到后端,即便 required 属性为 true,它也不会报错。

7.3.3 实体类

参数除了是简单数据类型以外,也能够是实体类。实际上,在开发中,大部分状况下,都是实体类。

仍是上面的例子,咱们改用一个 Book 对象来接收前端传来的数据:

public class Book {
    private String name;
    private String author;
    private Double price;
    private Boolean ispublic;

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", ispublic=" + ispublic +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Boolean getIspublic() {
        return ispublic;
    }

    public void setIspublic(Boolean ispublic) {
        this.ispublic = ispublic;
    }
}
复制代码

服务端接收数据方式以下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book) {
    System.out.println(book);
}
复制代码

前端页面传值的时候和上面的同样,只须要写属性名就能够了,不须要写 book 对象名。

固然,对象中可能还有对象。例如以下对象:

public class Book {
    private String name;
    private Double price;
    private Boolean ispublic;
    private Author author;

    public void setAuthor(Author author) {
        this.author = author;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                ", ispublic=" + ispublic +
                ", author=" + author +
                '}';
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Boolean getIspublic() {
        return ispublic;
    }

    public void setIspublic(Boolean ispublic) {
        this.ispublic = ispublic;
    }
}
public class Author {
    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

Book 对象中,有一个 Author 属性,如何给 Author 属性传值呢?前端写法以下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>书名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>做者姓名:</td>
            <td><input type="text" name="author.name"></td>
        </tr>
        <tr>
            <td>做者年龄:</td>
            <td><input type="text" name="author.age"></td>
        </tr>
        <tr>
            <td>价格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic">是
                <input type="radio" value="false" name="ispublic">否
            </td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="添加">
           </td>
        </tr>
    </table>
</form>
</body>
</html>
复制代码

这样在后端直接用 Book 对象就能够接收到全部数据了。

7.3.4 自定义参数绑定

前面的转换,都是系统自动转换的,这种转换仅限于基本数据类型。特殊的数据类型,系统没法自动转换,例如日期。例如前端传一个日期到后端,后端不是用字符串接收,而是使用一个 Date 对象接收,这个时候就会出现参数类型转换失败。这个时候,须要咱们手动定义参数类型转换器,将日期字符串手动转为一个 Date 对象。

@Component
public class DateConverter implements Converter<String, Date> {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    public Date convert(String source) {
        try {
            return sdf.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}
复制代码

在自定义的参数类型转换器中,将一个 String 转为 Date 对象,同时,将这个转换器注册为一个 Bean。

接下来,在 SpringMVC 的配置文件中,配置该 Bean,使之生效。

<mvc:annotation-driven conversion-service="conversionService"/>
<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService">
    <property name="converters">
        <set>
            <ref bean="dateConverter"/>
        </set>
    </property>
</bean>
复制代码

配置完成后,在服务端就能够接收前端传来的日期参数了。

7.3.5 集合类的参数

  • String 数组

String 数组能够直接用数组去接收,前端传递的时候,数组的传递其实就多相同的 key,这种通常用在 checkbox 中较多。

例如前端增长兴趣爱好一项:

<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>书名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>做者姓名:</td>
            <td><input type="text" name="author.name"></td>
        </tr>
        <tr>
            <td>做者年龄:</td>
            <td><input type="text" name="author.age"></td>
        </tr>
        <tr>
            <td>出生日期:</td>
            <td><input type="date" name="author.birthday"></td>
        </tr>
        <tr>
            <td>兴趣爱好:</td>
            <td>
                <input type="checkbox" name="favorites" value="足球">足球
                <input type="checkbox" name="favorites" value="篮球">篮球
                <input type="checkbox" name="favorites" value="乒乓球">乒乓球
            </td>
        </tr>
        <tr>
            <td>价格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic"><input type="radio" value="false" name="ispublic"></td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="添加">
           </td>
        </tr>
    </table>
</form>
复制代码

在服务端用一个数组去接收 favorites 对象:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book,String[] favorites) {
    System.out.println(Arrays.toString(favorites));
    System.out.println(book);
}
复制代码

注意,前端传来的数组对象,服务端不可使用 List 集合去接收。

  • List 集合

若是须要使用 List 集合接收前端传来的数据,List 集合自己须要放在一个封装对象中,这个时候,List 中,能够是基本数据类型,也能够是对象。例若有一个班级类,班级里边有学生,学生有多个:

public class MyClass {
    private Integer id;
    private List<Student> students;

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", students=" + students +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }
}
public class Student {
    private Integer id;
    private String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

添加班级的时候,能够传递多个 Student,前端页面写法以下:

<form action="/addclass" method="post">
    <table>
        <tr>
            <td>班级编号:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="students[0].id"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="students[0].name"></td>
        </tr>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="students[1].id"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="students[1].name"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

服务端直接接收数据便可:

@RequestMapping("/addclass")
@ResponseBody
public void addClass(MyClass myClass) {
    System.out.println(myClass);
}
复制代码
  • Map

相对于实体类而言,Map 是一种比较灵活的方案,可是,Map 可维护性比较差,所以通常不推荐使用。

例如给上面的班级类添加其余属性信息:

public class MyClass {
    private Integer id;
    private List<Student> students;
    private Map<String, Object> info;

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", students=" + students +
                ", info=" + info +
                '}';
    }

    public Map<String, Object> getInfo() {
        return info;
    }

    public void setInfo(Map<String, Object> info) {
        this.info = info;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }
}
复制代码

在前端,经过以下方式给 info 这个 Map 赋值。

<form action="/addclass" method="post">
    <table>
        <tr>
            <td>班级编号:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>班级名称:</td>
            <td><input type="text" name="info['name']"></td>
        </tr>
        <tr>
            <td>班级位置:</td>
            <td><input type="text" name="info['pos']"></td>
        </tr>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="students[0].id"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="students[0].name"></td>
        </tr>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="students[1].id"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="students[1].name"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

8. 文件上传

SpringMVC 中对文件上传作了封装,咱们能够更加方便的实现文件上传。从 Spring3.1 开始,对于文件上传,提供了两个处理器:

  • CommonsMultipartResolver
  • StandardServletMultipartResolver

第一个处理器兼容性较好,能够兼容 Servlet3.0 以前的版本,可是它依赖了 commons-fileupload 这个第三方工具,因此若是使用这个,必定要添加 commons-fileupload 依赖。

第二个处理器兼容性较差,它适用于 Servlet3.0 以后的版本,它不依赖第三方工具,使用它,能够直接作文件上传。

8.1 CommonsMultipartResolver

使用 CommonsMultipartResolver 作文件上传,须要首先添加 commons-fileupload 依赖,以下:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
复制代码

而后,在 SpringMVC 的配置文件中,配置 MultipartResolver:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver"/>
复制代码

注意,这个 Bean 必定要有 id,而且 id 必须是 multipartResolver

接下来,建立 jsp 页面:

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="上传">
</form>
复制代码

注意文件上传请求是 POST 请求,enctype 必定是 multipart/form-data

而后,开发文件上传接口:

@Controller
public class FileUploadController {
    SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");

    @RequestMapping("/upload")
    @ResponseBody
    public String upload(MultipartFile file, HttpServletRequest req) {
        String format = sdf.format(new Date());
        String realPath = req.getServletContext().getRealPath("/img") + format;
        File folder = new File(realPath);
        if (!folder.exists()) {
            folder.mkdirs();
        }
        String oldName = file.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        try {
            file.transferTo(new File(folder, newName));
            String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
            return url;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "failed";
    }
}
复制代码

这个文件上传方法中,一共作了四件事:

  1. 解决文件保存路径,这里是保存在项目运行目录下的 img 目录下,而后利用日期继续宁分类
  2. 处理文件名问题,使用 UUID 作新的文件名,用来代替旧的文件名,能够有效防止文件名冲突
  3. 保存文件
  4. 生成文件访问路径

这里还有一个小问题,在 SpringMVC 中,静态资源默认都是被自动拦截的,没法访问,意味着上传成功的图片没法访问,所以,还须要咱们在 SpringMVC 的配置文件中,再添加以下配置:

<mvc:resources mapping="/**" location="/"/>
复制代码

完成以后,就能够访问 jsp 页面,作文件上传了。

固然,默认的配置不必定知足咱们的需求,咱们还能够本身手动配置文件上传大小等:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
    <!--默认的编码-->
    <property name="defaultEncoding" value="UTF-8"/>
    <!--上传的总文件大小-->
    <property name="maxUploadSize" value="1048576"/>
    <!--上传的单个文件大小-->
    <property name="maxUploadSizePerFile" value="1048576"/>
    <!--内存中最大的数据量,超过这个数据量,数据就要开始往硬盘中写了-->
    <property name="maxInMemorySize" value="4096"/>
    <!--临时目录,超过 maxInMemorySize 配置的大小后,数据开始往临时目录写,等所有上传完成后,再将数据合并到正式的文件上传目录-->
    <property name="uploadTempDir" value="file:///E:\\tmp"/>
</bean>
复制代码

8.2 StandardServletMultipartResolver

这种文件上传方式,不须要依赖第三方 jar(主要是不须要添加 commons-fileupload 这个依赖),可是也不支持 Servlet3.0 以前的版本。

使用 StandardServletMultipartResolver ,那咱们首先在 SpringMVC 的配置文件中,配置这个 Bean:

<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver">
</bean>
复制代码

注意,这里 Bean 的名字依然叫 multipartResolver

配置完成后,注意,这个 Bean 没法直接配置上传文件大小等限制。须要在 web.xml 中进行配置(这里,即便不须要限制文件上传大小,也须要在 web.xml 中配置 multipart-config):

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <multipart-config>
        <!--文件保存的临时目录,这个目录系统不会主动建立-->
        <location>E:\\temp</location>
        <!--上传的单个文件大小-->
        <max-file-size>1048576</max-file-size>
        <!--上传的总文件大小-->
        <max-request-size>1048576</max-request-size>
        <!--这个就是内存中保存的文件最大大小-->
        <file-size-threshold>4096</file-size-threshold>
    </multipart-config>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
复制代码

配置完成后,就能够测试文件上传了,测试方式和上面同样。

8.3 多文件上传

多文件上传分为两种,一种是 key 相同的文件,另外一种是 key 不一样的文件。

8.3.1 key 相同的文件

这种上传,前端页面通常以下:

<form action="/upload2" method="post" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <input type="submit" value="上传">
</form>
复制代码

主要是 input 节点中多了 multiple 属性。后端用一个数组来接收文件便可:

@RequestMapping("/upload2")
@ResponseBody
public void upload2(MultipartFile[] files, HttpServletRequest req) {
    String format = sdf.format(new Date());
    String realPath = req.getServletContext().getRealPath("/img") + format;
    File folder = new File(realPath);
    if (!folder.exists()) {
        folder.mkdirs();
    }
    try {
        for (MultipartFile file : files) {
            String oldName = file.getOriginalFilename();
            String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
            file.transferTo(new File(folder, newName));
            String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
            System.out.println(url);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

8.3.2 key 不一样的文件

key 不一样的,通常前端定义以下:

<form action="/upload3" method="post" enctype="multipart/form-data">
    <input type="file" name="file1">
    <input type="file" name="file2">
    <input type="submit" value="上传">
</form>
复制代码

这种,在后端用不一样的变量来接收就好了:

@RequestMapping("/upload3")
@ResponseBody
public void upload3(MultipartFile file1, MultipartFile file2, HttpServletRequest req) {
    String format = sdf.format(new Date());
    String realPath = req.getServletContext().getRealPath("/img") + format;
    File folder = new File(realPath);
    if (!folder.exists()) {
        folder.mkdirs();
    }
    try {
        String oldName = file1.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        file1.transferTo(new File(folder, newName));
        String url1 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
        System.out.println(url1);
        String oldName2 = file2.getOriginalFilename();
        String newName2 = UUID.randomUUID().toString() + oldName2.substring(oldName2.lastIndexOf("."));
        file2.transferTo(new File(folder, newName2));
        String url2 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName2;
        System.out.println(url2);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

9. 全局异常处理

项目中,可能会抛出多个异常,咱们不能够直接将异常的堆栈信息展现给用户,有两个缘由:

  1. 用户体验很差
  2. 很是不安全

因此,针对异常,咱们能够自定义异常处理,SpringMVC 中,针对全局异常也提供了相应的解决方案,主要是经过 @ControllerAdvice 和 @ExceptionHandler 两个注解来处理的。

以第八节的文件上传大小超出限制为例,自定义异常,只须要提供一个异常处理类便可:

@ControllerAdvice//表示这是一个加强版的 Controller,主要用来作全局数据处理
public class MyException {
    @ExceptionHandler(Exception.class)
    public ModelAndView fileuploadException(Exception e) {
        ModelAndView error = new ModelAndView("error");
        error.addObject("error", e.getMessage());
        return error;
    }
}
复制代码

在这里:

  • @ControllerAdvice 表示这是一个加强版的 Controller,主要用来作全局数据处理
  • @ExceptionHandler 表示这是一个异常处理方法,这个注解的参数,表示须要拦截的异常,参数为 Exception 表示拦截全部异常,这里也能够具体到某一个异常,若是具体到某一个异常,那么发生了其余异常则不会被拦截到。
  • 异常方法的定义,和 Controller 中方法的定义同样,能够返回 ModelAndview,也能够返回 String 或者 void

例如以下代码,指挥拦截文件上传异常,其余异常和它不要紧,不会进入到自定义异常处理的方法中来。

@ControllerAdvice//表示这是一个加强版的 Controller,主要用来作全局数据处理
public class MyException {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ModelAndView fileuploadException(MaxUploadSizeExceededException e) {
        ModelAndView error = new ModelAndView("error");
        error.addObject("error", e.getMessage());
        return error;
    }
}
复制代码

10. 服务端数据校验

B/S 系统中对 http 请求数据的校验多数在客户端进行,这也是出于简单及用户体验性上考虑,可是在一些安全性要求高的系统中服务端校验是不可缺乏的,实际上,几乎全部的系统,凡是涉及到数据校验,都须要在服务端进行二次校验。为何要在服务端进行二次校验呢?这须要理解客户端校验和服务端校验各自的目的。

  1. 客户端校验,咱们主要是为了提升用户体验,例如用户输入一个邮箱地址,要校验这个邮箱地址是否合法,没有必要发送到服务端进行校验,直接在前端用 js 进行校验便可。可是你们须要明白的是,前端校验没法代替后端校验,前端校验能够有效的提升用户体验,可是没法确保数据完整性,由于在 B/S 架构中,用户能够方便的拿到请求地址,而后直接发送请求,传递非法参数。
  2. 服务端校验,虽然用户体验很差,可是能够有效的保证数据安全与完整性。
  3. 综上,实际项目中,两个一块儿用。

Spring 支持 JSR-303 验证框架,JSR-303 是 JAVA EE 6 中的一项子规范,叫作 Bean Validation,官方参考实现是 Hibernate Validator(与Hibernate ORM 没有关系),JSR-303 用于对 Java Bean 中的字段的值进行验证。

10.1 普通校验

普通校验,是这里最基本的用法。

首先,咱们须要加入校验须要的依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>
复制代码

接下来,在 SpringMVC 的配置文件中配置校验的 Bean:

<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>
复制代码

配置时,提供一个 LocalValidatorFactoryBean 的实例,而后 Bean 的校验使用 HibernateValidator。

这样,配置就算完成了。

接下来,咱们提供一个添加学生的页面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>学生邮箱:</td>
            <td><input type="text" name="email"></td>
        </tr>
        <tr>
            <td>学生年龄:</td>
            <td><input type="text" name="age"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

在这里须要提交的数据中,假设学生编号不能为空,学生姓名长度不能超过 10 且不能为空,邮箱地址要合法,年龄不能超过 150。那么在定义实体类的时候,就能够加入这个判断条件了。

public class Student {
    @NotNull
    private Integer id;
    @NotNull
    @Size(min = 2,max = 10)
    private String name;
    @Email
    private String email;
    @Max(150)
    private Integer age;

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

在这里:

  • @NotNull 表示这个字段不能为空
  • @Size 中描述了这个字符串长度的限制
  • @Email 表示这个字段的值必须是一个邮箱地址
  • @Max 表示这个字段的最大值

定义完成后,接下来,在 Controller 中定义接口:

@Controller
public class StudentController {
    @RequestMapping("/addstudent")
    @ResponseBody
    public void addStudent(@Validated Student student, BindingResult result) {
        if (result != null) {
            //校验未经过,获取全部的异常信息并展现出来
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
            }
        }
    }
}
复制代码

在这里:

  • @Validated 表示 Student 中定义的校验规则将会生效
  • BindingResult 表示出错信息,若是这个变量不为空,表示有错误,不然校验经过。

接下来就能够启动项目了。访问 jsp 页面,而后添加 Student,查看校验规则是否生效。

默认状况下,打印出来的错误信息时系统默认的错误信息,这个错误信息,咱们也能够自定义。自定义方式以下:

因为 properties 文件中的中文会乱码,因此须要咱们先修改一下 IDEA 配置,点 File-->Settings->Editor-->File Encodings,以下:

而后定义错误提示文本,在 resources 目录下新建一个 MyMessage.properties 文件,内容以下:

student.id.notnull=id 不能为空
student.name.notnull=name 不能为空
student.name.length=name 最小长度为 2 ,最大长度为 10
student.email.error=email 地址非法
student.age.error=年龄不能超过 150
复制代码

接下来,在 SpringMVC 配置中,加载这个配置文件:

<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <property name="validationMessageSource" ref="bundleMessageSource"/>
</bean>
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="bundleMessageSource">
    <property name="basenames">
        <list>
            <value>classpath:MyMessage</value>
        </list>
    </property>
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="cacheSeconds" value="300"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>
复制代码

最后,在实体类上的注解中,加上校验出错时的信息:

public class Student {
    @NotNull(message = "{student.id.notnull}")
    private Integer id;
    @NotNull(message = "{student.name.notnull}")
    @Size(min = 2,max = 10,message = "{student.name.length}")
    private String name;
    @Email(message = "{student.email.error}")
    private String email;
    @Max(value = 150,message = "{student.age.error}")
    private Integer age;

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

配置完成后,若是校验再出错,就会展现咱们本身的出错信息了。

10.2 分组校验

因为校验规则都是定义在实体类上面的,可是,在不一样的数据提交环境下,校验规则可能不同。例如,用户的 id 是自增加的,添加的时候,能够不用传递用户 id,可是修改的时候则必须传递用户 id,这种状况下,就须要使用分组校验。

分组校验,首先须要定义校验组,所谓的校验组,其实就是空接口:

public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}
复制代码

而后,在实体类中,指定每个校验规则所属的组:

public class Student {
    @NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class)
    private Integer id;
    @NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    @Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    private String name;
    @Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    private String email;
    @Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class})
    private Integer age;

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

在 group 中指定每个校验规则所属的组,一个规则能够属于一个组,也能够属于多个组。

最后,在接收参数的地方,指定校验组:

@Controller
public class StudentController {
    @RequestMapping("/addstudent")
    @ResponseBody
    public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
        if (result != null) {
            //校验未经过,获取全部的异常信息并展现出来
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
            }
        }
    }
}
复制代码

配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。

10.3 校验注解

校验注解,主要有以下几种:

  • @Null 被注解的元素必须为 null
  • @NotNull 被注解的元素必须不为 null
  • @AssertTrue 被注解的元素必须为 true
  • @AssertFalse 被注解的元素必须为 false
  • @Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注解的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注解的元素必须是一个过去的日期
  • @Future 被注解的元素必须是一个未来的日期
  • @Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式
  • @NotBlank(message =) 验证字符串非 null,且长度必须大于0
  • @Email 被注解的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
  • @NotEmpty 被注解的字符串的必须非空
  • @Range(min=,max=,message=) 被注解的元素必须在合适的范围内

11.1 数据回显基本用法

数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。通常来讲,若是使用 Ajax 来作数据提交,基本上是没有数据回显这个需求的,可是若是是经过表单作数据提交,那么数据回显就很是有必要了。

11.1.1 简单数据类型

简单数据类型,实际上框架在这里没有提供任何形式的支持,就是咱们本身手动配置。咱们继续在第 10 小节的例子上演示 Demo。加入提交的 Student 数据不符合要求,那么从新回到添加 Student 页面,而且预设以前已经填好的数据。

首先咱们先来改造一下 student.jsp 页面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="id" value="${id}"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="name" value="${name}"></td>
        </tr>
        <tr>
            <td>学生邮箱:</td>
            <td><input type="text" name="email" value="${email}"></td>
        </tr>
        <tr>
            <td>学生年龄:</td>
            <td><input type="text" name="age" value="${age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

在接收数据时,使用简单数据类型去接收:

@RequestMapping("/addstudent")
public String addStudent2(Integer id, String name, String email, Integer age, Model model) {
    model.addAttribute("id", id);
    model.addAttribute("name", name);
    model.addAttribute("email", email);
    model.addAttribute("age", age);
    return "student";
}
复制代码

这种方式,至关于框架没有作任何工做,就是咱们手动作数据回显的。此时访问页面,服务端会再次定位到该页面,并且数据已经预填好。

11.1.2 实体类

上面这种简单数据类型的回显,实际上很是麻烦,由于须要开发者在服务端一个一个手动设置。若是使用对象的话,就没有这么麻烦了,由于 SpringMVC 在页面跳转时,会自动将对象填充进返回的数据中。

此时,首先修改一下 student.jsp 页面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="id" value="${student.id}"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="name" value="${student.name}"></td>
        </tr>
        <tr>
            <td>学生邮箱:</td>
            <td><input type="text" name="email" value="${student.email}"></td>
        </tr>
        <tr>
            <td>学生年龄:</td>
            <td><input type="text" name="age" value="${student.age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

注意,在预填数据中,多了一个 student. 前缀。这 student 就是服务端接收数据的变量名,服务端的变量名和这里的 student 要保持一直。服务端定义以下:

@RequestMapping("/addstudent")
public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
    if (result != null) {
        //校验未经过,获取全部的异常信息并展现出来
        List<ObjectError> allErrors = result.getAllErrors();
        for (ObjectError allError : allErrors) {
            System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
        }
        return "student";
    }
    return "hello";
}
复制代码

注意,服务端什么都不用作,就说要返回的页面就好了,student 这个变量会被自动填充到返回的 Model 中。变量名就是填充时候的 key。若是想自定义这个 key,能够在参数中写出来 Model,而后手动加入 Student 对象,就像简单数据类型回显那样。

另外一种定义回显变量别名的方式,就是使用 @ModelAttribute 注解。

11.2 @ModelAttribute

@ModelAttribute 这个注解,主要有两方面的功能:

  1. 在数据回显时,给变量定义别名
  2. 定义全局数据

11.2.1 定义别名

在数据回显时,给变量定义别名,很是容易,直接加这个注解便可:

@RequestMapping("/addstudent")
public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) {
    if (result != null) {
        //校验未经过,获取全部的异常信息并展现出来
        List<ObjectError> allErrors = result.getAllErrors();
        for (ObjectError allError : allErrors) {
            System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
        }
        return "student";
    }
    return "hello";
}
复制代码

这样定义完成后,在前端再次访问回显的变量时,变量名称就不是 student 了,而是 s:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>学生编号:</td>
            <td><input type="text" name="id" value="${s.id}"></td>
        </tr>
        <tr>
            <td>学生姓名:</td>
            <td><input type="text" name="name" value="${s.name}"></td>
        </tr>
        <tr>
            <td>学生邮箱:</td>
            <td><input type="text" name="email" value="${s.email}"></td>
        </tr>
        <tr>
            <td>学生年龄:</td>
            <td><input type="text" name="age" value="${s.age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>
复制代码

11.2.2 定义全局数据

假设有一个 Controller 中有不少方法,每一个方法都会返回数据给前端,可是每一个方法返回给前端的数据又不太同样,虽然不太同样,可是没有方法的返回值又有一些公共的部分。能够将这些公共的部分提取出来单独封装成一个方法,用 @ModelAttribute 注解来标记。

例如在一个 Controller 中 ,添加以下代码:

@ModelAttribute("info")
public Map<String,Object> info() {
    Map<String, Object> map = new HashMap<>();
    map.put("username", "javaboy");
    map.put("address", "www.javaboy.org");
    return map;
}
复制代码

当用户访问当前 Controller 中的任意一个方法,在返回数据时,都会将添加了 @ModelAttribute 注解的方法的返回值,一块儿返回给前端。@ModelAttribute 注解中的 info 表示返回数据的 key。

12.1 返回 JSON

目前主流的 JSON 处理工具主要有三种:

  • jackson
  • gson
  • fastjson

在 SpringMVC 中,对 jackson 和 gson 都提供了相应的支持,就是若是使用这两个做为 JSON 转换器,只须要添加对应的依赖就能够了,返回的对象和返回的集合、Map 等都会自动转为 JSON,可是,若是使用 fastjson,除了添加相应的依赖以外,还须要本身手动配置 HttpMessageConverter 转换器。其实前两个也是使用 HttpMessageConverter 转换器,可是是 SpringMVC 自动提供的,SpringMVC 没有给 fastjson 提供相应的转换器。

12.1.1 jackson

jackson 是一个使用比较多,时间也比较长的 JSON 处理工具,在 SpringMVC 中使用 jackson ,只须要添加 jackson 的依赖便可:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.1</version>
</dependency>
复制代码

依赖添加成功后,凡是在接口中直接返回的对象,集合等等,都会自动转为 JSON。以下:

public class Book {
    private Integer id;
    private String name;
    private String author;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
    Book book = new Book();
    book.setId(1);
    book.setName("三国演义");
    book.setAuthor("罗贯中");
    return book;
}
复制代码

这里返回一个对象,可是在前端接收到的则是一个 JSON 字符串,这个对象会经过 HttpMessageConverter 自动转为 JSON 字符串。

若是想返回一个 JSON 数组,写法以下:

@RequestMapping("/books")
@ResponseBody
public List<Book> getAllBooks() {
    List<Book> list = new ArrayList<Book>();
    for (int i = 0; i < 10; i++) {
        Book book = new Book();
        book.setId(i);
        book.setName("三国演义:" + i);
        book.setAuthor("罗贯中:" + i);
        list.add(book);
    }
    return list;
}
复制代码

添加了 jackson ,就可以自动返回 JSON,这个依赖于一个名为 HttpMessageConverter 的类,这自己是一个接口,从名字上就能够看出,它的做用是 Http 消息转换器,既然是消息转换器,它提供了两方面的功能:

  1. 将返回的对象转为 JSON
  2. 将前端提交上来的 JSON 转为对象

可是,HttpMessageConverter 只是一个接口,由各个 JSON 工具提供相应的实现,在 jackson 中,实现的名字叫作 MappingJackson2HttpMessageConverter,而这个东西的初始化,则由 SpringMVC 来完成。除非本身有一些自定义配置的需求,不然通常来讲不须要本身提供 MappingJackson2HttpMessageConverter。

举一个简单的应用场景,例如每一本书,都有一个出版日期,修改 Book 类以下:

public class Book {
    private Integer id;
    private String name;
    private String author;
    private Date publish;


    public Date getPublish() {
        return publish;
    }

    public void setPublish(Date publish) {
        this.publish = publish;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
复制代码

而后在构造 Book 时添加日期属性:

@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
    Book book = new Book();
    book.setId(1);
    book.setName("三国演义");
    book.setAuthor("罗贯中");
    book.setPublish(new Date());
    return book;
}
复制代码

访问 /book 接口,返回的 json 格式以下:

若是咱们想本身定制返回日期的格式,简单的办法,能够经过添加注解来实现:

public class Book {
    private Integer id;
    private String name;
    private String author;
    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
    private Date publish;
复制代码

注意这里必定要设置时区。

这样,就能够定制返回的日期格式了。

可是,这种方式有一个弊端,这个注解能够加在属性上,也能够加在类上,也就说,最大能够做用到一个类中的全部日期属性上。若是项目中有不少实体类都须要作日期格式化,使用这种方式就比较麻烦了,这个时候,咱们能够本身提供一个 jackson 的 HttpMesageConverter 实例,在这个实例中,本身去配置相关属性,这里的配置将是一个全局配置。

在 SpringMVC 配置文件中,添加以下配置:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id="httpMessageConverter">
    <property name="objectMapper">
        <bean class="com.fasterxml.jackson.databind.ObjectMapper">
            <property name="dateFormat">
                <bean class="java.text.SimpleDateFormat">
                    <constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"/>
                </bean>
            </property>
            <property name="timeZone" value="Asia/Shanghai"/>
        </bean>
    </property>
</bean>
复制代码

添加完成后,去掉 Book 实体类中日期格式化的注解,再进行测试,结果以下:

12.1.2 gson

gson 是 Google 推出的一个 JSON 解析器,主要在 Android 开发中使用较多,不过,Web 开发中也是支持这个的,并且 SpringMVC 还针对 Gson 提供了相关的自动化配置,以至咱们在项目中只要添加 gson 依赖,就能够直接使用 gson 来作 JSON 解析了。

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>
复制代码

若是项目中,同时存在 jackson 和 gson 的话,那么默认使用的是 jackson,为社么呢?在 org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 类的构造方法中,加载顺序就是先加载 jackson 的 HttpMessageConverter,后加载 gson 的 HttpMessageConverter。

加完依赖以后,就能够直接返回 JSON 字符串了。使用 Gson 时,若是想作自定义配置,则须要自定义 HttpMessageConverter。

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.GsonHttpMessageConverter" id="httpMessageConverter">
    <property name="gson">
        <bean class="com.google.gson.Gson" factory-bean="gsonBuilder" factory-method="create"/>
    </property>
</bean>
<bean class="com.google.gson.GsonBuilder" id="gsonBuilder">
    <property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
复制代码

12.1.3 fastjson

fastjson 号称最快的 JSON 解析器,可是也是这三个中 BUG 最多的一个。在 SpringMVC 并没针对 fastjson 提供相应的 HttpMessageConverter,因此,fastjson 在使用时,必定要本身手动配置 HttpMessageConverter(前面两个若是没有特殊须要,直接添加依赖就能够了)。

使用 fastjson,咱们首先添加 fastjson 依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.60</version>
</dependency>
复制代码

而后在 SpringMVC 的配置文件中配置 HttpMessageConverter:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
    <property name="fastJsonConfig">
        <bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
            <property name="dateFormat" value="yyyy-MM-dd"/>
        </bean>
    </property>
</bean>
复制代码

fastjson 默认中文乱码,添加以下配置解决:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
    <property name="fastJsonConfig">
        <bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
            <property name="dateFormat" value="yyyy-MM-dd"/>
        </bean>
    </property>
    <property name="supportedMediaTypes">
        <list>
            <value>application/json;charset=utf-8</value>
        </list>
    </property>
</bean>
复制代码

12.2 接收 JSON

浏览器传来的参数,能够是 key/value 形式的,也能够是一个 JSON 字符串。在 Jsp/Servlet 中,咱们接收 key/value 形式的参数,通常是经过 getParameter 方法。若是客户端商户惨的是 JSON 数据,咱们能够经过以下格式进行解析:

@RequestMapping("/addbook2")
@ResponseBody
public void addBook2(HttpServletRequest req) throws IOException {
    ObjectMapper om = new ObjectMapper();
    Book book = om.readValue(req.getInputStream(), Book.class);
    System.out.println(book);
}
复制代码

可是这种解析方式有点麻烦,在 SpringMVC 中,咱们能够经过一个注解来快速的将一个 JSON 字符串转为一个对象:

@RequestMapping("/addbook3")
@ResponseBody
public void addBook3(@RequestBody Book book) {
    System.out.println(book);
}
复制代码

这样就能够直接收到前端传来的 JSON 字符串了。这也是 HttpMessageConverter 提供的第二个功能。

13. RESTful

本小节选自外部博客,原文连接:www.ruanyifeng.com/blog/2011/0…

愈来愈多的人开始意识到,网站即软件,并且是一种新型的软件。这种"互联网软件"采用客户端/服务器模式,创建在分布式体系上,经过互联网通讯,具备高延时(high latency)、高并发等特色。网站开发,彻底能够采用软件开发的模式。可是传统上,软件和网络是两个不一样的领域,不多有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通讯。互联网的兴起,使得这两个领域开始融合,如今咱们必须考虑,如何开发在互联网环境中使用的软件。

RESTful 架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,因此正获得愈来愈多网站的采用。

可是,到底什么是 RESTful 架构,并非一个容易说清楚的问题。下面,我就谈谈我理解的 RESTful 架构。、

RESTful 它不是一个具体的架构,不是一个软件,不是一个框架,而是一种规范。在移动互联网兴起以前,咱们都不多说起 RESTful,主要是由于用的少,移动互联网兴起后,RESTful 获得了很是普遍的应用,由于在移动互联网兴起以后,咱们再开发后端应用,就不只仅只是开发一个网站了,还对应了多个前端(Android、iOS、HTML5 等等),这个时候,咱们在设计后端接口是,就须要考虑接口的形式,格式,参数的传递等等诸多问题了。

13.1 起源

REST 这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。

Fielding 是一个很是重要的人,他是 HTTP 协议(1.0版和1.1版)的主要设计者、Apache 服务器软件的做者之1、Apache 基金会的第一任主席。因此,他的这篇论文一经发表,就引发了关注,而且当即对互联网开发产生了深远的影响。

他这样介绍论文的写做目的:

"本文研究计算机科学两大前沿----软件和网络----的交叉点。长期以来,软件研究主要关注软件设计的分类、设计方法的演化,不多客观地评估不一样的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通讯行为的细节、如何改进特定通讯机制的表现,经常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对总体表现有更大的影响。我这篇文章的写做目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,获得一个功能强、性能好、适宜通讯的架构。"

13.2 名称

Fielding 将他对互联网软件的架构原则,定名为REST,即 Representational State Transfer 的缩写。我对这个词组的翻译是"表现层状态转化"。

若是一个架构符合 REST 原则,就称它为 RESTful 架构。

要理解 RESTful 架构,最好的方法就是去理解 Representational State Transfer 这个词组究竟是什么意思,它的每个词表明了什么涵义。若是你把这个名称搞懂了,也就不难体会 REST 是一种什么样的设计。

13.3 资源(Resources)

REST 的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。

所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它能够是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你能够用一个 URI (统一资源定位符)指向它,每种资源对应一个特定的 URI。要获取这个资源,访问它的 URI 就能够,所以 URI 就成了每个资源的地址或独一无二的识别符。

所谓"上网",就是与互联网上一系列的"资源"互动,调用它的 URI。

在 RESTful 风格的应用中,每个 URI 都表明了一个资源。

13.4 表现层(Representation)

"资源"是一种信息实体,它能够有多种外在表现形式。咱们把"资源"具体呈现出来的形式,叫作它的"表现层"(Representation)。

好比,文本能够用 txt 格式表现,也能够用 HTML 格式、XML 格式、JSON 格式表现,甚至能够采用二进制格式;图片能够用 JPG 格式表现,也能够用 PNG 格式表现。

URI 只表明资源的实体,不表明它的形式。严格地说,有些网址最后的 ".html" 后缀名是没必要要的,由于这个后缀名表示格式,属于 "表现层" 范畴,而 URI 应该只表明"资源"的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对"表现层"的描述。

13.5 状态转化(State Transfer)

访问一个网站,就表明了客户端和服务器的一个互动过程。在这个过程当中,势必涉及到数据和状态的变化。

互联网通讯协议 HTTP 协议,是一个无状态协议。这意味着,全部的状态都保存在服务器端。所以,若是客户端想要操做服务器,必须经过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是创建在表现层之上的,因此就是"表现层状态转化"。

客户端用到的手段,只能是 HTTP 协议。具体来讲,就是 HTTP 协议里面,四个表示操做方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操做:

  • GET 用来获取资源
  • POST 用来新建资源(也能够用于更新资源)
  • PUT 用来更新资源
  • DELETE 用来删除资源

13.6 综述

综合上面的解释,咱们总结一下什么是 RESTful 架构:

  • 每个 URI 表明一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端经过四个 HTTP 动词,对服务器端资源进行操做,实现"表现层状态转化"。

13.7 误区

RESTful 架构有一些典型的设计误区。

最多见的一种设计错误,就是 URI 包含动词。由于"资源"表示一种实体,因此应该是名词,URI 不该该有动词,动词应该放在 HTTP 协议中。

举例来讲,某个 URI 是 /posts/show/1,其中 show 是动词,这个 URI 就设计错了,正确的写法应该是 /posts/1,而后用 GET 方法表示 show。

若是某些动做是HTTP动词表示不了的,你就应该把动做作成一种资源。好比网上汇款,从帐户 1 向帐户 2 汇款 500 元,错误的 URI 是:

  • POST /accounts/1/transfer/500/to/2

正确的写法是把动词 transfer 改为名词 transaction,资源不能是动词,可是能够是一种服务:

POST /transaction HTTP/1.1
Host: 127.0.0.1
from=1&to=2&amount=500.00
复制代码

另外一个设计误区,就是在URI中加入版本号:

由于不一样的版本,能够理解成同一种资源的不一样表现形式,因此应该采用同一个 URI。版本号能够在 HTTP 请求头信息的 Accept 字段中进行区分(参见 Versioning REST Services):

Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=1.1
Accept: vnd.example-com.foo+json; version=2.0
复制代码

13.8 SpringMVC 的支持

SpringMVC 对 RESTful 提供了很是全面的支持,主要有以下几个注解:

  • @RestController

这个注解是一个组合注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	/** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) * @since 4.0.1 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}
复制代码

通常,直接用 @RestController 来标记 Controller,能够不使用 @Controller。

请求方法中,提供了常见的请求方法:

  • @PostMapping
  • @GetMapping
  • @PutMapping
  • @DeleteMapping

另外还有一个提取请求地址中的参数的注解 @PathVariable:

@GetMapping("/book/{id}")//http://localhost:8080/book/2
public Book getBookById(@PathVariable Integer id) {
    Book book = new Book();
    book.setId(id);
    return book;
}
复制代码

参数 2 将被传递到 id 这个变量上。

14. 静态资源访问

在 SpringMVC 中,静态资源,默认都是被拦截的,例如 html、js、css、jpg、png、txt、pdf 等等,都是没法直接访问的。由于全部请求都被拦截了,因此,针对静态资源,咱们要作额外处理,处理方式很简单,直接在 SpringMVC 的配置文件中,添加以下内容:

<mvc:resources mapping="/static/html/**" location="/static/html/"/>
复制代码

mapping 表示映射规则,也是拦截规则,就是说,若是请求地址是 /static/html 这样的格式的话,那么对应的资源就去 /static/html/ 这个目录下查找。

在映射路径的定义中,最后是两个 *,这是一种 Ant 风格的路径匹配符号,一共有三个通配符:

通配符 含义
** 匹配多层路径
* 匹配一层路径
? 匹配任意单个字符

一个比较原始的配置方式可能以下:

<mvc:resources mapping="/static/html/**" location="/static/html/"/>
<mvc:resources mapping="/static/js/**" location="/static/js/"/>
<mvc:resources mapping="/static/css/**" location="/static/css/"/>
复制代码

可是,因为 ** 能够表示多级路径,因此,以上配置,咱们能够进行简化:

<mvc:resources mapping="/**" location="/"/>
复制代码

15. 拦截器

SpringMVC 中的拦截器,至关于 Jsp/Servlet 中的过滤器,只不过拦截器的功能更为强大。

拦截器的定义很是容易:

@Component
public class MyInterceptor1 implements HandlerInterceptor {
    /** * 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行 * @param request * @param response * @param handler * @return * @throws Exception */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor1:preHandle");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor1:postHandle");

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor1:afterCompletion");

    }
}
@Component
public class MyInterceptor2 implements HandlerInterceptor {
    /** * 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行 * @param request * @param response * @param handler * @return * @throws Exception */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor2:preHandle");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor2:postHandle");

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor2:afterCompletion");

    }
}
复制代码

拦截器定义好以后,须要在 SpringMVC 的配置文件中进行配置:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <ref bean="myInterceptor1"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <ref bean="myInterceptor2"/>
    </mvc:interceptor>
</mvc:interceptors>
复制代码

若是存在多个拦截器,拦截规则以下:

  • preHandle 按拦截器定义顺序调用
  • postHandler 按拦截器定义逆序调用
  • afterCompletion 按拦截器定义逆序调用
  • postHandler 在拦截器链内全部拦截器返成功调用
  • afterCompletion 只有 preHandle 返回 true 才调用

关注微信公众号【江南一点雨】,回复 springmvc,获取本文电子版,或者访问 springmvc.javaboy.org 查看本文电子书。

相关文章
相关标签/搜索