SpringMVC

Spring 全家桶之 SpringMVC

coffee beans
coffee beans

快速上手

MVC 是一种架构思想,将软件按照模型、视图、控制器来划分。用作处理数据的 JavaBean 可看作模型层;与用户进行交互,展示数据的前端页面即视图层;接收请求和响应浏览器的 servlet 是控制层。

用户通过视图层发送请求到服务器,在服务器中请求被 controller 接收,并调用相应的 Model 层处理请求,处理完毕将结果返回到 Controller,Controller 再根据请求处理的结果找到相应的 View 视图,渲染数据后最终响应给浏览器。

SpringMVC 是为表述层,即前端和后台 Servlet,提供的一整套完备的解决方案。

HelloWorld

前端控制器 DispatcherServlet 会处理符合 <servlet-mapping> 中 url-pattern 属性的请求,<servlet-mapping> 的作用是根据具体的 url 通知容器选择具体 Servlet。

前端控制器读取 springMVC 配置文件后,会通过扫描组件的方式搜寻控制器,并将请求地址和控制器中 @RequestMapping 注解的 value 属性值进行匹配。

若匹配成功,该注解所标识的控制器方法就是处理请求的方法。请求处理方法需返回一个字符串,该字符串作为视图名称被视图解析器解析,加上前缀和后缀组成视图的路径,通过 Thymeleaf 对视图进行渲染,最终转发到视图所对应页面。

  • Maven 的传递性

无需将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。

<dependencies>
    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- Spring5和Thymeleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
</dependencies>
  • 配置 web.xml

配置文件 <servlet-name>-servlet.xml 默认位于 WEB-INF 下,实际开发中通常将其放置于 resources 目录。

<!-- 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">
    <!-- 配置springmvc的前端控制器,对浏览器发送的请求统一处理 -->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置SpringMVC配置文件的位置和名称 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!--servlet初始化默认在访问时初始化 => 影响访问速度-->
        <!--dispatcherServlet初始化时间提前到服务器启动时-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <!-- /表示浏览器发送的所有请求,但是不包括jsp后缀 -->
        <!-- 不匹配 jsp 原因是其本质就是一个 servlet,需要服务器指定的 servlet 进行处理;就算能接收 jsp,那么也会当成普通请求处理,也不会找到其对应的 jsp 页面 -->
        <!-- /* 是表示所有请求,包括jsp -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  • controller

RequestDispatcher 接口提供将请求转发送到另一资源的功能,即 html、servlet 或 jsp 等。从路由到控制器的方法需通过 @RequestMapping 指定请求与处理方法之间的映射。

// com.xxx.controller.HelloController
@Controller // 标识为控制层组件 => Controller控制层组件、Service 业务层组件、Component 普通组件、repository 持久层组件
// 注解加扫描才能作为 Bean 进行管理
public class HelloController {
    // SpringMVC还有视图解析器 => 负责页面跳转
    // / => /WEB-INF/template/index.html
    // 控制器方法是处理请求的方法
    @RequestMapping(value="/") // 请求映射注解 => 请求与方法创建控制器关系
    public String index(){
        // 返回视图名称 => 决定最终跳转的页面
        return "index";
    }
    @RequestMapping(value = "/target")
    public String toTarget(){
        return "target";
    }
}
  • springMVC.xml
<!-- 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 扫描组件 -->
    <context:component-scan base-package="com.zszy.mvc.controller"></context:component-scan>
    <!-- 配置Thymeleaf视图解析器 -->
    <!-- 每当实现页面跳转时,若视图名称是符合条件的话,会被视图解析器解析,指派相对应的页面跳转 -->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
        <property name="order" value="1"/>
        <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>
</beans>
  • 视图层页面
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>index page</h1>
    <!-- 解决浏览器解析的绝对路径 -->
    <a th:href="@{/target}">target page</a>
</body>
</html>
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>target</title>
</head>
<body>
    hello target
</body>
</html>

@RequestMapping 注解

见名知意,@RequestMapping 将请求和处理请求的控制器方法进行关联映射。

  • @RequestMapping 注解位置

@RequestMapping 注解标识一个类 => 设置映射请求路径的初始信息
@RequestMapping 注解标识一个方法 => 设置映射请求路径的具体信息

  • @RequestMapping 注解的 value 属性

必须设置的 value 属性通过请求的请求地址匹配请求映射,其值是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求。

  • @RequestMapping 注解的 method 属性

通过请求方式匹配请求映射的 method 属性,其值是一个 RequestMethod.?? 类型的数组,表示该请求映射能够匹配多种请求方式的请求。

若当前请求的请求地址满足请求映射的 value 属性,但请求方式不满足 method 属性,浏览器会报出 405 的错误 => Request method 'POST' not supported

  • @RequestMapping 注解的 params 属性

params 属性通过请求参数匹配请求映射,其值是一个字符串类型的数组,可通过表达式形式设置请求参数和请求映射的匹配关系。

param => 要求请求映射所匹配的请求必须携带param请求参数
!param => 要求请求映射所匹配的请求必须不能携带param请求参数
param=value => 要求请求映射所匹配的请求必须携带param请求参数且param=value
param!=value => 要求请求映射所匹配的请求必须携带param请求参数但是param!=value
  • @RequestMapping 注解的 headers 属性

headers 属性通过请求头信息匹配请求映射,其属性是一个字符串类型的数组,也可通过表达式设置请求头信息和请求映射的匹配关系。

header => 要求请求映射所匹配的请求必须携带header请求头信息
!header => 要求请求映射所匹配的请求必须不能携带header请求头信息
header=value => 要求请求映射所匹配的请求必须携带header请求头信息且header=value
header!=value => 要求请求映射所匹配的请求必须携带header请求头信息且header!=value

若请求满足 value 和 method 属性,但不满足 headers 属性,此时页面显示 404 错误,即资源未找到。

  • ant 风格路径(模糊匹配风格)
设置 requestmapping 中的 value 属性
? => 表示任意的单个字符
* => 表示任意的0个或多个字符
/** => 表示任意的一层或多层目录 // 注意 => 在使用 /** 时,只能使用 /**/xxx 的方式
  • 路径占位符(重点)

