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 可以有效的简化这一步骤。

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

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。

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 等等就全部都加入进来了。

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

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.创建视图

这里我们就采用 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 就可以看到如下页面:

跟松哥学 SpringMVC(1/2)_SpringMVC

3. SpringMVC 工作流程

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

跟松哥学 SpringMVC(1/2)_SpringMVC_02

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被定义两次,后面一个优先)。
namespaceWebApplicationContext命名空间。默认值是[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 来加载,这两个容器不一样:

跟松哥学 SpringMVC(1/2)_SpringMVC_03

从图中可以看出:

  • 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:

跟松哥学 SpringMVC(1/2)_SpringMVC_04

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 也有诸多实现类:

跟松哥学 SpringMVC(1/2)_SpringMVC_05

  • 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 以及请求请求访问到了。强行访问会报如下错误:

跟松哥学 SpringMVC(1/2)_SpringMVC_06

当然,限定的方法也可以有多个:

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

转自:江南一点雨,https://mp.weixin.qq.com/s/7rvGBlfYie9A_4_J1uoqpg