简介

SpringMVC是Spring下的一个用于Web表现层开发的技术。相比Servlet,SpringMVC更加高效快捷。

SpringMVC流程如下:

  • 浏览器发送请求到Tomcat服务器;
  • Tomcat接收请求后,将请求交给SpringMVC中的DispatcherServlet(前端控制器)来处理。
  • DispatcherServlet按照对应规则将请求分发到对应的Bean
  • Bean由我们自己编写来处理不同的请求。 每个Bean中可以处理一个或多个不同的请求 URL。

DispatcherServletBean对象都需要交给Spring容器来进行管理。

综上,我们需要编写的内容为:

  • Bean对象;

  • 请求URL和Bean对象对应关系的配置;

  • 构建Spring容器。

    DispatcherServletBean对象交给容器管理。

  • 配置Tomcat服务器。

    使Tomcat能够识别Spring容器,并将请求交给容器中的DispatcherServlet来分发请求。

项目的基本实现步骤如下:

  1. 创建Web工程(Maven结构),并在工程的pom.xml中添加SpringMVC和Servlet坐标。
  2. 创建SpringMVC控制器类(等同于Servlet功能)。
  3. 初始化SpringMVC环境(同Spring环境),设定SpringMVC加载对应的Bean
  4. 初始化Servlet容器,加载SpringMVC环境,并设置SpringMVC技术处理的请求。

Spring MVC 工作流程

SpringMVC的使用过程共分两个阶段:

  1. 启动服务器初始化过程;

    1. 服务器启动,执行ServletConfig类,初始化Web容器。

    2. 根据getServletConfigClasses获取所需的SpringMVC配置类(这里是SpringMvcConfig)来初始化SpringMVC的容器。

    3. 加载SpringMvcConfig配置类。

    4. 执行@ComponentScan加载对应的Bean

      扫描指定包下所有类上的注解,将所有的Controller类(如有@Controller@RestController等注解的类)加载到容器中。

    5. 加载每一个Controler

      使用@RequestMapping建立请求路径与Controler中的方法的对应关系。

    6. 执行getServletMappings方法,定义所有的请求都通过SpringMVC。如:

      protected Spring[]  getServletMappings() {
          return new Spring[]{"/"};
      }    
      

      /代表所拦截请求的路径规则,只有被拦截后才能交给SpringMVC来处理请求。

  2. 单次请求过程。

    1. 根据请求路径发送请求。
    2. Web容器将符合设置的请求路径的请求交给SpringMVC处理。
    3. 解析请求路径。
    4. 执行匹配对应请求路径的方法。
    5. 将有@ResponseBody方法的返回值作为响应体返回给请求方。

项目环境及结构

项目结构

使用SpringMVC开发的项目结构如下:

  • 📁project-file-name
    • 📁src
      • 📁main
        • 📁java
          • 📁com.linner
            • 📁config —— 配置类
            • 📁controller —— 表现层
            • 📁dao —— 持久层
            • 📁domain —— 实体类
            • 📁service —— 业务层
        • 📁resourcs
        • 📁webapp

这种项目结构采用了SSM架构,即:

  • 表现层;
  • 持久层;
  • 业务层。

通过IDEA创建SpringMVC项目步骤如下:

  1. 创建基础的Maven-Archetype-Webapp项目。

  2. 补全项目结构:

    • 📁src
      • 📁main
        • 📁java
        • 📁resourcs
        • 📁webapp
  3. 修改pom.xml文件:

    将多余的内容删除,然后添加SpringMVC所需的依赖。如:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.linner</groupId>
      <artifactId>springmvc-demo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
    
      <dependencies>
    
        <!-- ... -->
    
      </dependencies>
    
      <build>
        <plugins>
    
          <!-- ... -->
    
        </plugins>
      </build>
    </project>
    

基础环境

Tomcat7 Maven插件:

<plugin>
  <groupId>org.apache.tomcat.maven</groupId>
  <artifactId>tomcat7-maven-plugin</artifactId>
  <version>2.1</version>
  <configuration>
    <port>80</port>
    <path>/</path>
    <uriEncoding>UTF-8</uriEncoding>
  </configuration>
</plugin>

基础依赖:

<!-- Servlet -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <!-- Servlet需要修改作用范围,否则会与Tomcat中的servlet-api包发生冲突 -->
    <!-- provided代表的是该包只在编译和测试的时候用 -->
    <scope>provided</scope>
</dependency>
<!-- SpringMVC -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

DAO 相关依赖环境

使用SpringMVC构建Web项目,除了以上基础配置外,还需要导入其他配置。

DAO相关的坐标:

<!-- Spring JDBC -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>5.2.10.RELEASE</version>
</dependency>
<!-- MySQL -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.47</version>
</dependency>
<!-- MyBatis -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.6</version>
</dependency>
<!-- MyBatis Spring依赖 -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>1.3.0</version>
</dependency>
<!-- Druid 数据库连接池 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.16</version>
</dependency>

其它依赖环境

<!-- Jackson -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.0</version>
</dependency>

<!-- 日志 -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>

<!-- Thymeleaf(不常用) -->
<dependency>
  <groupId>org.thymeleaf</groupId>
  <artifactId>thymeleaf-spring5</artifactId>
  <version>3.0.12.RELEASE</version>
</dependency>

<!-- 文件上传依赖 -->
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.3.1</version>
</dependency>

<!-- Test -->
<!-- Junit 单元测试 -->
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>3.8.1</version>
  <scope>test</scope>
</dependency>

配置

配置SpringMVC有两种方式:

  • web.xml配置文件
  • 配置类

配置文件方式

web.xml中注册SpringMVC的前端控制器DispatcherServlet

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <!-- 指定SpringMVC前端控制器: -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <!-- 
            配对路径
            / 表示处理所有不包括.jsp的请求
            因为.jsp有自己的servlet,如果在DispatcherServlet中处理.jsp,会导致找不到相应页面,从而导致渲染失败
        -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</webapp>

