SpringMVC 入门(一):基本使用

SpringMVC 是一种基于 Java 实现 MVC 设计模型的请求驱动类型的轻量级 Web 框架,属于 Spring 框架的后续产品,已经融合在 Spring Web Flow 中。SpringMVC 是目前最主流的 MVC 框架之一,通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口,同时它还支持 RESTful 编程风格的请求。

快速入门

入门案例

☕️ 建立 Web 的 Maven 工程

使用 IDEA 创建普通 Maven 项目之后,右击项目,选择Add Frameworks Support,勾选Java EE下面的Web Application,并选择 web.xml 的版本,点击确定,会自动在当前项目下创建 web 目录。

Web 项目还需要配置 Tomcat,此处设置项目的Application context/,访问端口为 8080。入门案例的整个项目目录如下所示:

☕️ 在 pom.xml 文件中导入 jar 包依赖

<dependencies>
    <!-- spring-context,该 jar 包会将 aop、beans、core、expression 一并下下来 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    <!-- Servlet -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- jsp -->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.3</version>
        <scope>provided</scope>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

☕️ 编写 Spring 核心配置文件 applicationContext.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"
       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">

    <!-- 配置 IoC 容器注解扫描的包路径 -->
    <context:component-scan base-package="com.example">
        <!-- 制定扫包规则,不扫描 @Controller 注解修饰的 Java 类,其它还是要扫描 -->
        <context:exclude-filter type="annotation"
                                expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
</beans>

☕️ 编写 SpringMVC 的核心配置文件 springmvc-config.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置 IoC 容器注解扫描的包路径 -->
    <!-- 使用 context:include-filter 时,use-default-filters 属性必须设置为 false -->
    <context:component-scan base-package="com.example" use-default-filters="false">
        <!-- 制定扫包规则,只扫描使用 @Controller 注解修饰的 Java 类 -->
        <context:include-filter type="annotation"
                                expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 开启 SpringMVC 的注解支持
         该注解会自动加载 RequestMappingHandlerMapping(处理器映射器)和 RequestMappingHandlerAdapter (处理适配器)等默认配置
    -->
    <mvc:annotation-driven/>
    
    <!-- 配置视图解析器 -->
    <!-- 真正的视图路径为:prefix + 逻辑视图名 + suffix -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

该文件的配置和 Spring 是一样,根标签都是<beans>,毕竟 SpringMVC 是 Spring 的子模块。为了方便管理,一般将非 web 模块的 bean 放在 Spring 核心配置文件中,web 模块的 bean 放在 SpringMVC 核心配置文件中。

需要注意:如果在<context:component-scan>标签中使用<context:include-filter>标签,一般都要设置 use-default-filters 属性为 false,原因请查看 component-scan 标签的 use-default-filters 属性用法