SpringMVC 路径中的占位符常用于 RESTful 风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的 @RequestMapping 注解的 value 属性中通过占位符 {xxx} 表示传输的数据,在通过 @PathVariable 注解,将占位符所表示的数据赋值给控制器方法的形参。

原始方式 => /deleteUser?id=1
rest方式 => /deleteUser/1
@Controller
//@RequestMapping("/hello") // 加在类上用于不同模块控制器设置
public class RequestMappingController {
    @RequestMapping( // 不设置Method属性就是不以请求方式为条件,get、post都可以
            value={"/testRequestMapping","/test"}, // 处理多个请求
            method = {RequestMethod.GET,RequestMethod.POST}
    )
    public String success(){
        return "success";
    }
    @GetMapping("/testGetMapping") // 派生注解
    public String testgetMapping(){
        return "success";
    }
    @RequestMapping(value = "/testPut", method = RequestMethod.PUT)
    public String testPut(){
        return "success";
    }
    @RequestMapping(value = "/testParamsAndHeaders",params={"username","password!=123"},headers = {"Host=localhost:8080"})
    public String testParamsAndHeaders(){
        return "success";
    }
    @RequestMapping("/a?a/testAnt")
    public String testAnt(){
        return "success";
    }
    @RequestMapping("/testPath/{id}")
    public String testPath(@PathVariable("id") Integer id){ // 修饰房钱形参注解
        System.out.println(id);
        return "success";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
<h1>首页</h1>
<a th:href="@{/hello/testRequestMapping}">测试testRequestMapping注解位置</a><br>
<a th:href="@{/test}">测试testRequestMapping注解value属性 => test</a><br>
<a th:href="@{/testRequestMapping}">测试testRequestMapping注解value属性 => testRequestMapping</a><br>
<a th:href="@{/testRequestMapping}">测试testRequestMapping注解Method属性 => Get</a><br>
<form th:action="@{/test}" method="post">
    <input type="submit" value="测试testRequestMapping注解的Method属性 => post">
</form>
<a th:href="@{/testGetMapping}">测试GetMapping注解 => /testGetMapping</a><br>
<!--form若请求方式非get、post一律按get请求算-->
<form th:action="@{/testPut}" method="put">
    <input type="submit" value="测试form表单能否发送put或delete请求 => put">
</form>
<!-- 属性想要给thymeleaf解析加上th -->
<!--<a th:href="@{/testParamsAndHeaders?username=admin}">测试RequestMapping注解的mparams属性 => /testparamsAndheaders</a>-->
<a th:href="@{/testParamsAndHeaders(username='admin',password=12345)}">测试RequestMapping注解的mparams属性 => /testparamsAndheaders</a><br>
<a th:href="@{/aaa/testAnt}">测试requestMapping注解的ant风格 => /testAnt</a>
<a th:href="@{/testPath/1}">测试requestMapping注解支持路径中的占位符 => /testPath</a>
</body>
</html>
<!-- web.xml 配置解决中文乱码 -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    <!--配置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>
    <!-- 注册前端控制器 -->
    ...
</web-app>

获取请求参数

在能匹配到请求的基础上,就能通过 SpringMVC 获取参数,并在控制器方法中处理请求。dispatcherServlet 底层中调用的控制器方法,会根据当前控制器方法的形参,注入不同类型的值。

通过原生 ServletAPI 获取

将 HttpServletRequest 作为控制器方法的形参,此时 HttpServletRequest 类型的参数表示封装了当前请求的请求报文的对象。

控制器方法的形参获取请求参数

在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在 DispatcherServlet 中就会将请求参数赋值给相应的形参。

若请求所传输的请求参数中有多个同名的请求参数,可在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数。前者类型的形参数组中包含了每一个数据,后者参数的值为每个数据中间使用逗号拼接的结果。

通过注解获取

  • @RequestParam

@RequestParam 将请求参数和控制器方法的形参创建映射关系,其中属性 value 指定为形参赋值的请求参数的参数名。

属性 required 设置是否必须传输此请求参数,默认值为 true,即要求请求必须传输。当没有传输该请求参数,且未设置 defaultValue 属性时,页面会出现 400 报错,即 Required String parameter 'xxx' is not present

属性 defaultValue 不管 required 属性值为 true 或 false,当 value 所指定的请求参数没有传输或传输的值为 "" 时,使用默认值为形参赋值。

  • @RequestHeader

@RequestHeader 将请求头信息和控制器方法的形参创建映射关系。此注解的属性和用法同 @RequestParam。

  • @CookieValue

在首次执行 getSession() 方法时,JSESSIONID 的 Cookie 会存在于响应报文中,经此之后存在于请求报文。

@CookieValue 将 cookie 数据和控制器方法的形参创建映射关系。此注解的属性和用法同 @RequestParam。

通过 POJO 获取

在控制器方法的形参位置设置一个实体类类型的形参,若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值。

总结代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>testparams</title>
</head>
<body>
<h1>testparams</h1>
<a th:href="@{/testServletAPI(username='admin',password=12345)}">测试使用ServletAPI获取请求参数</a>
<a th:href="@{/testParam(username='admin',password=12345)}">测试使用控制器的形参获取请求参数</a>
<form th:action="@{/testParam}" method="post">
    用户名:<input type="text" name="user_name"><br>
    密码:<input type="password" name="password"><br>
    补充:<input type="checkbox" name="hobby" value="a">a
    <input type="checkbox" name="hobby" value="b">b
    <input type="checkbox" name="hobby" value="c">c<br>
    <input type="submit" value="测试使用控制器的形参获取请求参数">
</form>
<form th:action="@{/testBean}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    性别:<input type="radio" name="sex" value="男">男<input type="radio" name="sex" value="女">女<br>
    年龄:<input type="text" name="age"><br>
    邮箱:<input type="text" name="email"><br>
    <input type="submit" value="使用pojo实体类接收请求参数">
</form>
</body>
</html>
@Controller
public class ParamController {
    @RequestMapping("/testServletAPI")
    // 原生 servlet 获取请求参数
    // 在控制器方法中如果写了 request 对象类型的参数,那么这个参数就是表示当前的请求!
    public String testServletAPI(HttpServletRequest req){ // 这个方法是dispathcerServlet调用
        HttpSession session = req.getSession();
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        System.out.println("username: "+ username+", password "+ password);
        return "success";
    }
    // springmvc 获取请求参数
    @RequestMapping("/testParam")
    public String testParam(
            // 通过注解解决请求参数与形参名不相同的问题 => 前后端不是你改就是我改lol => 第二个属性表示是否传输所对应的请求参数 => 第三个参数在开发中使用最多
            @RequestParam(value="user_name",required=false,defaultValue = "defaultvalue") String username, // @RequestParam将请求参数与形参创建映射关系
            String password,
            String[] hobby,
            @RequestHeader(value="Host",required=false,defaultValue = "defaultvalue") String host,
            @CookieValue("JSESSIONID") String JSESSIONID){ // 保证当前控制器方法的形参和当前的请求参数名相同即可自动赋值
        // 多请求参数中出现多个同名的请求参数可以使用 String 或者 String[] 接收.
        // 若使用字符串类型的形参,最终结果为请求参数的每一个值之间使用逗号进行拼接的结果.
        System.out.println("username: "+ username+", password "+ password + ", hobby "+ Arrays.toString(hobby));
        System.out.println("host: "+ host);
        System.out.println("JSESSIONID: "+ JSESSIONID);
        return "success";
        // 若传递多个同名的请求参数
    }
    @RequestMapping("/testBean")
    public String testBean(User user){
        System.out.println(user);
        return "success";
    }
}

域对象共享数据

在能获取请求参数的情况下,下一步处理请求的过程应该是将请求参数作为条件去调用 service 的业务逻辑,service 业务逻辑又会调用 dao 操作数据库,最后再将结果通过返回给业务层传递到控制层。该过程中若有数据发往页面,那么应将这些数据在域对象中进行共享。

请求域 request、会话域 session、应用域 servletcontext 组成的域对象里,后者使用频率远小于前两者,原因是范围太大而较少有使用的必要。数据一直处于变化的状态,故在选择域对象的时候,要选择能实现功能且范围最小的域。开发中所常见的查询列表、表单回显数据都会放在会请求域,当然放在更大的域中也是可行的。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>index page</h1>
    <a th:href="@{/testRequestByServletAPI}">通过servletAPI向域共享数据</a><br>
    <a th:href="@{/testModelAndView}">通过ModelAndView向域共享数据</a><br>
    <a th:href="@{/testModel}">通过Model向域共享数据</a><br>
    <a th:href="@{/testMap}">通过Map向域共享数据</a><br>
    <a th:href="@{/testModelMap}">通过Map向域共享数据</a><br>
    <a th:href="@{/testSession}">通过原生 servletAPI HttpSession向session域共享数据</a><br>
    <a th:href="@{/testApplication}">通过原生 servletAPI 向Application域共享数据</a><br>
</body>
</html>

向 request 域共享数据

  • ServletAPI 向 request 域对象共享数据
/* ScopeController */
@Controller
public class ScopeController {
    // 使用servletAPI 向 request 域对象共享数据
    @RequestMapping("/testRequestByServletAPI")
    public String testRequestByServletAPI(HttpServletRequest req){
        // 原生方法
        req.setAttribute("testRequestScope","hello,servletAPI");
        return "success"; // 转发 => webINF下的资源重定向访问不了的 => 就是转发
    }
}
  • ModelAndView 向 request 域对象共享数据
/* ScopeController */
@Controller
public class ScopeController {
    // 使用 ModelAndView 向 request 域对象共享数据
    @RequestMapping("/testModelAndView")
    public ModelAndView testModelAndView(){ // 返回值必须是ModelAndView
        ModelAndView mav = new ModelAndView();
        // 处理模型数据,向请求域共享数据
        mav.addObject("testRequestScope","hello ModelAndView");
        // 设置视图名称 => 相当于返回的字符串
        mav.setViewName("success");
        return mav; // ModelAndView 实例必作为该方法的返回值返回
    }
}
  • Model 向 request 域对象共享数据
/* ScopeController */
@Controller
public class ScopeController {
    // Model 向 request 域对象共享数据
    @RequestMapping("/testModel")
    public String testModel(Model model){
        model.addAttribute("testRequestScope","hello model");
        System.out.println(model.getClass().getName()); // 全类名 => BindingAwareModelMap
        return "success";
    }
}
  • map 向 request 域对象共享数据
/* ScopeController */
@Controller
public class ScopeController {
    // 使用 map 向 request 域对象共享数据
    @RequestMapping("/testMap")
    public String testMap(Map<String, Object> map){
        map.put("testRequestScope", "hello Map");
        System.out.println(map);
        return "success";
    }
}
  • ModelMap 向 request 域对象共享数据
/* ScopeController */
@Controller
public class ScopeController {
    // ModelMap 向 request 域对象共享数据
    @RequestMapping("/testModelMap")
    public String testModelMap(ModelMap modelMap){ // ModelMap是Map的实现类(继承自LinkedHashMap)
        // ModelMap继承自Map,同时addAttribute内部调用了put方法.
        modelMap.addAttribute("testRequestScope", "hello ModelMap");
        System.out.println(modelMap);
        return "success";
    }
}
  • Model、ModelMap、Map的关系

Model、ModelMap、Map 类型的参数其实本质上都是 BindingAwareModelMap 类型。总而言之,三者的实际实例化的全类名是 BindingAwareModelMap。

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}
xxx.getClass().getName() => BindingAwareModelMap

向 session 域共享数据