<url-pattern>

  • /:表示匹配所有不包括.jsp的请求;
  • /*:能够匹配所有请求,包括.jsp

扩展配置:

web.xml中的配置还可以定义在其它文件中,例如在resourece下创建新的配置文件springMVC.xml。然后在web.xml添加新配置:

<!-- 
    配置SpringMVC的前端控制器
    对浏览器发送的请求统一进行处理
 -->
<webapp>
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 
            通过初始化参数指定SpringMVC配置文件的位置和名称
         -->
        <init-param>
            <!-- contextConfigLocation为固定值 -->
            <param-name>contextConfigLocation</param-name>
            <!-- 
                使用 classpath: 表示从类路径查找配置文件,例如maven工程中的src/main/resources
             -->
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!-- 
            将启动控制DispatcherServlet的初始化时间提前到服务器启动时:
        -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- ... -->

</webapp>

springMVC.xml中配置:

<?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 http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 自动扫描包 -->
    <context:component-scan base-package="com.linner.controller"/>

    <!-- 配置Thymeleaf视图解析器(不常用) -->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
        <property name="order" value="1"/>
        <!-- 设置编码,将编码都转为 UTF-8 -->
        <property name="characterEncoding" value="UTF-8"/>
        <property name="templateEngine">
            <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
                <property name="templateResolver">
                    <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
        
                        <!-- 视图前缀 -->
                        <property name="prefix" value="/WEB-INF/templates/"/>
        
                        <!-- 视图后缀 -->
                        <property name="suffix" value=".html"/>
                        <property name="templateMode" value="HTML5"/>
                        <property name="characterEncoding" value="UTF-8" />
                    </bean>
                </property>
            </bean>
        </property>
    </bean>

    <!-- 静态资源访问 -->
    <mvc:view-controller path="/" view-name="index" />

    <!-- 
        处理静态资源,例如html、js、css、jpg
        若只设置该标签,则只能访问静态资源,其他请求则无法访问
        此时必须设置<mvc:annotation-driven/>解决问题
    -->
    <mvc:default-servlet-handler/>

    <!-- 开启mvc注解驱动 -->
    <mvc:annotation-driven>
        <mvc:message-converters>
            <!-- 处理响应中文内容乱码 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="defaultCharset" value="UTF-8" />
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html</value>
                        <value>application/json</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
</beans>

在视图解析器中设置了视图前缀<property name="prefix" ...>和视图后缀<property name="suffix" ...>,那么在Controller的方法中,想要跳转到视图时,只需返回对应的视图文件名称,并去掉其后缀。例如访问hello.html只需返回"hello",但hello.html必须要在对应的前缀路径中。

SpringMVC编码过滤器(必须在web.xml中进行注册):

<webapp>
    <!-- 配置 SpringMVC 的编码过滤器 -->
    <filter>
        <filter-name>CharacterEncodingFilter</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>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <!-- ... -->
    </servlet>

    <!-- ... -->
    
</webapp>

SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效。

配置类方式

使用配置类则无需在webapp/WEB-INF中添加web.xml文件(当然也可以选择添加)。

SpringMVC项目至少需要ServletConfigSpringConfigSpringMvcConfig这三个配置类。

  • ServletConfig:Spring MVC项目初始化类,也是项目的入口,作用与web.xml类似
  • SpringConfig:控制业务(Service)和功能(如DataSource、SqlSessionFactoryBean、 MapperScannerConfigurer等)相关的Bean。
  • SpringMvcConfig(WebConfig):加载表现层Bean(Controller)。

ServletConfig(Web项目入口配置类):

public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 指定Spring的配置类
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    /**
     * 指定Spring MVC的配置类
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    /**
     * 指定DispatcherServlet的映射路径,即url-pattern
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

ServletConfig最重要的是继承AbstractAnnotationConfigDispatcherServletInitializer这个类,并反别重写它的三个方法。

在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerlnitializer接口的类,如果找到的话就用它来配置Servlet容器。

Spring提供了这个接口的实现,名为SpringServletContainerlnitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。

Spring3.2引l入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletlnitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer,并将其部署到Servlet3.O容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

SpringConfig(启动Tomcat服务器时加载Spring配置类):

@Configuration
@ComponentScan({"com.linner.service"})
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class}) // 引入其它配置
@EnableTransactionManagement    // 开启事务管理
public class SpringConfig {
}

Spring需要管理的是service包和dao包。但dao包最终是交给MapperScannerConfigurer对象来进行扫描处理的。所以SpringConfig只需要扫描到service包即可。

演示@ComponentScan的另一种用法(排除controller包中的Bean):

@ComponentScan(value="com.linner",
  excludeFilters=@ComponentScan.Filter(
      type = FilterType.ANNOTATION,
      classes = Controller.class // 排除@Controller定义的Bean
  )
)

上面方法本质是使用@ComponentScanexcludeFilters属性设置过滤规则。

  • type:设置排除规则。
    • ANNOTATION:按照注解排除。
    • ASSIGNABLE_TYPE:按照指定的类型过滤。
    • ASPECTJ:按照Aspectj表达式排除(基本上不会用)。
    • REGEX:按照正则表达式排除。
    • CUSTOM:按照自定义规则排除。

classes:设置排除的具体注解类。

SpringMvcConfig

@Configuration
@ComponentScan({"com.linner.controller"})
@EnableWebMvc   // MVC注解驱动
public class WebConfig implements WebMvcConfigurer {

    /**
     * 相当于 default-servlet-handler
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * 视图控制器 view-controller
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/hello").setViewName("hello");
    }

    /**
     * 文件上传解析器
     */
    @Bean
    public MultipartResolver getMultipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        return commonsMultipartResolver;
    }

    /**
     * 异常处理解析器
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        prop.setProperty("java.lang.ArithmeticException", "error");
        exceptionResolver.setExceptionMappings(prop);
        // 可以不设置,默认键即为 exception
        exceptionResolver.setExceptionAttribute("exception");
        exceptionResolvers.add(exceptionResolver);
    }

    /**
     * 模板解析器
     */
    @Bean
    public ITemplateResolver getTemplateResolver() {

        // 获取当前的 WebApplicationContext
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // 使用WebApplicationContext获取ServletContext,并构造ServletContextTemplateResolver
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(
                webApplicationContext.getServletContext());

        // 设置视图前缀
        templateResolver.setPrefix("/WEB-INF/templates/");
        // 设置视图后缀
        templateResolver.setSuffix(".html");

        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);

        return templateResolver;
    }

    /**
     * 模板引擎
     * @param templateResolver 模板解析器(自动装配,@Autowired可忽略不写)
     */
    @Bean
    public SpringTemplateEngine getTemplateEngine(ITemplateResolver templateResolver) {

        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);

        return templateEngine;
    }

    /**
     * 设置视图解析器
     * @param templateEngine 模板引擎(自动装配)
     */
    @Bean
    public ViewResolver getViewResolver(SpringTemplateEngine templateEngine) {

        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);

        return viewResolver;
    }
}

关于Spring MVC的配置类,除了扫描组件和配置模板解析器外,Spring提供了两种方式来配置,一种是实现WebMvcConfigurer接口,另一种是继承WebMvcConfigurationSupport。由于Java 8的接口中提供了default关键字来修饰接口方法,使得接口可以存在默认的实现,所以使用WebMvcConfigurer接口也不必实现所有接口。而在SpringBoot 2中使用WebMvcConfigurationSupport有可能会导致SpringBoot的自动配置不生效,并且在Spring MVC中使用WebMvcConfigurationSupport也可能导致配置类不生效,所以我个人推荐使用WebMvcConfigurer

如果你使用WebMvcConfigurationSupport后发现拦截器等配置不生效,那么可以尝试实现WebMvcConfigurer接口来解决问题。

DAO相关配置类:

jdbc.properties(数据库配置,放在项目中resources目录下):

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring_db?useSSL=false&characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456

JdbcConfig

public class JdbcConfig {
    
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager ds = new DataSourceTransactionManager();
        ds.setDataSource(dataSource);
        return ds;
    }
}

MyBatisConfig

public class MyBatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setTypeAliasesPackage("com.linner.domain");
        return factoryBean;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.linner.dao");
        return msc;
    }
}

使用过滤器转换编码

ServletConfig中重写AbstractAnnotationConfigDispatcherServletInitializergetServletFilters()来注册过滤器:

/**
 * 注册过滤器
 */
@Override
protected Filter[] getServletFilters() {
    
    // 设置编码,将编码都转为 UTF-8
    CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
    characterEncodingFilter.setEncoding("UTF-8");
    characterEncodingFilter.setForceEncoding(true);

    HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

    return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
}

请求与响应

SpringMVC是Web层的框架,主要作用是接收请求、接收数据、响应结果。

编写Controller只需要在Controller类上使用@Controller注解即可。

请求映射 RequestMapping

映射请求路径使用@RequestMapping注解。注解中的使用value属性指定映射的请求路径(由于是value属性,所以当注解中无需指定其它参数时,可以省略)。

@RequestMapping可以分别作用在类和方法上:

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value = "/helloSpring")
    @ResponseBody
    public String helloSpring() {
        return "Hello Spring!";
    }

    @RequestMapping("/helloWorld")
    @ResponseBody
    public String helloWorld() {
        return "Hello World!";
    }
}
  • 方法上定义的@RequestMapping是具体的请求方式,包括请求路径和请求方式。

    即,如果在方法上使用了@RequestMapping并且没有在类上使用@RequestMapping,那么该方法的请求路径即为方法上@RequestMapping中的值。

  • 类上定义的@RequestMapping是请求目录。

    即,如果在方法和类上均使用了@RequestMapping,那么该方法的请求路径需要加上类@RequestMapping注解中定义的目录。例如上方代码中的请求路径为/user/helloSpring/user/helloWorld

value属性是一个字符串数组,可以通过以下方式来指定多个请求路径:

@RequestMapping({"hello", "helloWorld"})

@RequestMapping除了value属性外,还有method属性。method属性是用来指定请求方式的,如:@RequestMapping(method = RequestMethod.POST)(匹配POST请求方式)。

在客户端向服务器发送请求时,DispatcherServlet会首先根据@RequestMapping获取对应的控制器方法。

接收 Query 参数

Query参数,也就是拼接在请求路径后面,以?开始,使用&分隔每个参数项的参数传递方式。例如:

http://localhost/user/hello?name=张三

接收Query参数,GET和POST请求的编写方式一致。

如果要使用不同的方式接收请求,可以修改@RequestMapping注解:

@RequestMapping(value = "/save", method = RequestMethod.GET)