☕️ 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">

    <!-- 配置 Spring 提供的监听器,在 Web 容器启动时初始化 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- 指定 Spring 核心配置文件路径,用于初始化 Root WebApplicationContext 容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!-- 配置 SpringMVC 的核心组件 DispatcherServlet -->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 指定 SpringMVC 核心配置文件路径,用于初始化 Servlet WebApplicationContext 容器 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-config.xml</param-value>
        </init-param>
        <!-- 值为 0 或者大于 0 时,Web 容器在启动时会立即加载这个 Servlet  -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <!-- 设置能被 DispatcherServlet 拦截处理的请求 URL -->
        <servlet-name>DispatcherServlet</servlet-name>
        <!-- 默认配置,/ 表示拦截所有请求 URL,包括静态资源的请求,但不包括 *.jsp 请求;
             对于 *.jsp 的请求会交由 JspServlet 处理,所以非 WEB-INF 目录下的 *.jsp 能直接访问  
        -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- 配置编码过滤器 CharacterEncodingFilter,将请求消息的编码统一设置为 UTF-8 -->
    <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>
    </filter>
    <filter-mapping>
        <!-- 设置能被 CharacterEncodingFilter 拦截处理的请求 URL -->
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- * 是通配符,/* 表示拦截所有以 / 开头的请求 URL  -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

这里简单介绍下,在上面的配置中:

  • ContextLoaderListener 会被优先初始化时,根据指定的 Spring 的配置文件路径,创建出应用的根上下文Root WebApplicationContext

  • DispatcherServlet 是 SpringMVC 的核心组件。在初始化时,根据指定的 SpringMVC 的配置文件路径,创建出 SpringMVC 的独立上下文Web WebApplicationContext。该上下文在创建过程中,会判断Root WebApplicationContext是否存在,如果存在就将其设置为自己的 parent 上下文。这就是父子上下文(父子容器)的概念。

父子容器的作用在于,当我们尝试从子容器(Servlet WebApplicationContext)中获取一个 bean 时,如果找不到,则会委派给父容器(Root WebApplicationContext)进行查找。

需要注意1:由于Root WebApplicationContext是早于Servlet WebApplicationContext创建和初始化的,所以父容器的 bean 无法访问子容器的 bean,因为子容器还未初始化;而子容器的 bean 可以访问父容器的 bean,访问方式就是前面说的委派查找。说通俗点就是,在 Controller 层里可以注入 Service 对象,而 Service 层里无法注入 Controller 对象(编译可以通过,但是运行会出错)。

需要注意2:如果我们没有配置 ContextLoaderListener 来创建Root WebApplicationContext容器,那么Servlet WebApplicationContext的父上下文就是 null,也就是没有父容器。

需要注意3:上面配置中,DispatcherServlet 的 url-pattern 不能设置为/*,具体原因查看你真的了解DispatcherServlet 的 url

☕️ 编写 Controller 方法

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TestController {

    @RequestMapping("/test1")
    public String test1(Model model) {
        // Model 用于封装在视图上展示的数据
        model.addAttribute("msg", "您好!");
        // 返回的是逻辑视图名,需要视图解析器解析,解析后路径为 /WEB-INF/pages/index.jsp
        return "index";
    }
}

☕️ 编写 index.jsp 视图

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    ${msg}
</body>
</html>

☕️ 启动 tomcat,访问路径localhost:8080/test1

如果访问出现 404,服务器报错,可能原因是 IDEA 的项目发布中没有 lib 依赖,需要手动添加 jar 包(这是 IDEA 的 bug),流程如下:


流程分析

SpringMVC 的组件是是围绕 DispatcherServlet 运作的,该组件是 SpringMVC 的核心组件,是请求的入口,负责协调各个组件工作。具体流程如下:

发送请求

用户向服务器发送 HTTP 请求,请求会被 SpringMVC 的 DispatcherServlet 捕获。

映射处理器

DispatcherServlet 根据请求 URL,调用 HandlerMapping 以获得适合该请求的 Handler 配置(包括 Handler 对象本身及相关的拦截器),最后将 Handler 对象和拦截器打包,以 HandlerExecutionChain 对象的形式返回。

HandlerMapper 是处理器映射器,内部维护了<访问路径, 处理器>映射,负责为请求找到合适的 Handler。这里所谓的 Handler,简单理解的话,其实就是我们编写的 Controller 层方法。

处理器适配

DispatcherServlet 根据获得的 Handler,选择合适的 HandlerAdapter 去执行 Handler 逻辑。需要注意,如果成功获得合适的 HandlerAdapter 对象后,DispatcherServlet 会先去执行拦截器的 preHandler() 方法。

HandlerAdaptor 是处理器适配器,这里使用的是适配器模式。Spring 中的 Handler 的实现有多中,比如用户 Handler 既可以实现 Controller 接口,也可以用 @RequestMapping 注解将方法作为一个 Hander 等,这就导致 DispatcherServlet 不知道如何执行 Handler 逻辑。因此,此处使用到适配器模式,选择合适的 HandlerAdaptor 去执行 Handler 逻辑,最终返回统一形式的 ModelAndView 对象。

调用处理器方法

该过程首先会提取 Request 中的模型数据,填充 Handler 入参,然后开始执行 Handler 逻辑;Handler 执行完成后,会向 DispatcherServlet 返回一个 ModelAndView 对象。由于 SpringMVC 开发大多使用注解方式,所以此处的执行 Handler 逻辑其实就是执行被 @RequestMapping 相关注解修饰的 Controller 层方法。

DispatcherServlet 在获得 Handler 执行结果之后,会立即执行拦截器的 postHandler() 方法,然后再进行视图解析。

视图解析

DispatcherServlet 根据返回的 ModelAndView 选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver),解析出 View 对象,然后返回给 DispatcherServlet。

渲染视图

DispatcherServlet 将 Model 数据填充到 View 中,进行视图渲染。在成功渲染视图之后,DispatcherServlet 会调用拦截器的 afterCompletion() 方法。

响应请求

DispatcherServlet 将渲染后的 View 返回给用户。

⭐️ 更具体的流程图

⭐️ 代码分析图


RequestMapper 注解

该注解的作用是建立请求 URL 与 Controller 方法之间的对应关系,可以作用在类上,也可以作用在方法上:

  • 如果作用在类上,表示第一级的访问目录;
  • 如果作用在方法上,表示第二级的访问目录。

该注解的具体属性有:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";     // 给映射地址指定一个别名
    
    @AliasFor("path")
    String[] value() default {};  // 用于指定请求的路径,和 path 互为别名

    @AliasFor("value")
    String[] path() default {};   // 用于指定请求的路径,和 value 互为别名
 
    RequestMethod[] method() default {};   // 用于指定请求的方式(GET/POST/DELETE/PUT)

    String[] params() default {};    // 指定 Request 中必须包含某些参数值

    String[] headers() default {};   // 指定 Request 中必须包含某些指定的 Header 值
    
    String[] consumes() default {};  // 指定处理请求的提交内容类型(Content-Type),例如 application/json, text/html 

    String[] produces() default {};  // 指定返回的内容类型
}

✏️ value 或 path 属性

@RequestMapper 注解使用 value 或 path 属性指定请求的路径,这两个属性的类型都是String[],意味着一个 Controller 方法可以匹配多个路径,并且指定的请求路径可以使用通配符*。具体使用如下:

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping({
        // 指定请求路径     可以匹配到的路径
            "",              // /user
            "/page",         // /user/page
            "/page*",        // /user/page 或者 /user/pagexxx
            "/*page",        // /user/page 或者 /user/xxxpage
            "/page/*",       // /user/page/ 或者 /user/page/xxx
            "/page/*/abc",   // /user/page/xxx/abc
            "/page/**",      // /user/page 或者 /user/page/xxx/xxx(任意多级)
    })
    public String hello(Model model) {
        model.addAttribute("msg", "您好!");
        return "index";
    }
}