  • Session 的钝化与活化

session 中的数据与服务器是否关闭无关,只和浏览器是否关闭有关。

session 的钝化 => 当服务器关闭,浏览器未关闭时,会话仍然继续,此时 session 中的数据会经过序列化存储到磁盘上。

session 的活化 => 若浏览器持续未关闭而服务器重新开启,此时会将钝化的文件内容重写读取入 session 中。

/* ScopeController */
@Controller
public class ScopeController {
    // 向 session 域共享数据
    @RequestMapping("/testSession")
    public String testSession(HttpSession session){
        session.setAttribute("testSessionScope", "hello session");
        return "success";
    }
}

向 application 域共享数据

/* ScopeController */
@Controller
public class ScopeController {
    // 向 application 域共享数据
    @RequestMapping("/testApplication")
    public String testApplication(HttpSession session){
        ServletContext application = session.getServletContext();
        application.setAttribute("testApplicationScope", "hello application");
        return "success";
    }
}

SpringMVC 的视图

SpringMVC 中的视图是 View 接口,用作渲染数据,将模型 Model 中的数据展示给用户,其种类默认有转发视图和重定向视图。

若工程引入 jstl 的依赖,转发视图会自动转换为 JstlView;若使用的视图技术为 Thymeleaf 并配置完成后,由此视图解析器解析所得是 ThymeleafView。

ThymeleafView

当控制器方法中所设的视图名称无任何前缀时,该视图名称会被配置文件中的视图解析器解析,视图名称拼接视图前缀和视图后缀所得的最终路径,将通过转发的方式实现跳转。

转发视图 InternalResourceView

当控制器方法中所设视图名称以 forward: 为前缀,创建转发视图。此时视图名不会被配置文件所设视图解析器解析,而是将前缀 forward: 去掉,剩余部分作为最终路径以转发的方式实现跳转。

重定向视图 RedirectView

当控制器方法中所设视图名称以 redirect: 为前缀,创建重定向视图,此时视图名不会被配置文件所设的视图解析器解析,而是将前缀 redirect: 去掉,剩余部分作为最终路径通过重定向的方式实现跳转

重定向视图在解析时,会先将 redirect: 前缀去掉,然后判断剩余部分是否以 / 开头,是则自动拼接上下文路径。

@Controller
public class ViewController {
    @RequestMapping("/testThymeleafView")
    public String testThymeleafView(){
        return "success";
    }
    @RequestMapping("/testForward")
    public String testForward(){
        return "forward:/testThymeleafView";
    }
    @RequestMapping("/testRedirect")
    public String testRedirect(){
        return "redirect:/testThymeleafView";
    }
}

视图控制器 view-controller

控制器方法仅用来实现页面跳转,那么只需要设置视图名称,可将处理器方法以 view-controller 标签进行表示。

设置任何一个 view-controller 时,其他控制器中的请求映射将全部失效,此时需要在核心配置文件中设置开启 mvc 注解驱动的标签 <mvc:annotation-driven />。

<!-- 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 https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 扫描组件-->
    <context:component-scan base-package="com.zszy.mvc.controller"></context:component-scan>
    <!-- 配置视图解析器 -->
    <!-- 配置Thymeleaf视图解析器 -->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
        <property name="order" value="1"/>
        <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>
    <!-- 当前控制器方法没有其他请求处理过程,那么可以设置视图名称|红色但是不影响 -->
    <!-- 若只在springMVC配置文件中设置viewcontroller,那么请求控制器中映射全部失效 => 必须开启mvc注解驱动 -->
    <mvc:view-controller path="/" view-name="index"></mvc:view-controller>
    <mvc:view-controller path="/test_rest" view-name="test_rest"></mvc:view-controller>
    <!-- 开启MVC的注解驱动 => 使得其他请求控制器中映射恢复 -->
    <mvc:annotation-driven />
</beans>

RESTful 表现层资源状态转移

REST 风格提倡 url 地址的使用统一,从前到后各个单词使用斜杠分开作为地址的一部分,不使用问号键值对方式携带请求参数。

操作 传统方式 REST风格
查询操作 getUserById?id=1 user/1-->get请求方式
保存操作 saveUser user-->post请求方式
删除操作 deleteUser?id=1 user/1-->delete请求方式
更新操作 updateUser user-->put请求方式

HiddenHttpMethodFilter

SpringMVC 提供 HiddenHttpMethodFilter 帮助将 POST 请求转换为 DELETE 或 PUT 请求,以解决浏览器只支持发送 GET 和 POST 的请求方式。