普通参数

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

    // 使用 [http://localhost/user/hello?name=张三] 访问
    // 返回响应体 Hello 张三!
    @RequestMapping("/hello")
    @ResponseBody
    public String hello(String name) {
        return "Hello " + name + "!";
    }

    // 使用 [http://localhost/user/login?name=张三&password=abc] 访问
    // 返回响应体 OK,终端输出 userName=张三; password=abc
    @RequestMapping("/login")
    @ResponseBody
    public String login(@RequestParam("name") String userName, String password) {
        System.out.println("userName=" + userName + "; password=" + password);
        return "OK";
    }

}
  • 请求参数:

    定义相同的Query参数名与方法形参变量名即可接收参数。

    如果想要形参名与Query参数名不同则可使用@RequestParam注解定义Query参数名。

    如果有多个请求参数则定义多个方法参数。

  • 返回值:

    返回值使用@ResponseBody注解后直接在方法中使用return返回。这里注解@ResponseBody是指将返回值作为响应体。

POJO类型参数

使用POJO类型接收参数,只需要让请求参数名与形参对象属性名相同即可。如果有嵌套的POJO参数,请求参数名与形参对象属性名相同,然后按照对象层次结构关系即可接收。

定义一个POJO类:

public class Address {
    private String province;
    private String city;

    // setter、getter and toString...
}
public class User {
    private Integer id;
    private String name;
    private String password;
    private Address address;

    // setter、getter and toString...
}

Controller:

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

    // 访问 [http://localhost/user/login?id=123&password=abc&name=张三&address.province=广东&address.city=广州]
    // 返回响应体 OK,终端输出 Login: User{id=123, name='张三', password='abc', address=Address{province='广东', city='广州'}}
    @RequestMapping("/login")
    @ResponseBody
    public String login(User user) {
        System.out.println("Login: " + user);
        return "OK";
    }

}

数组、集合类型参数

接收数组参数只需让请求参数名与形参名相同且请求参数为多个即可(形参为数组类型)。

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

    // 访问 [http://localhost/user/setHobbies?id=123&hobbies=唱歌,跳舞,Rap,打篮球]
    // 返回响应体 OK,终端输出 User 123's hobbyies: [唱歌, 跳舞, Rap, 打篮球]
    @RequestMapping("/setHobbies")
    @ResponseBody
    public String setHobbies(Integer id, String[] hobbies) {
        System.out.println("User " + id + "'s hobbies: " + Arrays.toString(hobbies));
        return "OK";
    }

}

使用集合类型形参接收参数,使用方式与数组不同(会报错)。因为SpringMVC将List看做是一个POJO对象来处理,将其创建一个对象并准备把前端的数据封装到对象中,但是List是一个接口无法创建对象。

使用集合类型形参接收参数需要使用@RequestParam绑定参数关系。

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

    // 访问方式与数组形式相同,响应体与终端输出也相同
    @RequestMapping("/setHobbies")
    @ResponseBody
    public String setHobbies(Integer id, @RequestParam List<String> hobbies) {
        System.out.println("User " + id + "'s hobbies: " + hobbies);
        return "OK";
    }

}

日期格式

使用@DateTimeFormat可以设置参数的日期格式,如:

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

    // URL: http://localhost/user/setBirthday?id=123&birthday=2023/1/1 18:23:40
    @RequestMapping("/setBirthday")
    @ResponseBody
    public String setBirthday(Integer id, @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") Date birthday) {
        System.out.println("User " + id + "'s birthday is " + birthday);
        return "OK";
    }
    
}

JSON 数据参数

使用JSON传输需要添加相应依赖。SpringMVC默认使用的是jackson来处理json的转换:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

在配置类中添加@EnableWebMvc注解来开启JSON数据类型自动转换:

@Configuration
@ComponentScan("com.linner.controller")
@EnableWebMvc // 开启JSON数据类型自动转换
public class SpringMvcConfig {
}

使用JSON传输数据只需要在形参前添加@ResponseBody注解来将外部传递的JSON数据映射到形参到对象中:

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

    @RequestMapping("/login")
    @ResponseBody
    public String login(@RequestBody User user) {
        System.out.println("Login: " + user);
        return "OK";
    }

    @RequestMapping("/setHobbies")
    @ResponseBody
    public String setHobbies(Integer id, @RequestBody List<String> /* 也可以使用 String[] */ hobbies) {
        System.out.println("User " + id + "'s hobbies: " + hobbies);
        return "OK";
    }

}
  • login()

    URL:http://localhost/user/login

    JSON:

    {
        "id": 123,
        "name": "张三",
        "password": "123",
        "address": {
            "province": "广东",
            "city": "广州"
        }
    } 
    
  • setHobbies

    URL:http://localhost/user/setHobbies2?id=123

    JSON:

    ["唱歌", "跳舞", "Rap", "打篮球"]
    

响应 JSON 数据

响应JSON数据需要依赖于@ResponseBody(在Controller中)和@EnableWebMvc(在配置类中)注解。将返回值设置为实体类对象,设置返回值类型为实体类类型,即可实现返回对应对象的JSON数据:

实体类:

public class Book {
    private Integer id;
    private String name;
    // constructor、setter、getter and toString ...
}

Controller:

@Controller
@RequestMapping("/books")
public class BookController {

    @RequestMapping("/search")
    @ResponseBody
    public Book search(int id) {
        return new Book(id, "Hello SpringMVC");
    }

    @RequestMapping("/searchName")
    @ResponseBody
    public List<Book> searchName(String name) {
        System.out.println("Search the book " + name);
        List books = new ArrayList<Book>();
        for (int i = 0; i < 5; i++) {
            books.add(new Book(i, name + Integer.toString(i)));
        }
        return books;
    }

}
  • searchName()

    URL:http://localhost/books/searchName?name=SpringMVC 返回JOSN:

    [
        {
            "id": 0,
            "name": "SpringMVC0"
        },
        {
            "id": 1,
            "name": "SpringMVC1"
        },
        {
            "id": 2,
            "name": "SpringMVC2"
        },
        {
            "id": 3,
            "name": "SpringMVC3"
        },
        {
            "id": 4,
            "name": "SpringMVC4"
        }
    ]
    
  • search()

    URL:http://localhost/books/search?id=123 返回JSON:

    {
        "id": 123,
        "name": "SpringMVC"
    }
    

Ant 风格路径

在Ant风格中,定义了以下几种符号:

  • ?:表示匹配请求资源目录中的任意单个字符
  • *:表示匹配请求资源目录中的任意0个或多个字符
  • /**/:表示匹配请求路径中任意一层或多层目录。例如/**/user可以匹配/abc/user/abc/def/user等等。

REST 风格

REST(Representational State Transfer,表现形式状态转换),是一种软件架构风格。REST的优点有:

  • 隐藏资源的访问行为,无法通过地址得知对资源是何种操作。
  • 简化书写。

按照REST风格访问资源时使用行为动作区分对资源进行了何种操作:

  • GET:查询;
  • POST:新增。
  • PUT:修改。
  • DELETE:删除。

如:

  • http://localhost/users —— GET:

    查询全部用户信息(查询)。

  • http://localhost/users/1 —— GET:查询指定用户(id为1)信息(查询)。

  • http://localhost/users —— POST:添加用户信息(新增/保存)。

  • http://localhost/users —— PUT:修改用户信息(修改/更新)。

  • http://localhost/users/1 —— DELETE:删除用户信息(删除)。

描述模块的名称通常使用复数,表示此类资源,而非单个资源。

Example:

@Controller
@RequestMapping("/users")
public class UserController {

    /**
     * 添加用户
     */
    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public String save(@RequestBody User user) {
        return "OK";
    }

    /**
     * 删除用户
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable int id) {
        return "OK";
    }

    /**
     * 修改用户
     */
    @RequestMapping(method = RequestMethod.PUT)
    @ResponseBody
    public String update(@RequestBody User user) {
        return "OK";
    }

    /**
     * 根据用户id查询
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public String searchById(@PathVariable int id) {
        return "OK";
    }

    /**
     * 查询所有用户
     */
    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public String searchAll() {
        return "OK";
    }
}

上方Controller中每个方法的@RequestMapping中都使用了method元素来确定请求方式。并且根据需要save()update()都接收一个JSON数据。

delete()searchById()都使用了路径参数(value = "\{id}")。指定路径参数后,需要在方法参数列表中添加名称相同的参数,并且用@PathVariable注解(public String delete(@PathVariable int id))。

路径参数可以定义多个,如:

@RequestMapping(value = "/{id}/{name}", method = RequestMethod.GET)
@ResponseBody
public String searchById(@PathVariable int id, @PathVariable String name) {
    return "OK";
}