通配符*表示 0 个及以上个任意字符,一个*只能匹配一级路径,如果使用两个*则表示匹配任意多级路径。例如:/page/*可以匹配/page/xxx,但不能匹配/page/xxx/xxx,而/page/** 可以匹配任意以/page/开头的多级路径。

✏️ method 属性

该属性用于指定请求的方式,如果不使用该属性,默认任何方式的请求都会匹配。具体使用如下:

@RequestMapping(value = "/page")  // 匹配任意方式请求
public String all() {
    return "index";
}

@RequestMapping(value = "/page", method = RequestMethod.GET)  // 匹配 GET 方式请求
public String get() {
    return "index";
}

@RequestMapping(value = "/page", method = RequestMethod.POST) // 匹配 POST 方式请求
public String post() {
    return "index";
}

@RequestMapping(value = "/page", method = RequestMethod.PUT) // 匹配 PUT 方式请求
public String put() {
    return "index";
}

@RequestMapping(value = "/page", method = RequestMethod.DELETE) // 匹配 DELETE 方式请求
public String delete() {
    return "index";
}

为了方便书写,SpringMVC 提供了组合注解:

  • @GetMapping:等价@RequestMapping(method = RequestMethod.GET),匹配 GET 方式请求。
  • @PostMapping: 等价@RequestMapping(method = RequestMethod.POST),匹配 POST 方式请求。
  • @PutMapping:等价@RequestMapping(method = RequestMethod.PUT),匹配 PUT 方式请求。
  • @DeleteMapping:等价@RequestMapping(method = RequestMethod.DELETE),匹配 DELETE 方式请求。
@GetMapping("/page")  // 匹配 GET 方式请求
public String get() {  
    return "index";
}

@PostMapping("/page")  // 匹配 POST 方式请求
public String post() {
    return "index";
}

@PutMapping("/page")  // 匹配 PUT 方式请求
public String put() {
    return "index";
}

@DeleteMapping("/page")  // 匹配 DELETE 方式请求
public String delete() {
    return "index";
}

数据响应

Controller 方法的返回值可以是字符串类型、void 类型、ModelView 类型,甚至可以是 pojo 类。pojo 类的返回值一般会通过 @ResponseBody 注解使用 HttpMessageConverter 消息转换为 json,再返回给客户端,后续会介绍。

逻辑视图名和 Model

当 Controller 方法返回字符串时,该字符串一般被当做逻辑视图名,需要通过视图解析器解析为物理视图地址,之后经过视图渲染,最终返回给客户端视图页面。

📚 创建一个实体类:

package com.example.entity;

import lombok.Data;

@Data
public class User {
    private String username;
    private String password;
}

📚 创建 user.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    姓名:${user.username}<br>
    账号:${user.password}<br>
</body>
</html>

📚 创建 test2() 方法:

@Controller
public class TestController {
    //...
    @RequestMapping("/test2")
    public String test2(Model model) {
        // Model 用于封装在视图上展示的数据
        User user = new User();
        user.setUsername("张三");
        user.setPassword("123456");
        model.addAttribute("user", user);

        // 返回的是视图逻辑名,需要视图解析器解析,解析后路径为 /WEB-INF/pages/user.jsp
        return "user";
    }
}

📚 访问localhost:8080/test2

在前面的流程分析中提到,Handler 执行完成后,会向 DispatcherServlet 返回一个 ModelAndView 对象,返回的 ModelAndView 对象就是对这里的 Model 对象和逻辑视图名的封装。

SpringMVC 在调用 Controller 方法前,会创建一个隐含的数据模型,作为模型数据的存储容器。如果 Controller 方法入参为 Map 或者 Model 类型,SpringMVC 会将隐含模型的引用传递给 Map 或者 Model,在 Map 或者 Model 模型中可以访问 Controller 方法调用前存储的数据,也可以向模型中添加新的属性数据用于视图展示。


使用 @ResponseBody 注解

Controller 方法的返回字符串除了被当做为逻辑视图名进行解析外,也可以直接当做纯字符串返回给客户端的:

@Controller
public class TestController {
    //...
    @RequestMapping("/test3")
    @ResponseBody    // 将返回值经过 HttpMessageConverter 消息转换后返回给客户端
    public String test3() {
        return "nihao";
    }
}

访问localhost:8080/test3,页面显示:

nihao

重定向和请求转发

Controller 方法的返回字符串也可以用于重定向与请求转发:

  • redirect 重定向:将返回字符串设置为"redirect:url",请求会发生重定向,即要求浏览器访问新地址。浏览器进行了两次请求,所以浏览器地址栏发生变动,Request 域数据不可以共享。

  • forward 请求转发:将返回字符串设置为"forward:xxx",请求会进行转发。这是服务器内部的操作,所以浏览器地址栏不发生变动,Request 域数据可以共享。

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

    @RequestMapping("/test1")
    public String test1(HttpServletRequest request) {
        request.setAttribute("msg", "Hello World");
        // 请求转发到同一个类下的 /test2
        return "forward:test2";
    }

    @RequestMapping("/test2")
    public String test2(HttpServletRequest request) {
        System.out.println("test2:" + request.getAttribute("msg"));
        // 请求重定向到项目路径的 /user/test3
        return "redirect:/user/test3";
    }

    @RequestMapping("/test3")
    @ResponseBody
    public String test3(HttpServletRequest request) {
        System.out.println("test3:" + request.getAttribute("msg"));
        return "nihao";
    }
}

访问localhost:8080/user/test1,页面会跳转到localhost:8080/user/test3,显示:

nihao

后端控制台打印出:

test2:Hello World
test3:null

注意:请求转发和重定向时,url 前有斜线和没有斜线是有区别,有斜线是项目级别,无斜线是类级别下的。例如上述代码中,forward:test2是转发到同一个类下的/test2,也就是/user/test2;而forward:/test2是转发到项目路径下的/test2,所以 test2() 方法的返回值为redirect:/user/test3


使用 Servlet API

Controller 方法的返回类型可以为 void,此时可以使用 HttpServletResponse 返回给浏览器数据。Servlet 原始 API 可以作为 Controller 方法的参数,也就是说如果方法参数为 HttpServletRequest、HttpServletResponse 和 HttpSession,SpringMVC 在入参时会自动进行数据绑定。

@Controller
public class TestController {
    //...
    @RequestMapping("/test4")
    public void test4(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 遍历 Cookies
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie c : cookies) {
                String name = c.getName();
                String value = c.getValue();
                System.out.println(name + ":" + value);
            }
        }

        // 向浏览器发送数据
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("您好");
   }
}

ModelAndView

前面提到,ModelAndView 用来封装 Model 对象和逻辑视图名,其实 Controller 方法也可以将其作为返回值直接返回。具体使用如下:

@Controller
public class TestController {
    //...
    @RequestMapping("/test2")
    public ModelAndView test2(ModelAndView modelAndView) {
        // 添加 Model 数据
        User user = new User();
        user.setUsername("张三");
        user.setPassword("123456");
        modelAndView.addObject("user", user);
        // 添加逻辑视图名
        modelAndView.setViewName("user");
        return modelAndView;
    }
}

访问localhost:8080/test2

和 Model 对象一样,ModelAndView 可以直接写在 Controller 方法的形参中,SpringMVC 在入参时会自动进行数据绑定;也可以在 Controller 方法中创建 ModelAndView 对象,最终返回创建的对象,结果是一样的。


请求参数绑定

执行 Controller 方法时,SpringMVC 会将请求消息中的数据以一定方式转换并绑定在 Controller 方法的参数上,该操作叫做 SpringMVC 的数据绑定。具体流程如下:

① SpringMVC 将请求对象 ServletRequest 传递给 DataBinder。

② 将 Controller 方法的入参对象(形参)传递给 DataBinder。

③ DataBinder 调用 ConversionService 组件进行数据类型转换、数据格式化等工作,并将 ServletRequest 对象中的消息填充到参数对象中。

④ 调用 Validator 组件对已经绑定了请求消息数据的参数对象进行数据合法性校验。

⑤ 校验完成后会生成数据绑定结果 BindingResult 对象,SpringMVC 将 BindingResult 对象中的内容赋给 Controller 方法的相应参数上。

绑定简单数据类型

这里所谓的简单数据,指的是基本数据类型和 String 类型。下面分两种情况讨论:

✌ 请求中的参数名称和 Controller 方法的形参名称一致

对于简单数据,一般只要请求中的参数名称和 Controller 方法的形参名称一致即可,使用方式如下:

@Controller
public class TestController {
    //...
    @RequestMapping("/test5")
    @ResponseBody
    public String test5(String username, String password) {
        System.out.println("username:" + username);
        System.out.println("password:" + password);
        return "success";
    }
}

访问localhost:8080/test5?username=张三&password=123,后端控制台显示:

username:张三
password:123

✌ 请求中的参数名称和 Controller 方法的形参名称不一致

如果请求中的参数名称和 Controller 方法的形参名称不一致,可以使用 @RequestParam 注解把请求中的指定参数赋值给 Controller 方法的形参。该注解的属性如下:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";  // 指定请求中的参数名称,与 name 互为别名

    @AliasFor("value")
    String name() default "";   // 指定请求中的参数名称,与 value 互为别名

    boolean required() default true;  // 指定请求是否必须提供此参数,默认是 true

    // 表示如果请求没有同名参数时的默认值;如果设置该值,required 属性失效
    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

具体使用如下:

@Controller
public class TestController {
    //...
    // 将请求中的参数 username 赋值给形参 name;如果请求中没有 password 参数,默认赋值 123
    @RequestMapping("/test6")
    @ResponseBody
    public String test6(@RequestParam("username") String name, 
                        @RequestParam(defaultValue = "123") String password) {
        System.out.println("username:" + name);
        System.out.println("password:" + password);
        return "success";
    }
}

访问localhost:8080/test6?username=张三,后端控制台显示:

username:张三
password:123

绑定 pojo 类

绑定简单 pojo 类

pojo 类型也可以作为 Controller 方法的形参,要求请求中的参数名称和 pojo 的属性名称一致,SpringMVC 会自动进行数据绑定。具体使用如下:

@Controller
public class TestController {
    //...
    @RequestMapping("/test7")
    @ResponseBody
    public String test7(User user) {
        System.out.println("username:" + user.getUsername());
        System.out.println("password:" + user.getPassword());
        return "success";
    }
}

访问localhost:8080/test7?username=张三&password=123,后端控制台显示:

username:张三
password:123

注意:在使用 pojo 类型作为 Controller 方法形参时,请求中的参数名称一定与要绑定的 pojo 类中的属性名称一致,否则 pojo 对象中绑定的属性值为 null。

注意事项

需要注意,当 Controller 方法传入参数为 pojo 对象时,该对象是存储在 Model 中的。如下所示:

@RequestMapping("/test7")
@ResponseBody
public String test7(Model model, User user) {
    // 传入的 User 对象是存储在 Model 中的,key 为 user(类名,首字母小写)
    // 从 Model 获取 User 对象
    User user1 = (User) model.getAttribute("user");
    
    // 判断 user 和 user1 是否为同一个对象的引用
    System.out.println(user == user1);   // true
    
    System.out.println("username:" + user.getUsername());
    System.out.println("password:" + user.getPassword());
    return "success";
}

访问localhost:8080/test7?username=张三&password=123,后端控制台显示:

username:张三
password:123

上述结果表明,Controller 方法传入的 pojo 对象是存储在 Model 中的,所以如果需要在视图页面使用 user 对象的数据,不用显式地将 user 对象存入 Model 中。

绑定包装 pojo 类

使用简单 pojo 类型已经可以完成多数的数据绑定,但有时客户端请求中传递的参数会比较复杂。例如,在用户查询订单时,页面传递的参数可能包括订单编号、用户名称等信息,这就包含了订单和用户两个对象的信息。如果将订单和用户的所有查询条件都封装在一个简单 pojo 中,显然会比较混乱,这时就可以考虑使用包装 pojo 类型进行数据绑定。

@Data
public class Order {
    private Integer orderId;
    private User user;
}
@Controller
public class TestController {
    //...
    @RequestMapping("/test8")
    @ResponseBody
    public String test8(Order order) {
        System.out.println("orderId:" + order.getOrderId());
        System.out.println("user.username:" + order.getUser().getUsername());
        System.out.println("user.password:" + order.getUser().getPassword());
        return "success";
    }
}

访问localhost:8080/test8?orderId=1&user.username=abc&user.password=123,后端控制台显示:

orderId:1
user.username:abc
user.password:123

绑定数组和集合

绑定数组

在实际开发时,可能会遇到前端请求需要传递到后台一个或多个相同名称参数的情况 (如批量删除)。此时,就可以使用绑定数组的方式,来完成实际需求。

<!-- index.jsp -->
<form action="/test9" method="post">
    <table width="20%" border="1">
        <tr>
            <td>选择</td>
            <td>用户名</td>
        </tr>
        <tr>
            <td><input name="ids" value="1" type="checkbox"></td>
            <td>Tom</td>
        </tr>
        <tr>
            <td><input name="ids" value="2" type="checkbox"></td>
            <td>Jack</td>
        </tr>
        <tr>
            <td><input name="ids" value="3" type="checkbox"></td>
            <td>Mark</td>
        </tr>
    </table>
    <input type="submit" value="提交">
</form>
@Controller
public class TestController {
    //...
    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/test9")
    @ResponseBody
    public String test9(Integer[] ids) {
        for(int id : ids) {
            System.out.println("删除了" + id + "号");
        }
        return "success";
    }
}

访问localhost:8080/index

后端控制台输出:

删除了1号
删除了2号
删除了3号

绑定集合

在前面的绑定数组操作中,客户端请求传递的都是同名参数的用户 id,这种单一类型使用数组绑定数据即可。但如果是批量修改用户操作,前端请求传递过来的数据包含各种类型的数据,此时需要使用集合进行数据绑定。

@Data
public class UserVO {
    private List<User> users;
}
<!-- index.jsp -->
<form action="/test10" method="post">
  <table width="20%" border="1">
    <tr>
      <td>选择</td>
      <td>用户名</td>
    </tr>
    <tr>
      <td><input name="users[0].username" value="Tom" type="checkbox"></td>
      <td>Tom</td>
    </tr>
    <tr>
      <td><input name="users[1].username" value="Jack" type="checkbox"></td>
      <td>Jack</td>
    </tr>
    <tr>
      <td><input name="users[2].username" value="Mark" type="checkbox"></td>
      <td>Mark</td>
    </tr>
  </table>
  <input type="submit" value="修改">
</form>
@Controller
public class TestController {
    //...
    @RequestMapping("/test10")
    @ResponseBody
    public String test10(UserVO users) {
        for (User user : users.getUsers()) {
            System.out.println("修改名字为:" + user.getUsername());
        }
        return "success";
    }
}

访问localhost:8080/index

后端控制台输出:

修改名字为:Tom
修改名字为:Jack
修改名字为:Mark

自定义类型转换器

SpringMVC 默认提供了一些常用的类型转换器,例如前面使用中,请求的字符串转换成 int 类型进行参数绑定。但是不是所有的数据类型都提供了转换器,没有提供的就需要自定义转换器,例如:日期类型的数据就需要自定义转换器。

⭐️ 自定义 Converter 类需要实现org.springframework.core.convert.converter.Converter接口:

@FunctionalInterface
public interface Converter<S, T> {  // S 表示源类型,T 表示目标类型
    @Nullable
    T convert(S var1);
}

⭐️ 下面创建一个日期转换类 DateConverter:

package com.example.config;

import org.springframework.core.convert.converter.Converter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateConverter implements Converter<String, Date> {
    // 定义日期格式
    private String datePattern = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Date convert(String s) {
        // 格式化日期
        SimpleDateFormat format = new SimpleDateFormat(datePattern);

        try {
            return format.parse(s);
        } catch (ParseException e) {
            throw new IllegalArgumentException("无效的日期格式,请使用这种格式: " + datePattern);
        }
    }
}

⭐️ 在 springmvc-config.xml 添加一个自定义类型转换器配置:

<!-- 配置类型转换器工厂 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <!-- 给工厂注入一个新的类型转换器 -->
    <property name="converters">
        <array>
            <!-- 配置自定义类型转换器 -->
            <bean class="com.example.config.DateConverter"/>
        </array>
    </property>
</bean>

<!-- 开启 SpringMVC 的注解支持;引用类型转换器工厂 -->
<mvc:annotation-driven conversion-service="conversionService"/>

⭐️ 测试:

@Controller
public class TestController {
    //...
    @RequestMapping("/test11")
    @ResponseBody
    public String test11(Date date) {
        System.out.println(date);
        return "success";
    }
}

访问localhost:8080/test11?date=2020-01-01 15:55:55,后端控制台输出:

Wed Jan 01 15:55:55 CST 2020

json 数据转换

json 是一种轻量级的数据交换格式,广泛用于前后台的数据交互,SpringMVC 提供 json 数据转换。

MappingJackson2HttpMessageConverterHttpMessageConverter<T>接口的实现类,是 SpringMVC 默认处理 json 格式请求和响应的实现类。该实现类会将请求信息中的 json 数据转换为类型为 T 的对象,并绑定到 Controller 方法的形参中;将 Controller 方法的返回对象转换为 json 数据返回给客户端。

要使用MappingJackson2HttpMessageConverter必须引入 Jackson 的开源包,因为该实现类利用的是该开源包对 json 数据进行转换和读写。Jackson 所需的依赖如下:

<!-- JSON 转换核心包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.11.1</version>
</dependency>

<!-- JSON 转换的数据绑定包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.1</version>
</dependency>

<!-- JSON 转换注解包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.11.1</version>
</dependency>

📚 在使用 json 数据转换时,需要用到两个重要的注解:

  • @RequestBody:使用适合的 HttpMessageConverter 对请求体内容进行转换,并绑定到 Controller 方法的形参上,注意该注解不适用 GET 请求方式。
  • @ResponseBody:将 Controller 方法返回的对象通过合适的 HttpMessageConverter 转换后,返回给客户端。

📚 下面是 json 数据转换的具体案例:

<!-- index.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script type="text/javascript">
        function testJson() {
            $.ajax({
                url: "${pageContext.request.contextPath}/test12",
                type: "post",
                // data 表示发送的数据
                data: JSON.stringify({
                        username: $("#username").val(),
                        password: $("#password").val()
                    }),
                // 定义发送请求的数据格式
                contentType: "application/json;charset=UTF-8",
                // 成功响应的结果
                success: function (data) {
                    alert(JSON.stringify(data));
                }
            })
        }
    </script>
</head>
<body>
<form>
    用户名:<input type="text" name="username" id="username"><br>
    密&nbsp&nbsp&nbsp码:<input type="password" name="password" id="password"><br>
    <input type="button" value="测试" onclick="testJson()">
</form>
</body>
</html>
@Controller
public class TestController {
    //...
    @RequestMapping("/test12")
    @ResponseBody
    public User test12(@RequestBody User user) {
        System.out.println("username:" + user.getUsername());
        System.out.println("password:" + user.getPassword());
        return user;
    }
}

上述代码中,@RequestBody 注解将请求体中的 json 数据绑定到 User 对象上;@ResponseBody 注解将返回的 User 对象先转换为 json 数据,再返回给客户端。

访问localhost:8080/index

后端控制台显示:

username:张三
password:123

RESTful 风格

Restful 是一个资源定位及资源操作的风格,不是标准也不是协议,只是一种风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

✍ 功能:

  • 资源:互联网所有的事物都可以被抽象为资源
  • 资源操作:使用 POST、DELETE、PUT、GET,使用不同方法对资源进行操作。
  • 分别对应添加、删除、更新、查询。

✍ 传统方式操作资源:

  • localhost:8080/item/queryItem.action?id=1查询,GET
  • localhost:8080/item/saveItem.action新增,POST
  • localhost:8080/item/updateItem.action更新,POST
  • localhost:8080/item/deleteItem.action?id=1删除,GET或POST

✍ 使用RESTful操作资源

  • localhost:8080/item/1查询,GET
  • localhost:8080/item新增,POST
  • localhost:8080/item更新,PUT
  • localhost:8080/item/1删除,DELETE

✍ PathVariable 注解

RESTful 风格通常需要使用到 @PathVariable 注解。该注解可以将 URL 上的占位符绑定到 Controller 方法的形参中,例如:请求 URL 为/item/{id},这里的{id}就是占位符。该注解属性如下:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";   // 指定 URL 上的占位符,和 name 互为别名

    @AliasFor("value")
    String name() default "";   // 指定 URL 上的占位符,和 value 互为别名

    boolean required() default true;   // URL 上是否一定要提供指定占位符
}

@PathVariable 注解如果没有指定 value 或者 name 属性,默认会绑定和方法形参同名的占位符。使用如下:

@Controller
public class TestController {
    //...
    @RequestMapping("/test13/{username}/{password}")
    @ResponseBody
    public String test13(@PathVariable("username") String name, 
                         @PathVariable String password) {
        System.out.println("username:" + name);
        System.out.println("password:" + password);
        return "success";
    }
}

访问localhost:8080/test13/张三/123,后端控制台显示:

username:张三
password:123

✍ RESTFul 风格代码

@Controller
public class TestController {
    // 查询数据
    @GetMapping("/item/{id}")
    @ResponseBody
    public User selectUser(@PathVariable String id) {
		// ...
        return user;
    }

    // 新增数据
    @PostMapping("/item")
    public void insertUser(@RequestBody User user) {
        // ...
    }

    // 更新数据
    @PutMapping("/item")
    public void updateUser(@RequestBody User user) {
        // ...
    }

    // 删除数据
    @DeleteMapping("/item/{id}")
    public void deleteUser(@PathVariable String id) {
        // ...
    }
}

✍ @RestController 注解

RESTful 风格的数据交互形式一般使用 json,所以 SpringMVC 提供了 @RestController 注解。该注解是一个组合注解,是 @Controller 和 @ResponseBody 的组合,等价在 Controller 类的所有方法上添加 @ResponseBody 注解。

@RestController
public class TestController {
    //...
}

静态资源访问

DispatcherServlet 的路径拦截规则/会将静态资源的请求 URL 也拦截下来,这会导致静态资源无法直接访问。因此,需要添加对静态资源的访问映射配置,告诉处理器映射器去指定路径下寻找静态资源。

💡 在 springmvc-config.xml 中进行如下配置:

<!-- 配置静态资源的访问映射
     mapping:对外暴露的访问路径,"/**" 表示静态资源目录下的所有文件都能访问
     location:映射到的本地静态资源路径,具体到某个文件夹
-->
<mvn:resources mapping="/js/**" location="/js/" />
<mvn:resources mapping="/css/**" location="/css/"/>

配置完之后,就可以对 js 和 css 目录下的所有文件进行访问。

💡 下面进行测试,在 js 目录下创建个 test.js 文件,文件内容如下:

Hello World

访问localhsot:8080/js/test.js


文件上传和下载

文件上传

☕️ 导入 jar 包

SpringMVC 一般通过使用 MultipartResolver 接口的实现类 CommonsMultipartResolver 对上传的文件进行解析处理,需要导入支持文件上传的相关依赖:

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

☕️ 文件上传表单

<!-- index.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
    <form action="/fileUpload" method="post" enctype="multipart/form-data">
        名称:<input type="text" name="name"><br>
        <!-- 如果选择多文件上传,name 取值需要一样 -->
        文件1:<input type="file" name="uploadFile"><br>
        文件2:<input type="file" name="uploadFile"><br/>
        <input type="submit" value="上传">
    </form>
</body>
</html>

多数文件上传都是通过表单形式提交给后台服务器的,该表单必须满足以下 3 个条件:

  • form 表单的 method 属性设置为 post;
  • form 表单的 enctype 属性设置为multipart/form-data,浏览器会采用二进制流的方式来处理表单数据;
  • 提供<input type="file" name="filename"/>的文件上传输入框。

☕️ 在 springmvc-config.xml 中添加文件上传解析器的配置

<!-- 配置文件上传解析器 MultipartResolver
     此处需要注意,文件上传解析器的 id 是固定的,不能取其它的名称
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 设置编码方式 -->
    <property name="defaultEncoding" value="UTF-8"/>
    <!-- 设置允许上传文件的最大值,单位为字节 -->
    <property name="maxUploadSize" value="5242880"/>
    <!-- 设置缓存中的最大尺寸,单位为字节 --> 
    <property name="maxInMemorySize" value="40960" />    
    <!-- 推迟文件解析,以便在 Controller 中捕获文件大小异常 -->
    <property name="resolveLazily" value="true"/>
</bean>

☕️ 编写上传文件的 Controller 方法

@Controller
public class FileUploadController {
    
    // 执行文件上传
    @RequestMapping("/fileUpload")
    @ResponseBody
    public String handleFormUpload(String name, List<MultipartFile> uploadFile, HttpServletRequest request) {
        // 判断所上传文件是否存在
        if(!uploadFile.isEmpty() && uploadFile.size() > 0) {
            // 迭代处理多个文件
            for(MultipartFile file : uploadFile) {
                if(file.getSize() == 0) {
                    // 文件大小为 0
                    continue;
                }
                // 获取上传文件的原始名称
                String originalFilename = file.getOriginalFilename();
                // 设置上传文件的保存地址目录
                String dirPath = request.getServletContext().getRealPath("/upload");
                // 输出路径
                System.out.println(dirPath);
                File filePath = new File(dirPath);
                // 如果保存文件的目录不存在,就先创建目录
                if(!filePath.exists()) {
                    filePath.mkdirs();
                }
                // 使用 UUID 重新命名上传的文件名称(上传人_uuid_原始文件名称)
                String newFileName = name + "_" + UUID.randomUUID() + "_" + originalFilename;
                try {
                    //使用 MultipartFile 接口的方法完成文件上传到指定位置
                    file.transferTo(new File(dirPath + "/" + newFileName));
                } catch (IOException e) {
                    e.printStackTrace();
                    return "error";
                }
            }
            return "success";
        }else {
            return "error";
        }
    }
}

访问localhost:8080/index

选择文件,点击上传后,控制台后端输出路径为:

项目目录\out\artifacts\TestSpringMVC_war_exploded\upload

☕️ 前面我们通过使用 MultipartFile 接口提供的方法对上传的文件进行处理,该接口的方法如下:

// 以字节数组的形式返回文件的内容
byte[] getBytes();

// 返回文件的内容类型
String getContentType();

// 读取文件内容,返回一个 InputStream 流
InputStream getInputStream();

// 获取多部件 form 把表单的参数名称
String getName();

// 获取上传文件的初始化名
String getOriginalFilename();

// 获取上传文件的大小,单位是字节
long getSize();

// 判断上传的文件是否为空
boolean isEmpty();

// 将上传文件保存到目标目录下
void transferTo(File file);

文件下载

⭐️ 前端页面

在前端页面设置一个文件下载的超链接,该链接的 href 属性要指定后台文件下载的 Controller 方法以及文件名:

<!-- index.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>下载页面</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/download?filename=hello.jpg">
    hello.jpg
</a>
</body>
</html>

⭐️ 编写 Controller 方法

@Controller
public class FileDownloadController {

    @RequestMapping("/download")
    public ResponseEntity<byte[]> fileDownload(HttpServletRequest request, String filename) throws IOException {
        // 指定要下载的文件所在路径
        String path = request.getServletContext().getRealPath("/download");
        // 创建该文件对象
        File file = new File(path + "/" + filename);
        // 对文件名编码,防止中文文件乱码
        filename = this.getFilename(request, filename);
        // 设置响应头
        HttpHeaders headers = new HttpHeaders();
        // 通知浏览器以下载的方式打开文件
        headers.setContentDispositionFormData("attachment", filename);
        // 定义以二进制流的形式下载返回文件数据
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 使用 ResponseEntity 对象封装返回下载数据
        return new ResponseEntity<>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
    }

    // 根据浏览器的不同进行编码设置,返回编码后的文件名
    public String getFilename(HttpServletRequest request, String filename) throws UnsupportedEncodingException {
        // IE 不同版本 User-Agent 中出现的关键词
        String[] IEBrowserKeyWords = {"MSIE", "Trident", "Edge"};
        // 获取请求头代理信息
        String userAgent = request.getHeader("User-Agent");
        for(String keyWord : IEBrowserKeyWords) {
            if(userAgent.contains(keyWord)) {
                // IE 内核浏览器,统一为 UTF-8 编码显示
                return URLEncoder.encode(filename, "UTF-8");
            }
        }
        // 其他浏览器统一为 ISO-8859-1 编码显示
        return new String(filename.getBytes("UTF-8"),"ISO-8859-1");
    }
}

项目目录\out\artifacts\TestSpringMVC_war_exploded\download下存放一个命名为 hello.jpg 图片,然后访问localhost:8080/index

点击超链接会将 hello.jpg 图片下载下来。


拦截器

SpringMVC 中的拦截器(Interceptor)类似于 Servlet 中的过滤器,用于对 Handler 进行预处理和后处理,例如通过拦截器进行权限验证、记录请求信息的日志、判断用户是否登录等。

拦截器实现

☕️ SpringMVC 通过实现 Handlerlnterceptor 接口来自定义拦截器:

package com.example.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CustomInterceptor implements HandlerInterceptor {

    // 在 Controller 方法执行之前被调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandler 执行了!");
        return true;
    }

    // 在 Controller 方法执行之后,视图解析之前被调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandler 执行了!");
    }

    // 在整个流程处理完成,即渲染视图结束之后被调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion 执行了!");
    }
}

上述代码实现了 Handlerlnterceptor 接口的三个方法:

  • preHandler() :该方法会在 Controller 方法执行前被调用,其返回值决定是否继续后续操作:返回值为 true 时,继续向下执行;返回值为 false 时,会中断后续的所有操作(包括调用下一个拦截器和 Controller 方法的执行等)。
  • postHandle() :该方法会在 Controller 方法执行之后,视图解析之前被调用,可以通过此方法对请求域中的模型和视图做出进一步的修改;
  • afterCompletion():该方法会在整个流程处理完成,即渲染视图结束之后被调用,可以通过此方法实现一些资源清理、记录曰志信息等工作。

☕️ 在 springmvc-config.xml 中配置拦截器:

<!--配置拦截器-->
<mvc:interceptors>
    <mvc:interceptor>
        <!-- 配置需要拦截器作用的路径,/** 表示拦截全部的请求 -->
        <mvc:mapping path="/**"/>
        <!-- 配置不需要拦截作用的路径,静态资源的访问 URL 一般不拦截 -->
        <mvc:exclude-mapping path="/js/**"/>
        <mvc:exclude-mapping path="/css/**"/>
        <!-- 配置拦截器 bean -->
        <bean class="com.example.interceptor.CustomInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

前面提到过,静态资源的请求也会被 DispatcherServlet 拦截,并当做一个 handler 进行处理,而静态资源的请求一般不需要预处理和后处理,所以需要设置拦截器不对静态资源的请求 URL 进行拦截。

☕️ 创建一个测试用的 Controller 方法:

@Controller
public class TestController {
    //...
    @RequestMapping("/test14")
    public String test14() {
        System.out.println("test14 执行了!");
        return "index";
    }
}

访问localhost:8080/test14,后端控制台显示:

preHandler 执行了!
test14 执行了!
postHandler 执行了!
afterCompletion 执行了!

多个拦截器执行顺序

当有多个拦截器同时工作时,它们的 preHandle() 方法会按照拦截器在配置文件中的配置顺序执行,而它们的 postHandle() 方法和 afterCompletion() 方法则会按照配置顺序的反序执行:

☕️ 配置多个拦截器:

package com.example.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 拦截器 Intercepter1
public class Interceptor1 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Interceptor1 preHandler....");
        return true;
    }

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

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

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 拦截器 Interceptor2
public class Interceptor2 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Interceptor2 preHandler....");
        return true;
    }

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

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

☕️ 在 springmvc-config.xml 中配置拦截器:

<!--配置拦截器-->
<mvc:interceptors>
    <!-- 拦截器 Interceptor1 -->
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/js/**"/>
        <mvc:exclude-mapping path="/css/**"/>
        <bean class="com.example.interceptor.Interceptor1"/>
    </mvc:interceptor>

    <!-- 拦截器 Interceptor2 -->
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/js/**"/>
        <mvc:exclude-mapping path="/css/**"/>
        <bean class="com.example.interceptor.Interceptor2"/>
    </mvc:interceptor>
</mvc:interceptors>

访问localhost:8080/test14,后端控制台显示:

Interceptor1 preHandler....
Interceptor2 preHandler....
test14 执行了!
Interceptor2 postHandle....
Interceptor1 postHandle....
Interceptor2 afterCompletion....
Interceptor1 afterCompletion....

统一异常处理

SpringMVC 实现统一异常处理的思路如下图所示:

在系统开发时,Dao、Service、Controller 层出现的异常都通过 throws Exception 向上抛出,最后异常由前端控制器 DispatcherServlet 交由异常处理器进行处理。SpringMVC 的统一异常处理方式可以有以下两种:

  • @ControllerAdvice + @ExceptionHandler 注解;
  • 实现 HandlerExceptionResolver 接口。

@ControllerAdvice + @ExceptionHandler 注解

@ExceptionHandler 注解

@ExceptionHandle 注解用于修饰方法,该方法会捕获同一个 bean 的其它方法抛出的异常,并进行异常处理。该注解只有一个属性,用于指定要捕获的异常类型:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    Class<? extends Throwable>[] value() default {};  // 指定要捕获的异常类型
}

下面对该注解的使用进行讲解:

✏️ 创建 Controller 方法

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ExceptionController {
    @GetMapping("/e")
    public void test() {
        // 手动抛出一个异常
        throw new RuntimeException("出错了");
    }

    // 捕获同一个 bean 的其它方法抛出的异常,捕获的异常对象会自动绑定到方法形参
    @ExceptionHandler(RuntimeException.class)  // 指定要捕获的异常类型 
    public String exceptionHandler(Exception e, Model model) {
        model.addAttribute("msg", e.getMessage());
        return "exception";   // 将错误信息在视图页面展示
    }
}

✏️ 创建异常视图页面

<!-- exception.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    ${msg}
</body>
</html>

访问localhost:8080/e,客户端页面展示我们手动抛出的异常信息:

@ControllerAdvice 注解

@ControllerAdvice 注解的作用是对所有 Controller 控制器添加统一的操作或处理。该注解从名字拆开来就是 Controller 和 Advice,Advice 在 Spring AOP 中指的是通知,所以 @ControllerAdvice 可以简单理解为对所有 Controller 控制器进行 AOP 增强,而具体的业务织入则需要结合其它的注解进行实现。查看该注解的定义:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {}; // 扫描指定包路径下的 bean,与 basePackages 互为别名

    @AliasFor("value")
    String[] basePackages() default {}; // 扫描指定包路径下的 bean,与 value 互为别名

    Class<?>[] basePackageClasses() default {};  // 扫描指定 Class 的 bean

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};  // 扫描指定注解修饰的 bean
}

@ControllerAdvice 注解本身是 @Component 的衍生注解,所以被 Spring 容器的注解扫描后会注册为一个 bean。默认配置下,@ControllerAdvice 注解会扫描 Spring 容器中所有 bean 进行增强,可以使用 basePackages、basePackageClasses 或 annotations 属性对扫描范围进行限制。

前面也提到了,@ExceptionHandler 注解会捕获同一个 bean 中抛出的异常并进行处理,两个注解联合使用可以实现统一异常处理,捕获所有 Controller 控制器抛出的异常并进行处理。下面介绍该异常处理的具体使用:

📚 创建异常处理类

package com.example.config;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 和上述注解联合使用,会捕获所有 Controller 控制器抛出的 RuntimeException 异常
    @ExceptionHandler(RuntimeException.class)   // 指定要捕获的异常类型
    public String exceptionHandler(Exception e, Model model) {
        model.addAttribute("msg", e.getMessage());
        return "exception";   // 将错误信息在视图页面展示
    }
}

📚 创建一个抛出异常的 Controller 方法

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ExceptionController {
    @GetMapping("/e")
    public void test() {
        // 手动抛出一个异常
        throw new RuntimeException("出错了");
    }
}

访问localhost:8080/e,客户端页面展示我们手动抛出的异常信息:


实现 HandlerExceptionResolver 接口

SpringMVC 还可以通过实现 HandlerExceptionResolver 接口来自定义异常处理器,实现统一异常处理:

package com.example.config;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyExceptionResolver implements HandlerExceptionResolver {

    // 全局性的异常捕获方法
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("msg", e.getMessage());
        modelAndView.setViewName("exception");
        return modelAndView;
    }
}

访问localhost:8080/e,客户端页面展示我们手动抛出的异常信息:


参考

  1. spring与springmvc父子容器
  2. 你真的了解DispatcherServlet 的 url
  3. Spring-MVC-原理探秘-一个请求的旅行过程
  4. SpringMVC - 运行流程图及原理分析
posted @ 2020-08-23 00:25  呵呵233  阅读(519)  评论(0编辑  收藏  举报