  • HiddenHttpMethodFilter 处理 put 和 delete 请求的条件

当前请求的请求方式必须为 post;请求必须传输请求参数 _method。

当满足条件时,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数 _method 的值,因此请求参数 _method 的值才是最终的请求方式。

  • 在 web.xml 中注册 HiddenHttpMethodFilter

CharacterEncodingFilter 和 HiddenHttpMethodFilter 过滤器,在 web.xml 中注册时,必须先注册前者,再注册后者。

原因是前者通过 request.setCharacterEncoding(encoding) 设置字符集的方法要求在此前不能有任何获取请求参数的操作,而后者恰有一个获取请求参数(方式)的操作 request.getParameter(this.methodParam)

<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    <!-- 多个过滤器是按照 filter-mapping 的顺序 -->
    <!-- 配置编码过滤器 => 设置编码之前不能获取任何的请求参数 -->
    <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>
    <!-- 配置 HiddenHttpMethodFilter 过滤器 -->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 配置springMVC前端控制器dispatcherServlet -->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 改变 dispatcher 默认位置和名称 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!-- 将servlet初始化时间提前到服务器启动时 -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>rest</title>
</head>
<body>
    <a th:href="@{/user}">查询所有用户信息</a>
    <a th:href="@{/user/1}">根据id查询用户信息</a>
    <form th:action="@{/user}" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="添加"><br>
    </form>
    <form th:action="@{/user}" method="POST">
        <!-- 隐藏域 => 不让用户看见 -->
        <input type="hidden" name="_method" value="PUT"><br>
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="修改"><br>
    </form>
</body>
</html>
@Controller
public class UserController {
    // 使用restful模拟用户资源增删改查
    @RequestMapping(value="/user",method = RequestMethod.GET)
    public String getAllUser(){
        System.out.println("查询所有用户信息");
        return "success";
    }
    @RequestMapping(value="/user/{id}",method = RequestMethod.GET)
    public String getUserById(){
        System.out.println("根据id查询用户信息");
        return "success";
    }
    @RequestMapping(value="/user",method = RequestMethod.POST)
    public String insertUser(String username,String password){
        System.out.println("添加用户信息成功 => "+username+"-"+password);
        return "success";
    }
    @RequestMapping(value="/user",method = RequestMethod.PUT)
    public String updateUser(String username,String password){
        System.out.println("修改用户信息成功 => "+username+"-"+password);
        return "success";
    }
}

HttpMessageConverter

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

该类提供了两个注解和类型 @RequestBody、@ResponseBody、RequestEntity、
ResponseEntity。Entity 作为实体表示整个报文。

<!-- 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">
    <!-- 多个过滤器是按照 filter-mapping 的顺序 -->
    <!-- 配置编码过滤器 => 设置编码之前不能获取任何的请求参数 -->
    <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>
    <!-- 配置 HiddenHttpMethodFilter 过滤器 -->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 配置springMVC前端控制器dispatcherServlet -->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 改变 dispatcher 默认位置和名称 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!-- 将servlet初始化时间提前到服务器启动时 -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
<!-- 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 https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 扫描组件-->
    <context:component-scan base-package="com.zszy.mvc.controller"></context:component-scan>
    <!-- 配置视图解析器 -->
    <!-- 配置Thymeleaf视图解析器 -->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
        <property name="order" value="1"/>
        <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"></mvc:view-controller>
    <mvc:view-controller path="/file" view-name="file"></mvc:view-controller>
    <mvc:default-servlet-handler></mvc:default-servlet-handler>
    <mvc:annotation-driven></mvc:annotation-driven>
    <!-- 文件上传解析器 => 将上传的文件封装为 MutipartFile => 必须设置id,根据id获取 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
</beans>
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>index</h1>
    <form th:action="@{/testRequestBody}" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" value="测试@RequestBody">
    </form>
    <form th:action="@{/testRequestEntity}" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" value="测试@RequestEntity">
    </form>
    <a th:href="@{/testResponse}">通过servletAPI的response对象响应浏览器数据</a>
    <a th:href="@{/testResponseBody}">通过@ResponseBody对象响应浏览器数据</a><br>
    <a th:href="@{/testResponseUser}">通过@ResponseBody对象响应浏览器User对象</a>
</body>
</html>

@RequestBody

获取请求体数据需要在控制器方法中设置一个形参,使用 @RequestBody 进行标识,当前请求的请求体就会为当前注解所标识的形参赋值。

/* com.zszy.mvc.controller */
@Controller
public class HttpController {
    @RequestMapping("/testRequestBody")
    public String testRequestBody(@RequestBody String requestBody){
        System.out.println(requestBody);
        return "success";
    }
}

RequestEntity

获取请求报文数据需在控制器方法的形参中设置该类型的形参,当前请求报文就会赋值给该形参,可通过 getHeaders() 获取请求头信息,getBody() 获取请求体信息。

/* com.zszy.mvc.controller */
@Controller
public class HttpController {
    @RequestMapping("/testRequestEntity")
    public String testRequestEntity(RequestEntity<String> requestEntity){
        System.out.println("requestHeader:"+requestEntity.getHeaders());
        System.out.println("requestBody:"+requestEntity.getBody());
        return "success";
    }
}

@ResponseBody

此注解标识的控制器方法可将方法的返回值直接作为响应报文的响应体响应到浏览器。

/* com.zszy.mvc.controller */
@Controller
public class HttpController {
    @RequestMapping("/testResponse")
    public void testResponse(HttpServletResponse response) throws IOException {
        response.getWriter().print("hello response");
    }
    @RequestMapping("/testResponseBody")
    @ResponseBody // 标识当前控制器方法 => 有 @ResponseBody 则作为响应的数据; 无 @ResponseBody 则作为视图名称
    public String testResponseBody(){
        return "success by zs";
    }
}

SpringMVC 处理 json