如果想要路径参数名与形参名不同,需要在@PathVariable中注明对应关系,如:

@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable("id") int userId) {
    return "OK";
}

RESTful 快速开发

  • 使用@RestController注解:

    @ResponseBody注解提到类上,让所有的方法都有@ResponseBody的功能。 @RestController注解正好相当于@Controller加上@ResponseBody的功能,所以可以使用@RestController替代它们。

  • 使用@GetMapping@PostMapping@PutMapping@DeleteMapping等替代@RequestMapping

    例如@GetMapping就相当于RequestMapping(method = RequestMethod.GET)

Example:

@RestController
@RequestMapping("/books")
public class BookController {

    /**
     * 搜索全部书籍
     */
    @GetMapping
    public String getAll() {
        return "All Books";
    }

    /**
     * 搜索图书
     */
    @GetMapping("/{id}")
    public String getById(@PathVariable int id) {
        return "Get by id " + id;
    }

    /**
     * 保存图书
     */
    @PostMapping
    public String save(@RequestBody Book book) {
        return "Save " + book.toString();
    }

    /**
     * 修改图书
     */
    @PutMapping
    public String update(@RequestBody Book book) {
        return "Update " + book.toString();
    }

    /**
     * 删除图书
     */
    @DeleteMapping("/{id}")
    public String delete(@PathVariable int id) {
        return "Delete by id" + id;
    }

}

Params 请求参数映射匹配

params@RequestMapping中的一个属性,该属性通过请求的请求参数匹配请求映射。

用法如下:

  • "param":请求映射所匹配的请求必须携带param这个请求参数。

    例如:

    @RequestMapping(value = {"login"}, params = "username")
    

    请求路径示例:

    http://localhost/login?username=zhangsan
    
  • "!param":否定匹配,请求映射所匹配的请求必须不能携带param请求参数。

    例如:

    @RequestMapping(value = {"login"}, params = "!username")
    

    如果请求路径中包含参数username将不会匹配到该@RequestMapping

  • "param=value":等值匹配,请求映射所匹配的请求必须携带param请求参数,且param参数的值必须为value

    例如:

    @RequestMapping(value = {"login"}, params = "username=admin")
    

    请求路径为:

    http://localhost/login?username=admin
    
  • "param!=value":非值匹配,请求映射所匹配的请求必须携带param请求参数,但param的值不能为value

    例如:

    @RequestMapping(value = {"login"}, params = "username!=admin")
    

    如果请求路径携带参数username且值为admin则匹配失败。

  • {expression1[, expression2[, ...]]}

    params是字符串数组类型,可以指定多个参数规则,其中expression可以是以上任何类型的字符串表达式中的任何一种。

params只对其指定的参数有要求,没有被其指定的参数并没有任何限制。

如果请求的路径和方式都满足@RequestMapping,但是与params指定的规则不付,如果此时没有其它映射来匹配这个请求,服务器会返回给浏览器400错误。

headers 请求头匹配

headers@RequestMapping的属性,它指定请求的请求头信息匹配规则。

headers的字符串表达式语法与params十分相似:

  • "header":请求映射所匹配的请求必须携带header请求头信息。

  • "!header":请求映射所匹配的请求必须不能携带header请求头信息。

  • "header=value":请求映射所匹配的请求必须携带header请求头信息且header=value

    请求头中使用的是:分隔开的键值对,:左边是键(即上述中的header),右边是值(即上述中的value)。只需要将:替换为=即是对应的headers字符串表达式。

  • "header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value

  • {expression1[, expression2[, ...]]}headers属性同样是字符串数组类型,可以在{}中使用上述任何表达式。

@RequestMapping其它条件都满足,除了headers时,服务器会返回404错误。


域对象共享数据

request域对象共享数据的常用方式大致有5种:

  1. 通过ServletAPI获取(不建议使用)。即通过ServletRequestHttpServletRequest对象获取request域。
  2. 通过ModelAndView获取。
  3. 通过Model获取。
  4. 通过Map<String, Object>获取。
  5. 通过ModelMap获取。

ServletAPI

获取request域对象共享数据的方式之一就是使用ServletAPI。即,在Controller对象中的映射方法中,添加一个ServletRequestHttpServletRequest对象参数。

例如:

@RequestMapping("/testServletAPI")
@ResponseBody
public String testServletAPI(HttpServletRequest request) {
    request.setAttribute("testScope", "Hello ServletAPI!");
    return testRequestScope;
}

获取response也是类似的方法。在参数列表中指定一个ServletResponseHttpServletResponse对象参数。

Model

Model是SpringMVC提供的专用于共享request域对象数据。

使用Model的方式与使用ServletAPI类似,在形参列表中指定一个Model类型的参数即可。

@RequestMapping("/testModel")
@ResponseBody
public String testModel(Model model) {
    // 写入
    model.addAttribute("testRequestScope", "Hello Model!");
    // 读取
    String testRequestScope = (String) model.getAttribute("testRequestScope");
    System.out.println(testRequestScope);

    return testRequestScope;
}

Map String Object

使用Map<String, Object>共享request域对象数据,使用方式也是在形参列表中定义一个Map<String, Object>类型形参。

@RequestMapping("/testMap")
@ResponseBody
public String testMap(Map<String, Object> map) {
    // 写入
    map.put("testRequestScope", "Hello Map!");
    // 读取
    String testRequestScope = (String) map.get("testRequestScope");
    System.out.println(testRequestScope);
    
    return testRequestScope;
}

ModelMap

ModelMap的用法与Map十分类似。

@RequestMapping("/testModelMap")
@ResponseBody
public String testModelMap(ModelMap modelMap) {
    // 写入
    modelMap.addAttribute("testRequestScope", "Hello ModelMap!");
    // 读取
    String testRequestScope = (String) modelMap.getAttribute("testRequestScope");
    System.out.println(testRequestScope);
    
    return testRequestScope;
}

ModelAndView

ModelAndView是SpringMVC提供的用于共享request域对象数据和视图解析跳转的API。

通过ModelAndView共享request域数据,无需在形参中指定该类型参数,只需在方法中new一个即可。但是使用ModelAndView需要将该类型对象作为返回值返回。

除了使用原生ServletAPI之外,使用其它方法(如上MapModelModelMap等)共享request域数据,最终SpringMVC都会将模型数据和视图封装到ModelAndView中。

@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
    ModelAndView mav = new ModelAndView();
    // 处理模型数据,即向请求域request共享数据
    mav.addObject("testRequestScope", "Hello ModelAndView!");
    // 设置视图名称(返回视图需要有对应的页面)
    mav.setViewName("success");
    // 读取数据(第一次写入后可能读取失败,因为只有在方法执行完毕后才能真正写入)
    String testRequestScope = (String) mav.getModel().get("testRequestScope");
    System.out.println(testRequestScope);

    return mav;
}

success.html(在Webapp/WEB-INF/templates/下创建):

<!DOCTYPE html>
<html lang="zh">
    <head>
        <meta charset="UTF-8">
        <title>Success!</title>
    </head>
    <body>
        <h1>Success!</h1>
    </body>
</html>

其实ModelAndView也可以作为Controller方法的参数使用。

需要注意的是,使用ModelAndView设置视图对象无论是否使用@ResponseBody,返回的始终是视图。

Map、Model 和 ModelMap

在使用Map<String, Object>ModelModelMap时SpringMVC传入的都是BindingAwareModelMap类型对象。

  • Model是一个接口,它定义了addAllAttributes()getAttribute()等接口方法。

  • ModelMap继承了LinkedHashMap<String, Object>,所以它也是属于Map的子类。ModelMap给出了addAllAttributes()getAttribute()等接口的实现。

  • BindingAwareModelMap继承自ExtendedModelMap类,而ExtendedModelMap又是ModelMap的子类和Model接口的实现。

    public class ExtendedModelMap extends ModelMap implements Model { 
        /* ... */ 
    }
    

    BindingAwareModelMap重写了Mapput()putAll(),使得它能作为Map来读写request域。

综上,BindingAwareModelMap可以作为ModelMap<String, Object>ModelMap传入Controller方法中。

在浏览器发送请求后,实际上调用Controller中对应方法的是DispatcherServlet中的doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

    /* ... */

    try {
        ModelAndView mv = null;
        /* ... */

        try {

            /* ... */
            
            // Actually invoke the handler.(实际的请求处理者)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            // 最后返回一个封装好的ModelAndView对象
            
            /* ... */
            
        }
        
        /* ... */

        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    }/* ... */

    /* ... */

}

通过断点调试最后发现,无论是MapModelModelMap还是ModelAndView,它们最后都会被封装为ModelAndView(即使Controller方法返回的是ModelAndView,Controller方法中的ModelAndViewDispatcherServlet对象的doDispatch()中的ModelAndView地址也并不相同)。

processDispatchResult()中调用了下方所示方法来进行视图渲染:

render(mv, request, response);

向 Session 域共享数据

使用原生ServletAPI共享:

@RequestMapping("/testSession")
@ResponseBody
public String testSession(HttpSession session) {
    // 写入
    session.setAttribute("testSessionScope", "Hello Session!");
    // 读取
    String testSessionScope = (String) session.getAttribute("testSessionScope");
    System.out.println(testSessionScope);
    return testSessionScope;
}

Session可以从Request域中获取:

@RequestMapping("/testSessionInServlet")
@ResponseBody
public String testSession(HttpServletRequest request) {
    // 从Request域中获取Session
    HttpSession session = request.getSession();
    // 写入
    session.setAttribute("testSessionScope", "Hello Session In Servlet!");
    // 读取
    String testSessionScope = (String) session.getAttribute("testSessionScope");
    System.out.println(testSessionScope);
    return testSessionScope;
}

向 Application 域共享数据

通过Session域获取ServletContext

@RequestMapping("/testApplication")
@ResponseBody
public String testApplication(HttpSession session) {
    // 从Session域中获取ServletContext
    ServletContext application = session.getServletContext();
    // 写入
    application.setAttribute("testSessionScope", "Hello Application!");
    // 读取
    String testSessionScope = (String) application.getAttribute("testSessionScope");
    System.out.println(testSessionScope);

    return testSessionScope;
}

通过Request域获取ServletContext(不推荐):

@RequestMapping("/testApplicationInServlet")
@ResponseBody
public String testApplication(HttpServletRequest request) {
    // 从Request域中获取ServletContext
    ServletContext application = request.getServletContext();
    // 写入
    application.setAttribute("testSessionScope", "Hello Application In Servlet !");
    // 读取
    String testSessionScope = (String) application.getAttribute("testSessionScope");
    System.out.println(testSessionScope);

    return testSessionScope;
}

请求报文转换

HttpMessageConverter即报文信息转换器,能将请求报文转换为Java对象,或将Java对象转换为响应报文

HttpMessageConverter提供了两个注解和两个类型:

  • @ResponseBody:即上方Controller方法中使用的将返回值(Java对象)作为响应体发送给浏览器的注解。

  • ResponseEntity:可以作为Controller方法的返回值返回,并响应给浏览器。

  • @RequestBody:将Controller方法形参指定为请求体,并接收从浏览器发送过来的请求体。

    @PostMapping("/testRequestBody")
    @ResponseBody
    public String testRequestBody(@RequestBody String requestBody) {
        requestBody = "RequestBody{'" + requestBody + "'}";
        return requestBody;
    }
    
  • RequestEntity:是封装请求报文的一种类型,在Controller方法形参中使用,它获得的是整个请求报文

    @RequestMapping("/testRequestEntity")
    @ResponseBody
    public String testRequestEntity(RequestEntity<String> requestEntity) {
        HttpHeaders headers = requestEntity.getHeaders();
        String body = requestEntity.getBody();
        String response = "RequestEntity{" + headers +
                "}\nRequestBody{'" + body + "'}";
        System.out.println(response);
    
        return response;
    }
    

往响应体写入信息还有一个方法,就是使用原生ServletAPI

@RequestMapping("/testResponse")
public void testResponse(HttpServletResponse response) throws IOException {
    response.getWriter().print("Hello Response!");
}

文件上传下载

<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.3.1</version>
</dependency>

ResponseEntity 实现下载

ResponseEntity<byte[]>作为返回值,在其中设置好对应的响应头、响应体和状态码。

@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    // 获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    // 获取服务器中文件的真实路径 getRealPath()如果不带参数的话获取的是服务器的部署路径
    String realPath = servletContext.getRealPath("/static/img/test.png");
    System.out.println("RealPath: " + realPath);
    // 创建输入流
    FileInputStream is = new FileInputStream(realPath);
    // 创建字节数组
    byte[] bytes = new byte[is.available()];
    // 将流写到字节数组中
    is.read(bytes);
    // 创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    // 设置下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=test.png");
    // 设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    // 创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
    // 关闭输入流
    is.close();

    return responseEntity;
}

在下载文件之前,需要通过Session获取当前要下载文件的真实路径:

ServletContext servletContext = session.getServletContext();
String realPath = servletContext.getRealPath("/static/img/test.png");

获取了真实路径后,通过FileInputStream将文件读取并写入字节数组byte[]中:

FileInputStream is = new FileInputStream(realPath);
byte[] bytes = new byte[is.available()];
is.read(bytes);
is.close();

下载文件时,需要在响应头中设置下载方式以及文件名:

MultiValueMap<String, String> headers = new HttpHeaders();
// 以附件方式下载文件,并且默认文件名为test.png
headers.add("Content-Disposition", "attachment;filename=test.png");

最后将字节数组作为响应体,再加上响应头和状态码等信息,创建ResponseEntity

ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
return responseEntity;

文件上传

文件上传依赖:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

springMVC.xml中配置文件上传解析器:

<!-- 配置文件上传解析器,将上传的文件封装为MultipartFile -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

文件上传实现:

@PostMapping("/testUp")
@ResponseBody
public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    // 获取上传文件的文件名
    String fileName = photo.getOriginalFilename();
    // 获取上传文件的后缀名
    String suffixName = fileName.substring(fileName.lastIndexOf("."));
    // 将UUID作为文件名
    String uuid = UUID.randomUUID().toString();
    // 将UUID和后缀名拼接后的结果作为最终的文件名
    fileName = uuid + suffixName;
    // 需要通过 ServletContext 获取服务器中 photo 目录的路径
    ServletContext servletContext = session.getServletContext();
    // photo 其实也可以用 photo.getName() 替代
    String photoPath = servletContext.getRealPath("photo");
    File file = new File(photoPath);
    // 判断photoPath所对应路径是否存在
    if (!file.exists()) {
        // 若不存在,则创建目录
        file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName;
    photo.transferTo(new File(finalPath));
    return fileName + " OK!";
}

在上传文件时,需要考虑文件之间文件名的冲突问题,可以通过将文件名改为UUID解决:

// 获取上传文件的文件名
String fileName = photo.getOriginalFilename();
// 获取上传文件的后缀名
String suffixName = fileName.substring(fileName.lastIndexOf("."));
// 将UUID作为文件名
String uuid = UUID.randomUUID().toString();
// 将UUID和后缀名拼接后的结果作为最终的文件名
fileName = uuid + suffixName;

拦截器

拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行。拦截器可以在指定的发那个发调用前后执行预先设定的代码,可以阻止原始方法的执行。拦截器和过滤器在作用和执行顺序上很相似。它们的关系如下图所示:

拦截器和过滤器的关系

创建拦截器类:

@Component  // 由SpringMVC来管理
public class BookInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle...");
        return true;
    }

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

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

拦截器配置类

编写拦截器配置类有两种方式:

  • 实现WebMvcConfigurer接口;
  • 继承WebMvcConfigurationSupport类并重写方法。
@Configuration
// 扫描interceptor包
@ComponentScan("com.linner.interceptor")
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    @Autowired  // 自动装配
    private BookInterceptor bookInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        /*
            添加(声明)拦截器并配置拦截规则
            可以同时配置多个规则
            如果不添加拦截规则,默认拦截所有请求
        */
        registry.addInterceptor(bookInterceptor).addPathPatterns("/books", "/books/*");
    }
}

让SpringMVC扫描到拦截器的配置类:

@Configuration
@ComponentScan({"com.linner.controller", "com.linner.config"})
@EnableWebMvc
public class SpringMvcConfig {}

拦截器的配置类SpingMvcSupport可以书写在SpringMvcConfig中,以简化书写(并演示继承WebMvcConfigurer编写拦截器配置类):

@Configuration
@ComponentScan({"com.linner.controller", "com.linner.interceptor"})
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private BookInterceptor bookInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(bookInterceptor).addPathPatterns("/books", "/books/*");
    }
}

拦截器执行过程