  • 导入 jackson 依赖
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.1</version>
</dependency>
  • 在 SpringMVC 的核心配置文件中开启 mvc 的注解驱动
<!-- 开启mvc注解驱动 -->
<!-- 简洁版 -->
<mvc:annotation-driven />
<!-- 开发版 -->
<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>

配置文件开启 mvc 注解驱动后,会在 HandlerAdaptor 中自动装配一个消息转换器 MappingJackson2HttpMessageConverter,此举可以将响应到浏览器的 Java 对象转换为 Json 格式的字符串。

  • 在处理器方法上使用 @ResponseBody 注解进行标识

此时将 Java 对象直接作为控制器方法的返回值返回,就会自动转换为 Json 格式的字符串。

/* com.zszy.mvc.controller */
@Controller
public class HttpController {
    @RequestMapping("/testResponseUser")
    @ResponseBody
    public User testResponseUser(){ return new User(1001,"admin","12345",23,"male"); }
}

SpringMVC 处理 ajax

  • 在前端页面追加超链接以及 vue 与 axios 处理的点击事件
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>index</h1>
    <!-- ...追加上述 index.html -->
    <div id="app">
        <a th:href="@{/testAxios}" @click="testAxios">testAxios</a><br>
    </div>
    <script type="text/javascript" th:src="@{/js/vue.js}"></script>
    <script type="text/javascript" th:src="@{/js/axios.min.js}"></script>
    <script type="text/javascript">
        var vue = new Vue({
            el:"#app",
            methods:{
                testAxios:function (event) {
                    axios({
                        method:"post",
                        url:event.target.href,
                        params:{
                            username:"admin",
                            password:"12345"
                        }
                    }).then(function (response) {
                        alert(response.data);
                    });
                    event.preventDefault();
                }
            }
        });
    </script>
</body>
</html>
/* com.zszy.mvc.controller */
@Controller
public class HttpController {
    @RequestMapping("/testAxios")
    @ResponseBody
    public String testAxios(String username,String password){
        System.out.println(username+","+password);
        return "hello axios";
    }
}

@RestController注解

Spring 4.0 引入 @RestController 注解以简化 RESTful Web 服务的创建。

@RestController 注解具有 @Controller@ResponseBody 联合的效果。

控制器使用 @RestController 注解后其内方法不需要标记 @ResponseBody

控制器类的每个请求处理方法都会自动将返回对象序列化为 HttpResponse。

@Controller
@RequestMapping("books")
public class SimpleBookController {