运行程序,发送books开头的请求(如http://localhost/books),终端会有如下输出:

preHandle...
getAll...
postHandle...
afterCompletion...

拦截器的执行顺序如下:

  1. 执行preHandle()
    • preHandle()返回值为true
      1. 执行请求路径相应的方法或下一个拦截器的preHandle()
      2. 判断是否执行postHandle()
        • Controller被执行(后续拦截器链中没有一个preHandle()返回值为false),执行postHandle()
        • Controller没有被执行(后续拦截器链中存在一个preHandle()返回值为false),不执行postHandle()
      3. 执行afterCompletion()
    • preHandle()返回值为false
  2. 结束。

当配置多个拦截器时,形成拦截器链。多个preHandle()按照Interceptor被声明顺序执行;多个postHandle()按照Interceptor被声明顺序逆序执行。即,拦截器链的运行顺序以拦截器添加顺序为准

当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行。当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion()操作(afterCompletion()代表当前拦截器执行完成,与后续拦截器链中preHandle()的返回值和Controller是否被执行无关)。

假设现在有如下两个拦截器:

@Component
public class FirstInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("FirstInterceptor preHandle...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("FirstInterceptor postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("FirstInterceptor afterCompletion...");
    }
}

@Component
public class LaterInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("LaterInterceptor preHandle...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("LaterInterceptor postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("LaterInterceptor afterCompletion...");
    }
}

它们在拦截器类中的添加顺序为:

@Autowired
private FirstInterceptor firstInterceptor;
@Autowired
private LaterInterceptor laterInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 如果没有配置拦截路径,则默认拦截所有请求
    registry.addInterceptor(firstInterceptor);
    regisrty.addInterceptor(laterInterceptor);
}

访问任意资源后,终端输出:

FirstInterceptor preHandle...
LaterInterceptor preHandle...
LaterInterceptor postHandle...
FirstInterceptor postHandle...
LaterInterceptor afterCompletion...
FirstInterceptor afterCompletion...

实际上在DispatcherServletdoDispatch()方法中,在执行mv = ha.handle(...)之前进行了一个条件判断:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    /* ... */
    // 拦截器链
    HandlerExecutionChain mappedHandler = null;

    try {
        /* ... */
        try {
            /* ... */

            // 获取拦截器链
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            /* ... */

            // 执行相应Interceptor的preHandle
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            /* ... */

            // 执行相应Interceptor的postHandle
            mappedHandler.applyPostHandle(processedRequest, response, mv);

            /* ... */
        } catch /* ... */

        // 处理调度结果
        // 包含了ModelAndView的进一步处理(渲染视图、处理模型)、还有Interceptor的afterCompletion()调用等等
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

        /* ... */
    }
}

HandlerExecutionChain(控制器执行链)对象的applyPreHandle()方法:

/**
 * preHandle执行链
 */
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 读取interceptorList中的interceptor,并逐个执行它们的preHandle方法
    for (int i = 0; i < this.interceptorList.size(); i++) {
        // 获取interceptor
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        // 执行interceptor.preHandle
        // 如果有一个interceptor返回了false,则立即执行triggerAfterCompletion()并返回false
        if (!interceptor.preHandle(request, response, this.handler)) {
            triggerAfterCompletion(request, response, null);
            return false;
        }
        // 记录拦截器链中返回false的前一个拦截器下标
        this.interceptorIndex = i;
    }
    return true;
}

applyPostHandle()方法:

/**
 * postHandle执行链
 */
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
        throws Exception {
    // 按照倒序,逐个执行interceptor.postHandle()
    for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        interceptor.postHandle(request, response, this.handler, mv);
    }
}

triggerAfterCompletion()

/**
 * afterCompletion执行链
 */
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
    // 从interceptorIndex开始,倒序执行interceptor.afterCompletion()
    // 即triggerAfterCompletion()只会执行那些返回true的interceptor的afterCompletion()
    for (int i = this.interceptorIndex; i >= 0; i--) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        try {
            interceptor.afterCompletion(request, response, this.handler, ex);
        }
        catch (Throwable ex2) {
            logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
        }
    }
}

在一切正常运行完成后,调用Interceptor的afterCompletion()的情况有点复杂(DispatcherServlet中的processDispatchResult()):

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
        @Nullable Exception exception) throws Exception {

    /* ... */

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        // 渲染视图
        render(mv, request, response);
        /* ... */
    }

    /* ... */
        
    // 在处理完其它调度结果后,通过mappedHandler.triggerAfterCompletion()来调用interceptor.afterCompletion()
    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

DispatcherServletdoDispatch()方法中还有许多try ... catch,当捕捉到异常时,doDispatch()也会通过triggerAfterCompletion()方法来调用mappedHandler.triggerAfterCompletion()

更具上述代码总结出HandlerExecutionChain中的执行情况:

  • applyPreHandle():在执行Controller方法之前执行。

  • applyPostHandle():执行了Controller方法之后执行。

  • triggerAfterCompletion()

    两种执行情况:

    • 执行applyPreHandle()时,有一个拦截器返回了false
    • 执行完applyPostHandle()之后,且无错误时执行(即processDispatchResult()中的triggerAfterCompletion());
    • 执行applyPreHandle()applyPostHandle()和Controller方法过程中,出现异常时执行(出现异常时是在DispatcherServlettriggerAfterCompletion()中调用)。

配置文件配置拦截器

除了使用配置类外,也可以使用配置文件的方式来配置拦截器。

使用<mvc:interceptors>来配置拦截器。添加拦截器的方式有两种,一种是使用<bean class="..."/>,另一种是使用<ref bean="..."/>,这两种方式本质上没有什么区别。

<mvc:interceptors>
    <bean class="asia.linner.interceptor.FirstInterceptor"/>
    <!-- bean中指定的是Bean的默认id,即首字母小写的类名 -->
    <ref bean="laterInterceptor"/>
</mvc:interceptors>

<mvc:interceptors>中可以使用<mvc:interceptor>来配置具体的拦截规则:

<mvc:interceptors>
    <mvc:interceptor>
        <!-- <mvc:mapping>: 拦截路径 -->
        <mvc:mapping path="/**"/>
        <!-- <mvc:exclude-mapping>: 排除路径(不拦截) -->
        <mvc:exclude-mapping path="/users"/>
        <mvc:exclude-mapping path="/users/**"/>
        <!-- 假设只有users和books这两个请求 -->
        <ref bean="bookInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

静态资源处理器

@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 放行单个目录(Webapp目录下)
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        // 也可以将Webapp整个目录都添加:
        // registry.addResourceHandler("/**").addResourceLocations("classpath:/");
    }
}
  • addResourceHandler():定义访问资源路径。
  • addResourceLocations():定义访问路径时的静态资源目录。

异常处理

Spring MVC 自带了两个异常处理器分别是SimpleMappingExceptionResolverDefaultHandlerExceptionResolver

其中DefaultHandlerExceptionResolver是由Spring MVC定义的默认异常处理器,它的doResolveException()定义了一些常见的异常处理:

protected ModelAndView doResolveException(
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

    try {
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return handleHttpRequestMethodNotSupported(
                    (HttpRequestMethodNotSupportedException) ex, request, response, handler);
        }
        else if (ex instanceof HttpMediaTypeNotSupportedException) {
            return handleHttpMediaTypeNotSupported(
                    (HttpMediaTypeNotSupportedException) ex, request, response, handler);
        }
        else if (ex instanceof HttpMediaTypeNotAcceptableException) {
            return handleHttpMediaTypeNotAcceptable(
                    (HttpMediaTypeNotAcceptableException) ex, request, response, handler);
        }
        else if (ex instanceof MissingPathVariableException) {
            return handleMissingPathVariable(
                    (MissingPathVariableException) ex, request, response, handler);
        }
        else if (ex instanceof MissingServletRequestParameterException) {
            return handleMissingServletRequestParameter(
                    (MissingServletRequestParameterException) ex, request, response, handler);
        }
        else if (ex instanceof ServletRequestBindingException) {
            return handleServletRequestBindingException(
                    (ServletRequestBindingException) ex, request, response, handler);
        }
        else if (ex instanceof ConversionNotSupportedException) {
            return handleConversionNotSupported(
                    (ConversionNotSupportedException) ex, request, response, handler);
        }
        else if (ex instanceof TypeMismatchException) {
            return handleTypeMismatch(
                    (TypeMismatchException) ex, request, response, handler);
        }
        else if (ex instanceof HttpMessageNotReadableException) {
            return handleHttpMessageNotReadable(
                    (HttpMessageNotReadableException) ex, request, response, handler);
        }
        else if (ex instanceof HttpMessageNotWritableException) {
            return handleHttpMessageNotWritable(
                    (HttpMessageNotWritableException) ex, request, response, handler);
        }
        else if (ex instanceof MethodArgumentNotValidException) {
            return handleMethodArgumentNotValidException(
                    (MethodArgumentNotValidException) ex, request, response, handler);
        }
        else if (ex instanceof MissingServletRequestPartException) {
            return handleMissingServletRequestPartException(
                    (MissingServletRequestPartException) ex, request, response, handler);
        }
        else if (ex instanceof BindException) {
            return handleBindException((BindException) ex, request, response, handler);
        }
        else if (ex instanceof NoHandlerFoundException) {
            return handleNoHandlerFoundException(
                    (NoHandlerFoundException) ex, request, response, handler);
        }
        else if (ex instanceof AsyncRequestTimeoutException) {
            return handleAsyncRequestTimeoutException(
                    (AsyncRequestTimeoutException) ex, request, response, handler);
        }
    }
    catch (Exception handlerEx) {
        if (logger.isWarnEnabled()) {
            logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
        }
    }
    return null;
}

SimpleMappingExceptionResolver是Spring MVC提供的自定义异常处理器。

基于配置的异常处理

springMVC.xml中配置异常处理器:

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <!-- ... -->

    <!-- 配置异常处理 -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <!--
                    设置要处理的异常和返回的视图
                    prop的key:表示处理器方法执行过程中出现的异常
                    prop的值:表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
                 -->
                <prop key="java.lang.ArithmeticException">error</prop>
            </props>
        </property>
        <!-- 
            将异常信息共享在请求域中的键
            exceptionAttribute设置一个属性名,将出现的异常信息在请求域中进行共享
         -->
        <property name="exceptionAttribute" value="ex"/>
    </bean>

</beans>

在相应路径(如/WEB-INF/templates/)下创建异常视图(如error.html):

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
    <h1>出现异常</h1>
    <p th:text="${ex}"></p>
</body>
</html>

注:使用Thymeleaf管理Html视图,需要在<html>标签中声明xmlns:th="http://www.thymeleaf.org"

基于注解的异常处理

使用@ControllerAdvice标注在异常处理类上,这样的异常处理类与Controller类似。在类的方法上用@ExceptionHandler指定要处理的异常,@ExceptionHandlervalue属性接收一个Class类型的数组,意味着可以同时处理多个异常。

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler({
            ArithmeticException.class,
            NullPointerException.class
    })
    public String testException(
            Exception ex /* 当前出现的异常 */,
            Model model) {

        // 用Model设置属性,将异常信息返回
        model.addAttribute("exception", ex);
        return "error";
    }
}

ExceptionController中用@ExceptionHandler标注的方法,在它的参数列表中定义一个Exception类型的参数,可用于获取当前处理的实际的异常。


重定向

Spring MVC默认的方式是forward(即转发),而要使用redirect需要在视图名称中添加redirect:说明。Spring MVC重定向有以下几种方式:

@Controller
public class TestController {

    /**
     * 通过返回类型为String的方法,返回一个"redirect:..."的字符串进行重定向
     * @return 重定向的路径
     */
    @RequestMapping("/test1")
    public String test1() {
        return "redirect:/index.html";
    }

    /**
     * 通过ModelAndView设置视图名称为"redirect:..."
     * @return
     */
    @RequestMapping("/test2")
    public ModelAndView test2() {
        return new ModelAndView("redirect:/test1");
        // 相当于:
        /*
        ModelAndView mav = new ModelAndView();
        mav.setViewName("redirect:/index");
        return mav;
        */
    }

    /**
     * 通过原生ServletAPI
     */
    @RequestMapping("/test3")
    public void test3(HttpServletResponse response) throws IOException {
        response.sendRedirect("/test1");
    }
}

Spring MVC 执行流程

常用组件

  • DispatcherServlet前端控制器,由框架提供。

    作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求。

    Controller、Interceptor、HandlerExceptionResolver等等都由它来调用。

  • HandlerMapping处理器映射器,由框架提供。

    作用:根据请求的urlmethod等信息查找相应的Handler(即控制器方法)。

    就是请求中的@RequestMapping@GetMapping@PostMapping等等。将请求和控制器或控制器方法进行映射。

  • Handler处理器(控制器方法),由工程师开发。

    作用:在DispatcherServlet的控制下,Handler对具体的用户请求进行处理。

  • HandlerAdapter处理器适配器,由框架提供。

    作用:通过HandlerAdapter执行处理器(控制器方法)。

    由HandlerMapping找到对应的Handler,接着由HandlerAdapter执行对应的Handler。

  • ViewResolver视图解析器,由框架提供。

    作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView(例如forward,即转发时)、RedirectView(例如redirect,即重定向时)。

  • Viw视图,由框架或视图技术提供。

    作用:将模型数据通过页面展示给用户。

DispatcherServlet 继承链

DispatcherServlet $\xrightarrow{extends}$ FrameworkServlet $\xrightarrow{extends}$ HttpServletBean $\xrightarrow{extends}$ HttpServlet $\xrightarrow{extends}$ GenericServlet $\xrightarrow{implements}$ Servlet

DispatcherServlet 初始化过程