    @GetMapping("/{id}", produces = "application/json")
    public @ResponseBody Book getBook(@PathVariable int id) {
        return findBookById(id);
    }

    private Book findBookById(int id) {
        // ...
    }
}
@RestController
@RequestMapping("books-rest")
public class SimpleBookRestController {
    
    @GetMapping("/{id}", produces = "application/json")
    public Book getBook(@PathVariable int id) {
        return findBookById(id);
    }

    private Book findBookById(int id) {
        // ...
    }
}

ResponseEntity

ResponseEntity 表示整个 HTTP 响应:状态代码、标头和正文。

ResponseEntity 作为一种通用类型,可以使用作任何类型的响应主体。

// Create a ResponseEntity with a body, headers, and a status code
ResponseEntity(T body, MultiValueMap<String,String> headers, HttpStatus status)

文件上传和下载

文件下载

  • 使用 ResponseEntity 实现下载文件的功能
/* FileUpAndDownController */
@Controller
public class FileUpAndDownController {
    @RequestMapping("/testDown")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
        // 获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
        // 获取服务器中文件的真实路径
        String realPath = servletContext.getRealPath("/static/img/Neon80S.jpg");
        // 创建输入流
        InputStream 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=Neon80S.jpg");
        // 设置响应状态码
        HttpStatus statusCode = HttpStatus.OK;
        //创建ResponseEntity对象
        ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
        // 关闭输入流
        is.close();
        return responseEntity;
    }
}

文件上传

文件上传要求表单请求方式必为 post,并添加属性 enctype="multipart/form-data",SpringMVC 中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息。

  • 添加依赖
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>
  • 在 SpringMVC 的配置文件中添加配置
<!-- 必须通过文件解析器的解析才能将文件转换为MultipartFile对象 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
  • 实现上传文件的功能
/* FileUpAndDownController */
@Controller
public class FileUpAndDownController {
    @RequestMapping("/testUp")
    public String testUp(MultipartFile photo, HttpSession session) throws IOException {
        // photo.getName 是获取表单上传设置的 name 属性值 photo
        // 获取上传的文件的文件名
        String fileName = photo.getOriginalFilename();
        // 处理文件重名问题 => UUID => 32位不重复随机序列
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        fileName = UUID.randomUUID().toString() + suffixName;
        // 获取服务器中photo目录的路径
        ServletContext servletContext = session.getServletContext();
        String photoPath = servletContext.getRealPath("photo");
        File file = new File(photoPath);
        if(!file.exists()){
            file.mkdir();
        }
        String finalPath = photoPath + File.separator + fileName;
        // 实现上传功能
        photo.transferTo(new File(finalPath));
        return "success";
    }
}

拦截器与异常处理器