DispatcherServlet初始化过程需要根据它的继承链,查找每个类或接口的init()方法。

  • Servlet.init(ServletConfig)

    public void init(ServletConfig config) throws ServletException;
    
  • GenericServlet

    GenericServlet不仅实现了Servlet.init(ServletConfig)还给出了一个未实现的init()

    init(ServletConfig config)

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
    

    init()

    public void init() throws ServletException {}
    
  • HttpServlet

    HttpServlet并没有重写GenericServlet.init(ServletConfig)GenericServlet.init()

  • HttpServletBean

    HttpServletBean实现了GenericServlet.init()并且给出了一个未实现的initServletBean()

    init()

    @Override
    public final void init() throws ServletException {
    
        // Set bean properties from init parameters.
        /* ... */
    
        // Let subclasses do whatever initialization they like.
        initServletBean();
    }
    

    initServletBean():初始化ServletBean。

    protected void initServletBean() throws ServletException {}
    
  • FrameworkServlet

    FrameworkServlet实现了HttpServletBean.initServletBean(),并且给出了initServletBean()的实现。

    initServletBean()

    @Override
    protected final void initServletBean() throws ServletException {
    
        /* ... */
    
        try {
            // 初始化WebApplicationContext
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
        /* catch ... */
    
        /* ... */
    }
    

    initWebApplicationContext():初始化WebApplicationContext。

    protected WebApplicationContext initWebApplicationContext() {
        // 获取当前的WebApplicationContext
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;
    
        // 判断当前WebApplicationContext是否为空(第一次执行时恒为空)
        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent -> set
                        // the root application context (if any; may be null) as the parent
                        cwac.setParent(rootContext);
                    }
                    // 装配并刷新WebApplicationContext
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        /*
            如果wac为空则查找WebApplicationContext
            (第一次执行完后wac还是为空,因为没有任何WebApplicationContext)
         */
        if (wac == null) {
            // No context instance was injected at construction time -> see if one
            // has been registered in the servlet context. If one exists, it is assumed
            // that the parent context (if any) has already been set and that the
            // user has performed any initialization such as setting the context id
            wac = findWebApplicationContext();
        }
        // 如果wac为空则创建一个WebApplicationContext
        if (wac == null) {
            // No context instance is defined for this servlet -> create a local one
            // 创建一个WebApplicationContext
            wac = createWebApplicationContext(rootContext);
        }
    
        // 没有接收到刷新事件时
        if (!this.refreshEventReceived) {
            // Either the context is not a ConfigurableApplicationContext with refresh
            // support or the context injected at construction time had already been
            // refreshed -> trigger initial onRefresh manually here.
            synchronized (this.onRefreshMonitor) {
                // 刷新WebApplicationContext
                onRefresh(wac);
            }
        }
    
        if (this.publishContext) {
            // Publish the context as a servlet context attribute.
            // 将ServletContext作为属性,获取它的属性名
            String attrName = getServletContextAttributeName();
            // 将IOC容器在应用域共享
            getServletContext().setAttribute(attrName, wac);
        }
    
        return wac;
    }
    

    createWebApplicationContext(WebApplicationContext):通过WebApplicationContext创建WebApplicationContext。

    protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
        return createWebApplicationContext((ApplicationContext) parent);
    }
    

    createWebApplicationContext(ApplicationContext):通过ApplicationContext创建WebApplicationContext。

    protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        Class<?> contextClass = getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException(
                    "Fatal initialization error in servlet with name '" + getServletName() +
                    "': custom WebApplicationContext class [" + contextClass.getName() +
                    "] is not of type ConfigurableWebApplicationContext");
        }
        // Web IOC 容器对象(即SpringMVC IOC)
        ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    
        // 配置环境
        wac.setEnvironment(getEnvironment());
        /* 
            整合Spring和SpringMVC时,
            设置SpringMVC的父容器,
            让Spring和SpringMVC的IOC容器能无缝衔接
            SpringMVC IOC容器是Spring IOC容器的子容器
         */
        wac.setParent(parent);
        String configLocation = getContextConfigLocation();
        if (configLocation != null) {
            wac.setConfigLocation(configLocation);
        }
        // 装配并刷新WebApplicationContext
        configureAndRefreshWebApplicationContext(wac);
    
        return wac;
    }
    

    onRefresh():刷新WebApplicationContext。

    protected void onRefresh(ApplicationContext context) {
        // For subclasses: do nothing by default.
        // 由子类去实现
    }
    

    FrameworkServlete创建WebApplicationContext后,刷新容器,调用onRefresh(wac),此方法在DispatcherServlet进行了重写(实现)。

    getServletContextAttributeName()

    // FrameworkServlet的全类名.CONTEXT.
    public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class.getName() + ".CONTEXT.";
    
    public String getServletContextAttributeName() {
        // 前缀+Servlet友好名称(即前缀 + <servlet-name>)
        return SERVLET_CONTEXT_PREFIX + getServletName();
    }
    
  • DispatcherServlet

    实现了FrameworkServlet.onRefresh()

    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }
    

    initStrategies()DispatcherServlet初始化策略。

    protected void initStrategies(ApplicationContext context) {
        // 初始化多个解析器,例如文件上传解析器等
        initMultipartResolver(context);
        initLocaleResolver(context);
        // 初始化模板解析器
        initThemeResolver(context);
        // 初始化处理器映射器
        initHandlerMappings(context);
        // 初始阿虎处理器适配器
        initHandlerAdapters(context);
        // 初始化异常处理器
        initHandlerExceptionResolvers(context);
        // 初始化转换器,将请求转换为视图名称
        initRequestToViewNameTranslator(context);
        // 初始化视图解析器
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    

DispatcherServlet 请求处理过程

  • ServletGenericServlet

    Servlet提供了service()接口,GenericServlet并未对其进行实现。

    service(ServletRequest, ServletResponse)

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
    
  • HttpServlet

    service()HttpServlet实现。并且HttpServlet还提供了service(HttpServletRequest, HttpServletResponse)

    service(ServletRequest, ServletResponse)

    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException {
    
        HttpServletRequest  request;
        HttpServletResponse response;
    
        if (!(req instanceof HttpServletRequest &&
                res instanceof HttpServletResponse)) {
            throw new ServletException("non-HTTP request or response");
        }
    
        // 转换为HttpServletRequest和HttpServletResponse
        request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;
    
        service(request, response);
    }
    

    HttpServlet.service(ServletRequest, ServletResponse)的主要作用就是将ServletRequestServletResponse分别转换为HttpServletRequestHttpServletResponse,然后调用HttpServlet.service(HttpServletRequest, HttpServletResponse)

    service(HttpServletRequest, HttpServletResponse)

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    
        // 获取请求方式
        String method = req.getMethod();
    
        // 请求的分发处理,根据请求方式调用相应方法(如调用doGet)
        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
    
        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
    
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
    
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
    
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
    
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
    
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
    
        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //
    
            /* ... */
        }
    }
    
  • HttpServletBean

    没有对HttpServlet.service()HttpServletdo开头的处理请求分发的方法进行重写。

  • FrameworkServlet

    重写了HttpServlet.service(HttpServletRequest, HttpServletResponse)HttpServletdo开头的处理请求分发的方法。

    service(HttpServletRequest, HttpServletResponse)

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    
        HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
        // 请求方式为PATCH或null时
        if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
            // 执行请求
            processRequest(request, response);
        }
        else {
            super.service(request, response);
        }
    }
    

    processRequest()

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    
        /* ... */
    
        try {
            // 执行服务
            doService(request, response);
        }
        /* catch ... */
        finally {
            /* ... */
        }
    }
    

    FrameworkServlet中的doGet()doPost()doPut()doDelete()中都是直接调用processRequest()方法:

    processRequest(request, response);
    

    doOptions()doTrace()中也有对processRequest()方法的调用。

    doService():交由子类实现。

    protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
            throws Exception;
    
  • DispatcherServlet

    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);
    
        // Keep a snapshot of the request attributes in case of an include,
        // to be able to restore the original attributes after the include.
        /* ... */
    
        // Make framework objects available to handlers and view objects.
        /* ... */
    
        try {
            // 最终调用doDispatch()来处理
            doDispatch(request, response);
        }
        finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // Restore the original attribute snapshot, in case of an include.
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
            }
        }
    }
    

    doDispatch()

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        /*
            执行链
            HandlerExecutionChain包含以下三个部分:
            - handler:与请求所匹配的控制器方法
            - interceptorList:处理控制器方法的所有拦截器集合,即拦截器链
            - interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
         */
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
    
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
    
            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);
    
                // Determine handler for the current request.
                // 获取当前请求的执行链
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }
    
                // Determine handler adapter for the current request.
                /*
                    获取当前请求的处理器适配器
                    通过控制器方法创建对应的处理器适配器,从而能调用所对应的控制器方法
                 */
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
                // Process last-modified header, if supported by the handler.
                /* ... */
    
                // 执行拦截器链的 preHandle,正序执行
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
    
                // Actually invoke the handler.
                // 通过HandlerAdapter来调用请求处理,最终获得ModelAndView对象
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
    
                applyDefaultViewName(processedRequest, mv);
                // 执行拦截器链的 postHandle,倒序执行
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            /* catch ... */
            // 执行完请求的后续处理,如视图渲染、异常处理等等
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        /* catch ... */
        /* finally ... */
    }
    

    processDispatchResult()

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
            @Nullable Exception exception) throws Exception {
    
        boolean errorView = false;
    
        // 异常处理
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                // 获取异常页面的视图
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                /* ... */
            }
        }
    
        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
            // 视图渲染
            render(mv, request, response);
            /* ... */
        }
        /* else ... */
    
        /* ... */
    
        if (mappedHandler != null) {
    		// Exception (if any) is already handled..
            // 执行拦截器链的 afterCompletion,倒序执行(此处所有的异常已经被处理完成)
    		mappedHandler.triggerAfterCompletion(request, response, null);
    	}
    }
    

MVC 执行流程

  1. 用户向服务器发送请求,请求被SpringMVC前端控制器DispatcherServlet捕获。

  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射:

    • 如果没有对应的映射:

      判断是否配置了mvc:default-servlet-handler(默认处理器)。

      • 如果没配置,则控制台报映射查找不到,向客户端展示404错误。

      • 如果有配置,则访问目标资源(一般为静态资源,如JS、CSS、HTML等等)。

        如果找不到客户端也会展示404错误。

    • 如果存在对应的映射则接着执行下面的流程。

  3. 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。

  4. DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter

  5. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler()方法。

    根据拦截器链的顺序,正序执行。

  6. 提取Request中的模型数据,填充Handler入参,开始执行HandlerController)方法,处理请求。

    在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

    • HttpMessageConveter: 将请求消息(如JSON、XML等数据)转换成一个对象,或将对象转换为指定的响应信息。

    • 数据转换:对请求消息进行数据转换。

      String转换成IntegerDouble等。

    • 数据格式化:对请求消息进行数据格式化。

      如将字符串转换成格式化数字或格式化日期等。

    • 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResultError中。

    • Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象。

  7. 如果Handler被成功执行,则开始执行拦截器的postHandle()方法。

    根据拦截器链的顺序,倒序执行。

  8. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据ModelView,来渲染视图。

  9. 渲染视图完毕执行拦截器的afterCompletion()方法。

    根据拦截器链的顺序,倒序执行。

  10. 将渲染结果返回给客户端。