过滤器 ServletFilter 过滤从浏览器发送的所有请求,作用于 DispatcherServlet 之前。DispatcherServlet 对请求进行处理,根据请求信息与 @RequestMapping 匹配。请求映射所对应的控制器方法 Controllers 就是处理请求的方法。拦截器用于拦截控制器方法前后,需要实现 HandlerInterceptor。

拦截器的三个抽象方法

preHandle => 控制器方法执行前调用,其返回值的类型表示拦截或放行,返回 true 为放行,即调用控制器方法;返回 false 表示拦截,即不调用控制器方法

postHandle => 控制器方法执行之后执行

afterCompletion => 处理完视图和模型数据,渲染视图完毕之后执行

多个拦截器的执行顺序

  • 每个拦截器的 preHandle() 都返回 true

此时多个拦截器的执行顺序和拦截器在配置文件的配置顺序有关,preHandle() 会按照配置的顺序执行,而 postHandle()afterCompletion() 会按照配置的反序执行。

  • 若某个拦截器的 preHandle() 返回 false

返回 false 的拦截器和之前拦截器中 preHandle() 都会执行,postHandle() 都不执行,返回 false 的拦截器之前的拦截器的 afterCompletion() 会执行。

源码中拦截器放在 interceptorList,在 applyPreHandle 中以 i++ 形式执行,在 applyPostHandle 中以 i-- 形式执行。interceptorList 包括一个 Springmvc 内置的拦截器。mappedhandler 是执行链。

拦截器使用

  • 配置文件设置拦截器

将 bean 写入 <mvc:interceptors> 表示这一类型的 bean 是拦截器,这种方式的配置会导致所有的请求被拦截。

ref 引用当前 IOC 容器中某个 bean 的 id,故可将 ref 写入 <mvc:interceptors> 完成相同的效果,注意此方法的使用前提是将拦截器交给 IOC 容器管理。

值得注意的是 <mvc:view-controller path="/" view-name="index" /> 也会被拦截器所拦截。

<?xml version="1.0" encoding="UTF-8"?>
<beans ...
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="...http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    ...
    <!--配置拦截器-->
    <mvc:interceptors>
        <ref bean="firstInterceptor"></ref>
        <ref bean="secondInterceptor"></ref>
        <!-- 通过配置一个bean的方式配置拦截器,那么所有请求都会进行拦截 -->
        <!-- <bean class="com.zszy.mvc.interceptors.FirstInterceptor"></bean> -->
        <!-- 同上默认对所有请求进行拦截 -->
        <!-- <ref bean="firstInterceptor"></ref> -->
        <!-- ref 和 bean 拦截所有路径请求 -->
        <!-- 指定具体的拦截规则 -->
<!--        <mvc:interceptor>-->
<!--            &lt;!&ndash; mapping 设置拦截的路径 => /* 一层目录; /** 所有目录 &ndash;&gt;-->
<!--            <mvc:mapping path="/**"/>-->
<!--            &lt;!&ndash; exclude-mapping 设置不拦截的路径 &ndash;&gt;-->
<!--            <mvc:exclude-mapping path="/"/>-->
<!--            &lt;!&ndash; 指明使用具体的拦截器 &ndash;&gt;-->
<!--            <ref bean="firstInterceptor"></ref>-->
<!--        </mvc:interceptor>-->
    </mvc:interceptors>
    <!-- 配置异常处理 -->
    ...
</beans>
  • 设置拦截器

类似于过滤器 FilterChain 的 doFilter 方法,拦截器也需要放行使得完全访问控制器方法。

@Component // 标识组件的方式将拦截器交给 ioc 容器管理 => 使用 ref 的前提
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");
    }
}
  • 测试代码
/* TestController */
@Controller
public class TestController {
    @RequestMapping("/**/testInterceptor")
    public String testInterceptor(){
        return "success";
    }
}

基于配置的异常处理

处理控制器方法执行中出现异常的顶层接口 HandlerExceptionResolver 实现类有 DefaultHandlerExceptionResolver 和 SimpleMappingExceptionResolver。

  • 自定义的异常处理器 SimpleMappingExceptionResolver 使用方式

若在控制器方法执行中出现指定的异常,那么可返回一个新的 ModelAndView。

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
        	<!-- properties的键表示处理器方法执行过程中出现的异常; properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面 -->
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!-- exceptionAttribute属性设置一个属性名, 将出现的异常信息在请求域中进行共享 -->
    <property name="exceptionAttribute" value="ex"></property>
</bean>
/* ExceptionHandler */
@Controller
public class TestController {
    @RequestMapping("/testExceptionHandler")
    public String testExceptionHandler(){
        System.out.println(1/0);
        return "success";
    }
}
<!-- error.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ExceptionTest</title>
</head>
<body>
出现错误
<p th:text="${ex}"></p>
</body>
</html>

基于注解的异常处理

//@ControllerAdvice将当前类标识为异常处理的组件
@ControllerAdvice
public class ExceptionController {
    // @ExceptionHandler 用于设置所标识方法处理的异常
    @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
    // ex 表示当前请求处理中出现的异常对象
    public String testException(Exception ex, Model model){
        model.addAttribute("ex", ex);
        return "error";
    }
}

注解配置 SpringMVC

使用配置类和注解代替 web.xml 和 SpringMVC 配置文件。Servlet3.0 中容器会在类路径 src、resource 中查找实现 javax.servlet.ServletContainerInitializer 接口的类,若能找到就用其来配置 Servlet 容器,即 Tomcat 服务器。

Spring 提供此接口的实现 SpringServletContainerInitializer,这个类会查找实现 WebApplicationInitializer 的类并将配置的任务交其完成。

Spring3.2 中引入接口 WebApplicationInitializer 的基础实现,当类扩展了此实现并将其部署到 Servlet3.0 容器时,容器会自动检测,并用其配置 Servlet 上下文。

AbstractAnnotationConfigDispatcherServletInitializer

除在服务器启动时自动调用 servlet、filter、listener,WebInit 类也会被自动加载和执行。

  • 初始化 WebInit 类代替 web.xml
/* web 工程初始化类,用来代替 web.xml */
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    /* 指定spring的配置类 */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }
    /* 指定springmvc的配置类 */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }
    /* 指定dispatcherServlet的映射规则,即 URL—pattern */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    /* 注册过滤器 */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceResponseEncoding(true);
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter,hiddenHttpMethodFilter};
    }
}
  • WebConfig 类代替 SpringMVC 的配置文件
/* 代替 springMVC 的配置文件 => 扫描组件、视图解析器、view-controller、default-servlet-handler、mvc 注解驱动、文件上传解析器、异常处理、拦截器 */
@Configuration // 配置类
@ComponentScan("com.zszy.mvc.controller") // 扫描组件
@EnableWebMvc // 开启MVC注解驱动
public class WebConfig implements WebMvcConfigurer {
    // default-servlet-handler
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    // 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        TestInterceptor testInterceptor = new TestInterceptor();
        registry.addInterceptor(testInterceptor).addPathPatterns("/**");
    }

    // view-controller
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/hello").setViewName("hello");
    }

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

    // 异常处理
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty("java.lang.ArithmeticException","error");
        exceptionResolver.setExceptionMappings(properties);
        exceptionResolver.setExceptionAttribute("exception");
        resolvers.add(exceptionResolver);
    }

    // 配置生成模板解析器
    @Bean
    public ITemplateResolver templateResolver() {
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(webApplicationContext.getServletContext());
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        return templateResolver;
    }

    // 生成模板引擎并为模板引擎注入模板解析器
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    // 生成视图解析器并未解析器注入模板引擎
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }
}
  • Spring 配置类、Controller 层以及前端页面和拦截器
@Configuration
public class SpringConfig { }
@Controller
public class TestController {
    @RequestMapping("/")
    public String index(){
        return "index";
    }
}
<!-- index -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
  <h1>index</h1>
</body>
</html>
<!-- hello -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
  <h1>hello by zs</h1>
</body>
</html>
public class TestInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("TestInterceptor => preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

SpringMVC 执行流程

此坑晚点填

Bugs 处理

  • Maven 的 pom.xml 注入依赖飘红

刷新 pom.xml 与 maven,可查看到 IntelliJ 右下角正在下载 Jar 包。

  • HTTP 状态 404 未找到
// 异常报告
消息 The requested resource [/springMVC/hello/testRequestMapping] is not available
描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。

因在配置 web.xml 的前端控制器时,未配置 <url-pattern> 标签内的值。<servlet-mapping> 标签声明与该 servlet 相应的匹配规则,每个 <url-pattern> 标签代表一个匹配规则。当浏览器发起请求到 servlet 容器时,容器先会将请求的路径去除当前应用上下文的路径后,作为 servlet 的映射 url。

  • 在 Maven 模块 src 目录的 main 下创建 webapp 无蓝标

在 project structure 中选择 Facets 栏,为新增的模块增加 web。

// Deployment Descriptors 为项目描述符 => web.xml
/Users/<username>/IdeaProjects/idea-maven-repo/springMVC/springMVC-demo02/src/main/webapp/WEB-INF/web.xml
// web resource directories 为 web 资源目录 => 指明 webapp
/Users/<username>/IdeaProjects/idea-maven-repo/springMVC/springMVC-demo02/src/main/webapp
  • 启动 Tomcat 没有 artifacts

在 project structure 中选择 artifacts 栏,加上一个 web application exploded。

  • 导入项目文件夹并设置为 maven 项目

右键导入模块的 pom.xml 文件,选择 Add as Maven Project。

  • IntelliJ 项目中模块消失,只剩 iml 文件。

Project Structure => Modules => 加号 => import module。

  • 解决获取请求参数的乱码问题

SpringMVC 提供的编码过滤器 CharacterEncodingFilter 可解决获取请求参数的乱码问题,但必须在 web.xml 中进行注册。监听器、过滤器及 servlet 应依次加载。

  • 静态资源被 SpringMVC 框架拦截
No mapping found for HTTP request with URI [static/css/index.css] in DispatcherServlet

Servlet 的 RequestDispatcher 通过名称而不是路径来检索,即 SpringMVC 将接收到的所有请求都看作是一个普通的请求,包括对于静态资源的请求。那么所有对静态资源的请求都会被看作是一个普通的后台控制器请求,导致 @RequestMapping 无法成功匹配。

解决方案是在配置文件增加 <mvc:default-servlet-handler /> 标签。

DefaultServletHttpRequestHandler 会于 Web 容器启动时在上下文进行定义。其会对 DispatcherServlet 的请求进行处理。

若请求存在相应 @requestMapping => 后台对应的处理程序

若请求没有相应的 @requestMapping => 应用服务器默认的 Servlet 处理

  • 自定义拦截器实现 HandlerInterceptor 接口时没有 @override 提示

总所周知,在接口中只能定义方法,而不能对方法进行具体的实现。接口中定义的方法必须在接口的非抽象子类中进行具体的实现。HandlerInterceptor 里 default 方法能在接口内部包含一些默认的方法实现,故 IntelliJ 不出现 @Override 提示。

建议复制 HandlerInterceptor 接口的方法,在重写时将类访问修饰符范围扩大。

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!