SpringMVC学习笔记

  • 2023-10-15 16:19:59
  • 阅读:634次

1、认识SpringMVC

1、什么是MVC


MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分

M:Model,模型层,指工程中的JavaBean,作用是处理数据

JavaBean分为两类:

  • 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等
  • 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。

V:View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据

C:Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器

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

2、什么是SpringMVC

SpringMVC是Spring的一个后续产品,是Spring的一个子项目

SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案

注:三层架构分为表述层(或表示层)、业务逻辑层、数据访问层,表述层表示前台页面和后台servlet

2、HelloSpringMVC


1、快速入门

导入依赖

<dependency>  
	<groupId>org.springframework</groupId>  
	<artifactId>spring-webmvc</artifactId>  
	<version>5.3.21</version>  
</dependency>    
<dependency>        
	<groupId>javax.servlet</groupId>  
	<artifactId>servlet-api</artifactId>  
	<version>2.3</version>  
</dependency>

添加web框架


在web.xml中配置servlet

<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"  
         version="4.0">  
    <servlet>        
	    <servlet-name>dispatcherServlet</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>            
	        <param-name>contextConfigLocation</param-name>  
            <param-value>classpath:SpringMVC.xml</param-value>  
        </init-param>        
        <load-on-startup>1</load-on-startup>  
    </servlet>    
    <servlet-mapping>        
	    <servlet-name>dispatcherServlet</servlet-name>  
        <!--配置 / 则不能匹配jsp静态资源的请求-->  
        <!--配置 /* 匹配所有请求-->  
        <!--此配置覆盖了tomcat中的默认servlet,但是jsp文件有别的servlet匹配-->  
        <url-pattern>/</url-pattern>  
    </servlet-mapping>
</web-app>

关于<url-pattern>为什么要设置成/

tomcat提供的两个Servlet

1、DefaultServlet
DefaultServlet为默认的Servlet,当客户端请求不能匹配其他所有Servlet时,将由DefaultServlet处理。它配置的url-pattern为/。

DefaultServlet主要用于处理静态资源,如HTML、图片、CSS、JS文件等,而且为了提升服务器性能,Tomcat对访问文件进行缓存。按照默认配置,客户端请求路径与资源的物理路径是一致的。

如果我们希望Web应用覆盖Tomcat的DefaultServlet配置,只需将“ / ”添加到自定义Servlet的url-pattern中即可(此时,自定义Servlet将成为Web应用的默认的Servlet)。

2、JspServlet
默认情况下,JspServlet的url-pattern为.jsp和.jspx,因此他负责处理所有JSP文件的请求。

JspServlet主要做了这些事情:

根据JSP文件生成对应Servlet的Java代码(JSP文件生成类的父类为org.apache.jasper.runtime.HttpJspBase——实现了Servlet接口)。
将Java代码编译为Java Class。Tomcat支持Ant和JDT(Eclipse提供的编译器)两种编译JSP类,默认采用JDT。
构造Servlet类实例并且执行请求

DispatcherServlet的配置/和/*的区别

如果把DispatcherServlet的url-pattern配置成/*,那么它会覆盖掉jsp servlet,所有的jsp请求最交给DispatchServlet处理,如果Controller中没有配置相关处理方法那么会无法处理。事实上没有必要越俎代庖的处理.jsp请求,完全可以交给Tomcat容器处理jsp请求,因此DispatchServlet要配置成/。

配置SpringMVC配置文件

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xmlns:context="http://www.springframework.org/schema/context"  
       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.zh.controller"/>  
  
    <!-- <mvc:annotation-driven /> 会自动注册DefaultAnnotationHandlerMapping与AnnotationMethodHandlerAdapter 两个bean,  
    是spring MVC为@Controllers分发请求所必须的。它提供了数据绑定支持,读取json的支持 -->  
    <mvc:annotation-driven/>  
  
    <!--访问静态资源时使用默认的处理器处理,若不配置,则所有请求都当做正常请求处理,会添加视图解析器的前后缀,则会找不到资源-->  
    <mvc:default-servlet-handler/>  
    <!-- 配置jsp视图解析器 -->  
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="jspViewResolver">  
        <property name="prefix" value="/WEB-INF/jsp/"/>  
        <property name="suffix" value=".jsp"/>  
    </bean>  
</beans>

控制器

@Controller  
public class HelloController {  
  
    @RequestMapping("/hello")  
    public String Hello(){  
        return "hello";  
    }  
}

环境搭建成功

总结:
浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面

3、@RequestMapping注解


1、@RequestMapping注解的功能

从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。

SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。

2、@RequestMapping注解的位置

@RequestMapping标识一个类:设置映射请求的请求路径的初始信息

@RequestMapping标识一个方法:设置映射请求请求路径的具体信息

@Controller
@RequestMapping("/test")
public class RequestMappingController {

	//此时请求映射所映射的请求的请求路径为:/test/testRequestMapping
    @RequestMapping("/testRequestMapping")
    public String testRequestMapping(){
        return "success";
    }

}

3、@RequestMapping注解的value属性

@RequestMapping注解的value属性通过请求的请求地址匹配请求映射

@RequestMapping注解的value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求

@RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射

@Controller  
public class Test {  
  
    @RequestMapping(value = {"/test01","/test02"})  
    public String test01(){  
        return "test";  
    }  
}

4、@RequestMapping注解的method属性

@RequestMapping注解的method属性通过请求的请求方式(get或post)匹配请求映射

@RequestMapping注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求

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

@RequestMapping(
        value = {"/testRequestMapping", "/test"},
        method = {RequestMethod.GET, RequestMethod.POST}
)
public String testRequestMapping(){
    return "success";
}

注:

1、对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解

处理get请求的映射-->@GetMapping

处理post请求的映射-->@PostMapping

处理put请求的映射-->@PutMapping

处理delete请求的映射-->@DeleteMapping

2、常用的请求方式有get,post,put,delete

但是目前浏览器只支持get和post,若在form表单提交时,为method设置了其他请求方式的字符串(put或delete),则按照默认的请求方式get处理

若要发送put和delete请求,则需要通过spring提供的过滤器HiddenHttpMethodFilter,在RESTful部分会讲到

5、@RequestMapping注解的params属性(了解)

@RequestMapping注解的params属性通过请求的请求参数匹配请求映射

@RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系

"param":要求请求映射所匹配的请求必须携带param请求参数

"!param":要求请求映射所匹配的请求必须不能携带param请求参数

"param=value":要求请求映射所匹配的请求必须携带param请求参数且param=value

"param!=value":要求请求映射所匹配的请求必须携带param请求参数但是param!=value

注:

若当前请求满足@RequestMapping注解的value和method属性,但是不满足params属性,此时页面回报错400:Parameter conditions "username, password!=123456" not met for actual request parameters: username={admin}, password={123456}

6、@RequestMapping注解的headers属性(了解)

@RequestMapping注解的headers属性通过请求的请求头信息匹配请求映射

@RequestMapping注解的headers属性是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系

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

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

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

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

若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到

7、SpringMVC支持ant风格的路径

?:表示任意的单个字符

*:表示任意的0个或多个字符

**:表示任意的一层或多层目录

注意:在使用**时,只能使用/**/xxx的方式

8、SpringMVC支持路径中的占位符(重点)

原始方式:/deleteUser?id=1

rest方式:/deleteUser/1

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

4、SpringMVC获取请求参数


1、通过ServletAPI获取

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

//1、通过ServletAPI获取  
@RequestMapping("/test01")  
public String test01(HttpServletRequest request){  
    String name = request.getParameter("name");  
    String id = request.getParameter("id");  
  
    System.out.println("=====test01=====");  
    System.out.println(name);  
    System.out.println(id);  
  
    return "test";  
}

2、通过控制器方法的形参获取请求参数

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

//2、通过请求参数名字  
@RequestMapping("/test02")  
public String test02(String name,String id){  
  
    System.out.println("=====test02=====");  
    System.out.println(name);  
    System.out.println(id);  
  
    return "test";  
}

注:

若请求所传输的请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数

若使用字符串数组类型的形参,此参数的数组中包含了每一个数据

若使用字符串类型的形参,此参数的值为每个数据中间使用逗号拼接的结果

作用:获取前端多选框数据

3、@RequestParam

@RequestParam是将请求参数和控制器方法的形参创建映射关系

@RequestParam注解一共有三个属性:

value:指定为形参赋值的请求参数的参数名

required:设置是否必须传输此请求参数,默认值为true

若设置为true时,则当前请求必须传输value所指定的请求参数,若没有传输该请求参数,且没有设置defaultValue属性,则页面报错400:Required String parameter 'xxx' is not present;若设置为false,则当前请求不是必须传输value所指定的请求参数,若没有传输,则注解所标识的形参的值为null

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

//3、当前端参数名和方法形参名字不一样时通过@RequestParam获取  
@RequestMapping("/test03")  
public String test03(@RequestParam("name") String username, String id){  
  
    System.out.println("=====test03=====");  
    System.out.println(username);  
    System.out.println(id);  
  
    return "test";  
}

4、@RequestHeader

@RequestHeader是将请求头信息和控制器方法的形参创建映射关系

@RequestHeader注解一共有三个属性:value、required、defaultValue,用法同@RequestParam

//4、获取请求头  
@RequestMapping("/test04")  
public String test04(@RequestHeader("host") String host){  
  
    System.out.println("=====test04=====");  
    System.out.println(host);  
  
    return "test";  
}

5、@CookieValue

@CookieValue是将cookie数据和控制器方法的形参创建映射关系

@CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam

//5、获取cookie参数  
@RequestMapping("/test05")  
public String test05(@CookieValue("JSESSIONID") String session){  
  
    System.out.println("=====test05=====");  
    System.out.println(session);  
  
    return "test";  
}

6、通过POJO获取请求参数

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

<form action="/SpringMVC_03_Parameter/pojo" 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="submit">  
</form>
public class User {  
    private String username;  
    private String password;  
    private String sex;  
    private String age;  
  
    public User() {  
    }
    ......
//6、获取属性自动封装到类中  
@RequestMapping("/pojo")  
public String test06(User user){  
  
    System.out.println("=====pojo=====");  
    System.out.println(user);  
  
    return "test";  
}

User{username='张三', password='123321', sex='男', age='11'}

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

配置过滤器

<!--配置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>
</filter>  
  
<filter-mapping>  
    <filter-name>CharacterEncodingFilter</filter-name>  
    <!--这里设置成/*--> 
    <url-pattern>/*</url-pattern>  
</filter-mapping>

注:

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

5、域对象共享数据

1、使用ServletAPI向request域对象共享数据

@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
    request.setAttribute("testScope", "hello,servletAPI");
    return "success";
}

2、使用ModelAndView向request域对象共享数据

@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
    /**
     * ModelAndView有Model和View的功能
     * Model主要用于向请求域共享数据
     * View主要用于设置视图,实现页面跳转
     */
    ModelAndView mav = new ModelAndView();
    //向请求域共享数据
    mav.addObject("testScope", "hello,ModelAndView");
    //设置视图,实现页面跳转
    mav.setViewName("success");
    return mav;
}

3、使用Model向request域对象共享数据

@RequestMapping("/testModel")
public String testModel(Model model){
    model.addAttribute("testScope", "hello,Model");
    return "success";
}

4、使用map向request域对象共享数据

@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
    map.put("testScope", "hello,Map");
    return "success";
}

5、使用ModelMap向request域对象共享数据

@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
    modelMap.addAttribute("testScope", "hello,ModelMap");
    return "success";
}

6、Model、ModelMap、Map的关系

Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}

7、向session域共享数据

@RequestMapping("/testSession")
public String testSession(HttpSession session){
    session.setAttribute("testSessionScope", "hello,session");
    return "success";
}

8、向application域共享数据

@RequestMapping("/testApplication")
public String testApplication(HttpSession session){
	ServletContext application = session.getServletContext();
    application.setAttribute("testApplicationScope", "hello,application");
    return "success";
}

9、回顾四大域对象

【1】ServletContext 域--- application
ServletContext代表整个web应用的对象。

生命周期:web应用被tomcat服务器加载时,ServletContext对象产生,生命周期开始。

web应用被移除容器或者tomcat服务器关闭的时候,ServletContext对象销毁,生命周期结束。

作用范围:整个web应用。

主要功能:在整个web应用范围内共享数据。

【2】session 域---session
Session代表整个会话的对象

生命周期:当调用request.getSession()时,Session对象被创建。生命周期开始

调用session.invalidate()方法销毁Session对象

在设定的时间内,Session对象没有被使用,则Session对象被销毁。默认为30分钟

当服务器意外关闭的时候,Session对象被销毁。当服务器正常关闭的时候,Session对象中仍有数据,会序列化到磁盘上形成一个文件,

这个过程称之为钝化。在服务器再次启动的时候,这个文件会被重新读取到服务器中使用,这个过程称之为活化。

作用范围:整个会话范围

主要功能:在会话范围内共享数据

【3】request 域---request
Request代表请求的对象

生命周期:请求链开始,request对象被创建,请求链结束,request对象销毁。

作用范围:整个请求链

主要功能:在请求链内共享数据

【4】pageContext域---pageContext
PageContext代表当前页面的对象

生命周期:在访问jsp页面时,pageContext对象产生,生命周期开始。在结束访问jsp页面时,pageContext对象销毁,生命周期结束。

作用范围:整个jsp页面

主要功能:在整个jsp页面内共享数据

6、HttpMessageConverter

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

HttpMessageConverter提供了两个注解和两个类型:@RequestBody,@ResponseBody,RequestEntity,

ResponseEntity

1、@RequestBody

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

<form action="/SpringMVC_03_Parameter/pojo" 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="submit">  
</form>
public String test06(User user,@RequestBody String body){  
    System.out.println("=====pojo=====");  
  
    System.out.println(body);  
  
    System.out.println(user);

	return null;
}

输出结果:

username=%C3%A5%C2%BC%C2%A0%C3%A4%C2%B8%C2%89&password=111&sex=%C3%A7%C2%94%C2%B7&age=11
User{username='??????', password='111', sex='??·', age='11'}

2、RequestEntity

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

@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity){
    System.out.println("requestHeader:"+requestEntity.getHeaders());
    System.out.println("requestBody:"+requestEntity.getBody());
    return "success";
}

输出结果:
requestHeader:[host:"localhost:8080", connection:"keep-alive", content-length:"27", cache-control:"max-age=0", sec-ch-ua:"" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"", sec-ch-ua-mobile:"?0", upgrade-insecure-requests:"1", origin:"http://localhost:8080", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"]
requestBody:username=admin&password=123

3、@ResponseBody

@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器

@RequestMapping("/pojo")  
@ResponseBody  
public String test06(User user){  
    System.out.println("=====pojo=====");  
  
    System.out.println(user);  
  
    return user.toString();  
}

结果:浏览器页面显示User{username='张三', password='132', sex='ç”·', age='11'}

控制台输出
pojo
User{username='??????', password='132', sex='??·', age='11'}

4、解决乱码问题

乱码问题我认为可以分为两种

1、获取前端参数乱码

解决方法:配置过滤器

<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>  
    <filter-name>CharacterEncodingFilter</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>

过滤所有请求:/*

配置好后,控制台输出:
pojo
User{username='张三', password='123321', sex='男', age='111'}

2、后端返回数据后前端显示乱码

打开前端控制台可以发现响应报文有如下信息:
Content-Type:text/html;charset=ISO-8859-1

这就是导致乱码的原因

为什么返回的数据编码是ISO-8859-1?

在spring处理ResponseBody时涉及到org.springframework.http.converter.StringHttpMessageConverter这个类,该类在默认实现中将defaultCharset设为ISO-8859-1。当@RequestMapping标记的方法未配置produces属性时,将自动使用默认编码;如果配置了produces属性,AbstractHttpMessageConverter中的write方法将不会受supportedMediaTypes影响,而用produce设置的header赋值给contentType。

源码默认为ISO-8859-1

因此,解决方法是:指定给消息转换器的编码为utf-8

解决方法:

1、配置@RequestMapping(value = "/pojo",produces = "text/html;charset=UTF-8")

2、配置 HttpMessageConverter

<mvc:annotation-driven>  
    <mvc:message-converters>  
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">  
            <property name="supportedMediaTypes">  
                <list>                    
	                <value>text/html;charset=UTF-8</value>  
                </list>            
			</property>        
		</bean>    
	</mvc:message-converters>  
</mvc:annotation-driven>

这里值设置了返回一种媒体类型,可以设置多种,SpringMVC会自动寻找合适的消息转换器

关于过滤器为什么无法设置ContentType

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

@ResponseBody

public String getRuleList(HttpServletRequest request,

        HttpServletResponse response) {

    response.addHeader("test", "test");

    return service.getRuleList();

}

通过验证,我们可以看到test项已经被成功添加到response的头部信息

`Content-Length: 2 kilobytes`

`Content-Type:   text/plain;charset=ISO-8859-1`

`Server: Apache-Coyote/1.1`

`test: test`
@RequestMapping(value = "/rulelist", method = RequestMethod.GET)

@ResponseBody

public String getRuleList(HttpServletRequest request,

        HttpServletResponse response) {

    response.addHeader("Content-Type", "application/json;charset=UTF-8");

    return service.getRuleList();

}

没有设置成功

`Content-Length: 2 kilobytes`

`Content-Type:   text/plain;charset=ISO-8859-1`

`Server: Apache-Coyote/1.1`

下图清晰地向大家展示了Spring MVC处理HTTP请求的流程,(图片来自网络)

具体流程如下:

1、DispatcherServlet接收到Request请求

2、HandlerMapping选择一个合适的Handler处理Request请求

3-4、 选择合适的HandlerAdapter,调用用户编写的Controller处理业务逻辑。(HandlerAdapter主要是帮助Spring MVC支持多种类型的Controller)

5、Controller将返回结果放置到Model中并且返回view名称给Handler Adapter

6、DispatcherServlet选择合适的ViewResolver来生成View对象

7-8、 View对象利用Model中的数据进行渲染并返回数据

从上面的流程图我们可以看到,content-type header是单独被处理的,media-type被单独的逻辑进行处理,因此直接在ServletResponse中设置content-type header并不能正常生效。

5、SpringMVC处理json

@ResponseBody处理json的步骤:

a>导入jackson的依赖

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

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

<mvc:annotation-driven />

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

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

@RequestMapping("/pojo")  
@ResponseBody  
public User test06(User user){  
    System.out.println("=====pojo=====");  
  
    System.out.println(user);  
  
    return user;  
}

浏览器的页面中展示的结果:

{"username":"张三","password":"123321","sex":"男","age":"11"}

6、@RestController注解

@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解

7、ResponseEntity

ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文

7、SpringMVC的视图

SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户

SpringMVC视图的种类很多,默认有转发视图和重定向视图

当工程引入jstl的依赖,转发视图会自动转换为JstlView

若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView

1、ThymeleafView

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

@RequestMapping("/testHello")
public String testHello(){
    return "hello";
}

2、转发视图

SpringMVC中默认的转发视图是InternalResourceView

SpringMVC中创建转发视图的情况:

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

例如"forward:/","forward:/employee"

@RequestMapping("/testForward")
public String testForward(){
    return "forward:/testHello";
}

实例

@RequestMapping("/test07")  
public String test07(){  
    System.out.println("进入Test07");  
    return "forward:/test08";  
}  
@RequestMapping("/test08")  
public ModelAndView test08(User user){  
    ModelAndView modelAndView = new ModelAndView();  
  
    System.out.println("进入Test08");  
  
    modelAndView.addObject("user",user.toString());  
  
    modelAndView.setViewName("user");  
  
    return modelAndView;  
  
}

3、重定向视图

SpringMVC中默认的重定向视图是RedirectView

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

例如"redirect:/","redirect:/employee"

@RequestMapping("/testRedirect")
public String testRedirect(){
    return "redirect:/testHello";
}

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

4、视图控制器view-controller

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

<!--
	path:设置处理的请求地址
	view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/testView" view-name="success"></mvc:view-controller>

注:

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

<mvc:annotation-driven />

8、RESTful

1、RESTful简介

REST:Representational State Transfer,表现层资源状态转移。

a>资源

资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。

b>资源的表述

资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式。

c>状态转移

状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的。

2、RESTful的实现

具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。

它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。

REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。

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

3、HiddenHttpMethodFilter

由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢?

SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求

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

a>当前请求的请求方式必须为post

b>当前请求必须传输请求参数_method

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

在web.xml中注册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中提供了两个过滤器:CharacterEncodingFilter和HiddenHttpMethodFilter

在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter

原因:

  • 在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的

  • request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作

  • 而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作:

  • String paramValue = request.getParameter(this.methodParam);

4、RESTful案例

准备工作
配置

<?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:mvc="http://www.springframework.org/schema/mvc"  
       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/mvc       https://www.springframework.org/schema/mvc/spring-mvc.xsd       http://www.springframework.org/schema/context       https://www.springframework.org/schema/context/spring-context.xsd">  
  
  
    <context:component-scan base-package="com.zh"/>  
  
    <mvc:default-servlet-handler/>  
  
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="jspViewResolver">  
        <property name="prefix" value="/WEB-INF/jsp/"/>  
        <property name="suffix" value=".jsp"/>  
    </bean>  
    <mvc:annotation-driven>  
        <mvc:message-converters>  
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">  
                <property name="supportedMediaTypes">  
                    <list>                        
	                    <value>application/json;charset=UTF-8</value>  
                        <value>text/html;charset=UTF-8</value>  
                    </list>                
				</property>            
			</bean>        
		</mvc:message-converters>  
    </mvc:annotation-driven>  
  
    <mvc:view-controller path="/" view-name="index"/>  
  
    <mvc:view-controller path="/UserList" view-name="userlist"/>  
    <mvc:view-controller path="/addUser" view-name="addUser"/>  
  
</beans>

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>        
	    <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>  
    <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>  
  
    <servlet>        
	    <servlet-name>dispatcherServlet</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>            
	        <param-name>contextConfigLocation</param-name>  
            <param-value>classpath:SpringMVC.xml</param-value>  
        </init-param>    
	</servlet>    
	<servlet-mapping>        
		<servlet-name>dispatcherServlet</servlet-name>  
        <url-pattern>/</url-pattern>  
    </servlet-mapping>  
</web-app>

User类

package com.zh.pojo;  
  
import java.util.Objects;  
  
public class User {  
    private int id;  
    private String name;  
    private int age;  
    //省略set和get....
}

DAO类

package com.zh.userDAO;  
  
import com.zh.pojo.User;  
  
import java.util.ArrayList;  
import java.util.List;  
  
public class UserDAO {  
  
    private List<User> userList = new ArrayList<>();  
  
    private int idNum = 4;  
  
    public UserDAO() {  
        userList.add(new User(1,"张三",12));  
        userList.add(new User(2,"李四",14));  
        userList.add(new User(3,"王五",16));  
        userList.add(new User(4,"赵六",17));  
    }  
  
  
    //查询所有用户  
    public List<User> getUserList(){  
        return userList;  
    }  
  
    //根据用户id查询用户  
    public User getUserById(int id){  
        for (User user : userList) {  
            if (user.getId() == id){  
                return user;  
            }  
        }  
        return null;  
    }  
  
    //添加用户  
    public boolean addUser(User user){  
        idNum++;  
        user.setId(idNum);  
        return userList.add(user);  
    }  
  
  
    //根据id删除用户  
    public boolean delUser(int id){  
        for (User user : userList) {  
            if (user.getId() == id){  
                return userList.remove(user);  
            }  
        }  
        return false;  
    }  
  
    //根据id修改用户  
    public boolean updateUser(int id,User newUser){  
        for (User user : userList) {  
            if (user.getId() == id){  
                user.setAge(newUser.getAge());  
                user.setName(newUser.getName());  
                return true;            }  
        }  
        return false;  
    }  
}

Controller类

package com.zh.controller;  
  
import com.fasterxml.jackson.core.JsonProcessingException;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import com.zh.pojo.User;  
import com.zh.userDAO.UserDAO;  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.Model;  
import org.springframework.web.bind.annotation.PathVariable;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
import org.springframework.web.bind.annotation.ResponseBody;  
  
/**  
 * RESRFul风格实例  
 */  
  
  
@Controller  
public class Controller01 {  
  
    private UserDAO userDAO = new UserDAO();  
  
    /**  
     * 使用RESTFul模拟操作用户信息  
     * /user    get    查询所有用户信息  
     * /user/1  get    查询指定用户信息  
     * /user    post    增加用户信息  
     * /user/1  delete 删除指定用户信息  
     * /user/1  put    修改指定用户信息  
     */  
  
    //查询所有用户,返回JSON数组  
    @RequestMapping(value = "/user",method = RequestMethod.GET)  
    @ResponseBody  
    public String getUser() throws JsonProcessingException {  
  
        ObjectMapper objectMapper = new ObjectMapper();  
        String userListString = objectMapper.writeValueAsString(userDAO.getUserList());  
  
        return userListString;  
    }  
  
    //查询一个用户,并将用户数据绑定到user.jsp页面上并进行跳转  
    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)  
    public String getUserById(@PathVariable("id") int id, Model model){  
  
        User user = userDAO.getUserById(id);  
        model.addAttribute("user",user);  
  
        return "user";  
    }  
  
    //增加一个用户,然后重定向到列表页面  
    @RequestMapping(value = "/user",method = RequestMethod.POST)  
    public String addUser(String username,int age){  
  
        userDAO.addUser(new User(username,age));  
        return "redirect:/UserList";  
    }  
  
    //删除一个用户  
    @RequestMapping(value = "/user",method = RequestMethod.DELETE)  
    public String delUser(int id){  
        userDAO.delUser(id);  
        return "redirect:/UserList";  
    }  
  
    //修改用户信息  
    @RequestMapping(value = "/user",method = RequestMethod.PUT)  
    @ResponseBody  
    public void updUser(int id,String username,int age){  
        userDAO.updateUser(id,new User(username,age));  
    }  
}

JSP页面
UserList

<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<html>  
<head>  
    <title>UserList</title>  
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>  
    <style type="text/css">  
        table{  
            margin-top: 200px;  
        }  
        td{  
            text-align: center;  
        }  
        a{  
            text-decoration: none;  
        }  
    </style>  
</head>  
<body>  
    <table border="1" cellspacing="0" align="center" width="25%" id="table">  
  
    </table>    <p align="center">  
        <input type="button" id="btn1" value="刷新">&nbsp;&nbsp;&nbsp;&nbsp;  
        <input type="button" id="btn2" value="增加">  
    </p>  
</body>  
<script>  
    function  getPath(){  
        let pathName = document.location.pathname;  
        let index = pathName.substr(1).indexOf("/");  
        let result = pathName.substr(0,index+1);  
        return result;  
    }  
    function aDelClick(){  
        var as = document.getElementsByClassName('del');  
        for (let i = 0; i < as.length; i++) {  
            as[i].onclick = function (){  
                let tr = this.parentNode.parentNode;  
                let id = tr.children[0].innerHTML;  
                let name = tr.children[1].innerHTML;  
                var flag = confirm("确认删除"+ name + "吗?");  
                if(flag){  
                    //删除tr对应用户的信息  
                    $.ajax({  
                        type: 'post',  
                        dateType: 'json',  
                        url:getPath()+"/user",  
                        data: {  
                            _method:'delete',  
                            id:id  
                        },  
                        success:function (){  
  
                        }  
                    })  
                    tr.parentNode.removeChild(tr);  
                }  
            }  
        }  
    }  
    function aUpdClick(){  
        var as = document.getElementsByClassName('upd');  
        for (let i = 0; i < as.length; i++) {  
            as[i].onclick = function (){  
                let tr = this.parentNode.parentNode;  
                let id = tr.children[0].innerHTML;  
                window.location = getPath()+'/user'+'/'+id;  
            }  
        }  
    }  
    function getUserList(){  
        var table = $("table");  
        $.ajax({  
            type:"get",  
            dateType:"json",  
            url:getPath()+"/user",  
            data:{},  
            success:function (msg){  
                table.text('');  
                table.append('<tr><th colspan="4">用户列表</th></tr><tr><td>id</td><td>姓名</td> <td>年龄</td><td>操作</td> </tr>')  
                for (let i = 0; i <msg.length; i++) {  
                    let tr = '<tr><td>'+msg[i].id+'</td><td>'+msg[i].name+'</td><td>'+msg[i].age+'</td><td><a href="#" class="upd">修改</a>&nbsp;&nbsp;&nbsp;<a href="#" class="del">删除</a></tr>';  
                    table.append(tr);  
                }  
                aDelClick();  
                aUpdClick();  
            }  
        })  
    }  
    $("#btn1").click(function (){  
        getUserList();  
    })  
    $("#btn2").click(function (){  
        window.location = getPath()+'/addUser';  
    })  
    $(function (){  
        getUserList();  
    })  
</script>  
</html>

user.jsp

<%@ page import="com.zh.pojo.User" %>  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<html>  
<head>  
    <title>Title</title>  
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>  
</head>  
<style type="text/css">  
    h1{  
        text-align: center;  
    }  
    #btn{  
        margin: 0 auto;  
        margin-top: 10px;  
        margin-left: 100px;  
    }  
    #out{  
        margin: 0 auto;  
        margin-left: 650px;  
    }  
</style>  
<body>  
<div>  
    <h1>修改用户</h1>  
    <div id="out">  
        <form action="/RESTFul/user" method="post">  
            <%  
                User user = (User) request.getAttribute("user");  
            %>  
            姓名:<input type="text" name="username" id="username" value="<%=user.getName()%>"><br>  
            年龄:<input type="text" name="age" id="age" value="<%=user.getAge()%>"><br>  
            <div>                <button type="button" id="btn1">修改</button>  
                <button type="button" id="btn2">返回</button>  
            </div>        </form>    </div></div>  
</body>  
<script>  
    function  getPath(){  
        let pathName = document.location.pathname;  
        let index = pathName.substr(1).indexOf("/");  
        let result = pathName.substr(0,index+1);  
        return result;  
    }  
    $('#btn1').click(function (){  
        var id = <%=user.getId()%>;  
        var username = $('#username').val();  
        var age = $('#age').val();  
        $.ajax({  
            type:'post',  
            dataType:'text',  
            url:getPath()+'/user',  
            data:{  
                _method:'put',  
                id:id,  
                username:username,  
                age:age  
            },  
            success:function (){  
               window.location = getPath()+'/UserList';  
            }  
        })  
    })  
    $('#btn2').click(function (){  
        window.location = getPath()+'/UserList'  
    })  
</script>  
</html>

adduser.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<html>  
<head>  
    <title>Title</title>  
</head>  
<style type="text/css">  
    h1{  
        text-align: center;  
    }  
    #submit{  
        margin: 0 auto;  
        margin-top: 10px;  
        margin-left: 100px;  
    }  
    #out{  
        margin: 0 auto;  
        margin-left: 650px;  
    }  
</style>  
<body>  
<div>  
    <h1>增加用户</h1>  
    <div id="out">  
        <form action="/RESTFul/user" method="post">  
            姓名:<input type="text" name="username"><br>  
            年龄:<input type="text" name="age"><br>  
            <div>                <input type="submit" align="center" id="submit">  
            </div>        </form>    </div></div>  
</body>  
</html>

index

<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<html>  
<head>  
    <title>Title</title>  
</head>  
<body>  
    <h1>首页</h1>  
    <h4><a href="/RESTFul/UserList">用户列表</a></h4>  
</body>  
</html>

5、功能说明

查询所有用户

控制器映射的url是/user,请求方式是get,返回dao类中的集合的json字符串,前端页面一开始就发送一次Ajax请求,收到数据之后执行回调方法,根据JSON数组的长度动态的添加进表格(拼接标签),并为每个操作的a标签添加一个点击事件(点击修改之后,js代码获取点击时这一用户的id,然后重定向到此url,例如/user/1,点击删除之后,获取到点击时这一用户的id,发送Ajax请求到服务器,请求删除用户,然后重定向到列表页面刷新)

查询一个用户

url是/user/{id},get请求,符合的url将会执行这个方法,控制器拿到id之后,在dao层中获取到此用户的信息,然后把用户信息添加到请求域中,然后返回视图,前端中可以获取到这个数据

增加用户

点击增加用户之后,会跳转到增加用户的界面,url是/addUser,点击修改之后,发送Ajax请求,url是/user,请求方式:post,控制器接收到信息之后会操作数据,然后返回操作状态,前端根据返回的结果提示信息和进行跳转回用户列表

删除用户

点击删除按钮之后,会发送一个Ajax请求,请求参数包含_method和id,请求url: /user,_methon值为delet,控制器收到数据执行删除用户操作,删除完成前端进行跳转会用户列表

修改用户

点击修改用户之后,会重定向到要修改的用户的界面,例如/user/1,控制器已经将参数携带进请求中,可以进行回显,点击修改之后,提交用户信息,前端控制重定向回列表页面

至此,完成了RESTful风格的增删改查

url 操作 请求方式
/user 查询所有用户 get
/user/1 查询一个用户 get
/user 增加用户 post
/user 删除用户 delete
/user 修改用户 put

9、SpringMVC拦截器

请求---->过滤器--->前端控制器--->控制器

SpringMVC中的拦截器用于拦截控制器方法的执行

SpringMVC中的拦截器需要实现HandlerInterceptor

SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置:

package net.biancheng.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class TestInterceptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("afterCompletion方法在控制器的处理请求方法执行完成后执行,即视图渲染结束之后执行");

    }

    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle方法在控制器的处理请求方法调用之后,解析视图之前执行");
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle方法在控制器的处理请求方法调用之前执行");
        return false;
    }
}

上述拦截器的定义中实现了 HandlerInterceptor 接口,并实现了接口中的 3 个方法,说明如下。

  • preHandle( ):该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。
  • postHandle( ):该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。
  • afterCompletion( ):该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。

1、拦截器的配置

让自定义的拦截器生效需要在 Spring MVC 的配置文件中进行配置,配置示例代码如下:

<!-- 配置拦截器 -->
<mvc:interceptors>
    <!-- 配置一个全局拦截器,拦截所有请求 -->
    <bean class="net.biancheng.interceptor.TestInterceptor" /> 
    <mvc:interceptor>
        <!-- 配置拦截器作用的路径 -->
        <mvc:mapping path="/**" />
        <!-- 配置不需要拦截作用的路径 -->
        <mvc:exclude-mapping path="" />
        <!-- 定义<mvc:interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
        <bean class="net.biancheng.interceptor.Interceptor1" />
    </mvc:interceptor>
    <mvc:interceptor>
        <!-- 配置拦截器作用的路径 -->
        <mvc:mapping path="/gotoTest" />
        <!-- 定义在<mvc:interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
        <bean class="net.biancheng.interceptor.Interceptor2" />
    </mvc:interceptor>
</mvc:interceptors>

在上述示例代码中,元素说明如下。

  • <mvc:interceptors>:该元素用于配置一组拦截器。
  • <bean>:该元素是 mvc:interceptors 的子元素,用于定义全局拦截器,即拦截所有的请求。
  • mvc:interceptor:该元素用于定义指定路径的拦截器。
  • mvc:mapping:该元素是 mvc:interceptor 的子元素,用于配置拦截器作用的路径,该路径在其属性 path 中定义。path 的属性值为/**时,表示拦截所有路径,值为/gotoTest时,表示拦截所有以/gotoTest结尾的路径。如果在请求路径中包含不需要拦截的内容,可以通过 mvc:exclude-mapping 子元素进行配置。

需要注意的是,mvc:interceptor 元素的子元素必须按照 mvc:mapping.../mvc:exclude-mapping.../、<bean.../> 的顺序配置。

对于路径:

/**的意思是所有文件夹及里面的子文件夹
/*是所有文件夹,不含子文件夹
/是web项目的根目录

2、示例

<mvc:interceptors>  
    <mvc:interceptor>  
        <mvc:mapping path="/**"/>  
        <mvc:exclude-mapping path="/user"/>  
        <mvc:exclude-mapping path="/UserList"/>  
        <mvc:exclude-mapping path="/"/>  
        <bean class="com.zh.interceptor.MyInterceptor"/>  
    </mvc:interceptor>  
</mvc:interceptors>

配置了这个拦截器之后,对访问UserList页面和获取User数据的请求以及首页都不拦截

public class MyInterceptor implements HandlerInterceptor {  
    private Date date;  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        System.out.println("控制器执行前");  
        System.out.println(simpleDateFormat.format(new Date())+request.getRequestURI());  
        return true;    }  
}

当有多个拦截器时,顺序和配置的顺序有关

<mvc:interceptors>  
	<bean class="com.zh.interceptor.Interceptor1"/>  
	<bean class="com.zh.interceptor.Interceptor2"/>  
</mvc:interceptors>
preHandle:拦截器--->1
preHandle:拦截器--->2
postHandle:拦截器--->2
postHandle:拦截器--->1
afterCompletion:拦截器--->2
afterCompletion:拦截器--->1

当第一个拦截器拦截时

preHandle:拦截器--->1

当后面的拦截器拦截请求时

preHandle:拦截器--->1
preHandle:拦截器--->2
afterCompletion:拦截器--->1

10、异常处理器

SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver,该接口的类图如下

SpringMVC中,处理异常通常有4种方式

@ExceptionHandler 注解

使用 @ExceptionHandler 注解实现异常处理
使用该注解有一个不好的地方就是:进行异常处理的方法必须与出错的方法在同一个Controller里面。使用如下:

@Controller
public class GlobalController {

   /**
     * 用于处理异常的
     * @return
     */
    @ExceptionHandler({MyException.class})
    public String exception(MyException e) {
        System.out.println(e.getMessage());
        e.printStackTrace();
        return "exception";
    }

    @RequestMapping("test")
    public void test() {
        throw new MyException("出错了!");
    }
}

@ControllerAdvice+@ExceptionHandler

使用 @ControllerAdvice+@ExceptionHandler处理全局异常
@ExceptionHandler只能和发生异常的控制器在一起才能使用,也就是只能处理该控制器内的方法所抛出的异常,SpringMVC 3.2 新特性@ControllerAdvice是控制器增强,也就是专门处理异常的控制器,也就可以实现全局的异常处理,使用方法:

@ControllerAdvice  
public class ExceptionController {  
    @ExceptionHandler(Exception.class)  
    public String ExceptionHandle(Exception ex, Model model){  
        model.addAttribute("ex",ex);  
        return "error";  
    }  
}

异常处理接口 HandlerExceptionResolver

实现 Spring 的异常处理接口 HandlerExceptionResolver,自定义自己的异常处理器。

public class MyExceptionHandler implements HandlerExceptionResolver {  
    @Override  
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {  
  
        if (ex instanceof ArithmeticException){  
            ModelAndView arithmeticException = new ModelAndView();  
            arithmeticException.setViewName("ArithmeticException");  
            return arithmeticException;  
        }else if(ex instanceof NullPointerException){  
            ModelAndView nullPointerException = new ModelAndView();  
            nullPointerException.setViewName("NullPointerException");  
            return nullPointerException;  
        }else {  
            ModelAndView error = new ModelAndView();  
            error.setViewName("error");  
            return error;  
        }  
    }  
}

然后到SpringMVC配置文件中注册这个bean

<bean class="com.zh.exception.MyExceptionHandler"/>

可以看到源码中的异常处理器中已经有了我们自己定义的异常处理器

然后遍历这些异常处理器,异常处理器处理完成之后会返回视图,如果视图为空,则证明这个异常处理器无法处理这个异常,继续遍历,找到可以处理这个异常的异常处理器之后,返回结果,进行视图解析

SimpleMappingExceptionResolver

使用 Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver。
全局异常处理可使用 SimpleMappingExceptionResolver 来实现。它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常。

使用方法,直接在配置文件注册bean就行

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">  
    <!--异常没有被特殊处理时,默认的视图名字-->  
    <property name="defaultErrorView" value="error"/>  
    <!--异常信息的参数名字,可以根据这个取出异常信息-->  
    <property name="exceptionAttribute" value="ex"/>  
    <!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常页名作为值 -->  
    <property name="exceptionMappings">  
        <props>            
	        <prop key="java.lang.ArithmeticException">ArithmeticException</prop>
	        <prop key="java.lang.NullPointerException">NullPointerException</prop>  
        </props>    
	</property>
</bean>

可以看到,注册的这个异常处理器也被加载进来了

11、关于文件的上传和下载


1、文件上传

文件上传需要用到org.springframework.web.multipart.commons.CommonsMultipartResolver这个类,这个类极大简便了我们对文件操作

文件上传是项目开发中最常见的功能。为了能上传文件,必须将表单的method设置为POST,并将enctype设置为multipart/form-data。一旦设置了enctype为multipart/form-data,浏览器即会采用二进制流的方式来处理表单数据,只有在这样的情况下,浏览器才会把用户选择的文件以二进制数据发送给服务器。

Spring MVC 为文件上传提供了直接的支持,这种支持是用即插即用的 MultipartResolver 实现的。Spring MVC 使用 Apache Commons FileUpload技术实现了一个 MultipartResolver 实现类:CommonsMultipartResolver 。因此,SpringMVC的文件上传还需要依赖Apache Commons FileUpload的组件。

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

若没有这导入依赖,则报错

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'multipartResolver':
Failed to introspect bean class [org.springframework.web.multipart.commons.CommonsMultipartResolver] for lookup method metadata:
could not find class that it depends on; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/fileupload/FileItemFactory

SpringMVC上下文中默认没有装配 MultipartResolver,因此默认情况下其不能处理文件上传工作。如果想使用Spring的文件上传功能,则需要在上下文中配置 MultipartResolver。

MultipartResolver 用于处理文件上传,当收到请求时 DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isMultipart() 方法判断请求中是否包含文件。如果请求数据中包含文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller

<!--配置文件上传相关-->
2 <bean id="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
3     <!-- 设定文件上传的最大值-->
4     <property name="maxUploadSize" value="10485760"></property>
5     <!-- 设定文件上传时写入内存的最大值,如果小于这个参数不会生成临时文件,默认为10240 -->
6     <property name="maxInMemorySize" value="4096"></property>
7     <!-- 设定默认编码 -->
8     <property name="defaultEncoding" value="UTF-8"></property>
9 </bean>

这个类还有很多参数设置,根据需求设置

上传文件的页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<html>  
<head>  
    <title>Title</title>  
</head>  
<body>  
<form action="/RESTFul/upload" method="post" enctype="multipart/form-data">  
    文件说明:<input type="text" name="wjsm" /><br/>  
    附件:<input type="file" name="file" /><br/>  
    <input type="submit" value="提交"/>  
</form>  
  
</body>  
</html>

编写Controller以及处理方法:测试ok

MultipartFile 封装了请求数据中的文件,此时这个文件存储在内存中或临时的磁盘文件中,需要将其转存到一个合适的位置,因为请求结束后临时存储将被清空。在 MultipartFile 接口中有如下方法:

  • String getName(); // 获取参数的名称
  • String getOriginalFilename(); // 获取文件的原名称
  • String getContentType(); // 文件内容的类型
  • boolean isEmpty(); // 文件是否为空
  • long getSize(); // 文件大小
  • byte[] getBytes(); // 将文件内容以字节数组的形式返回
  • InputStream getInputStream(); // 将文件内容以输入流的形式返回
  • void transferTo(File dest); // 将文件内容传输到指定文件中
@RequestMapping("/upload")  
public String fileUpload(@RequestParam("file")MultipartFile file, HttpServletRequest request) throws IOException {  
  
    //获取存储该文件的文件夹在服务器上的真实路径,会自动拼接上  
    String realPath = request.getSession().getServletContext().getRealPath("/resource/userdata");  
  
    //获取文件原来的名字  
    String fileName = file.getOriginalFilename();  
  
    //创建一个File,此file会自动拼接路劲,就是这个文件下载之后存储到服务器上的路径  
    File desFile = new File(realPath,fileName);  
  
    //这个文件如果为空,则创建一个  
    if (!desFile.exists()){  
        desFile.mkdirs();  
    }  
    //开始往这个文件里面写数据(复制)  
    file.transferTo(desFile);  
  
    return "success";  
}

2、文件下载

SpringMVC提供了一个 ResponseEntity 类型,使用它可以很方便地定义返回的 HttpHeaders 和HttpStatus 。

@RequestMapping("/filedownload/{fileName}")  
public ResponseEntity<byte[]> fileDownload(@PathVariable String fileName, HttpSession session) throws IOException {  
      
    //获取文件的真实路径  
    String path = session.getServletContext().getRealPath("/resource/userdata");  
    File file = new File(path,fileName);  
  
    //创建一个对该文件的输入流  
    InputStream inputStream = new FileInputStream(file);  
  
    //创建一个和文件大小一样的byte数组  
    byte[] bytes = new byte[inputStream.available()];  
  
    //把数据读取到该数组  
    inputStream.read(bytes);  
  
    //创建响应报文的响应头  
    HttpHeaders httpHeaders = new HttpHeaders();  
  
    //设置浏览器处理该响应的方式,attachment是附件形式,后面跟客户端下载该文件的名字  
    //如果客户端下载文件会出现名字乱码,则应转换一下编码  
    //httpHeaders.add("Content-Disposition", "attachment;filename="+ URLEncoder.encode(fileName,"utf-8"));  
    httpHeaders.add("Content-Disposition", "attachment;filename="+ fileName);  

	//application/octet-stream: 二进制流数据(最常见的文件下载)  
	httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
	
    //设置响应状态  
    HttpStatus httpStatus = HttpStatus.OK;  
  
    return new ResponseEntity<byte[]>(bytes,httpHeaders,httpStatus);  
}

12、注解配置SpringMVC

完全使用配置类和注解代替web工程的配置文件web.xml和SpringMVC、Spirng的xml配置文件的功能。
在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

编写WebConfig

  • WebConfig.class用来代替web工程配置文件web.xml,
  • 继承了AbstractAnnotationConfigDispatcherServletInitializer.
package com.zh.config;  
  
import com.zh.filter.MyFilter;  
import org.springframework.web.filter.CharacterEncodingFilter;  
import org.springframework.web.filter.HiddenHttpMethodFilter;  
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;  
  
import javax.servlet.Filter;  
  
public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {  
    /**  
     * 指定Spring的配置类  
     * @return  
     */  
    @Override  
    protected Class<?>[] getRootConfigClasses() {  
        return new Class[0];  
    }  
  
  
    /**  
     * 指定SpringMVC的配置类  
     * @return  
     */  
    @Override  
    protected Class<?>[] getServletConfigClasses() {  
        return new Class[]{SpringMVCConfig.class};  
    }  
  
    /**  
     * 指定DispatcherServlet的映射规则,即url-pattern  
     * @return  
     */  
    @Override  
    protected String[] getServletMappings() {  
        return new String[]{"/"};  
    }  
  
  
    /**  
     *这里配置的过滤器是和DispatcherServlet一样,  
     * 也就是所有经过DispatcherServlet的都会经过这些过滤器  
     * 如果需要增加自己定义的过滤器,则可以使用注解@WebFilter  
     * @return  
     */  
  
    @Override  
    protected Filter[] getServletFilters() {  
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();  
        characterEncodingFilter.setEncoding("utf-8");   
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();  
        MyFilter myFilter = new MyFilter();  
        return new Filter[]{characterEncodingFilter,hiddenHttpMethodFilter,myFilter};  
    }  
}

以上配置过滤器的过滤路径是和DispatcherServlet的映射规则一样,如果还需要添加自己的过滤去,则需要注解配置路径
注:@WebFilter 注解是 servlet3.0 提供的注解,另外
关于导入serv依赖的时候出现的问题
servlet.jar 是servlet 3.0 版本之前的地址
javax.servlet-api.jar 是servlet 3.0 版本之后的地址

此配置类对应了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>        
	    <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>  
    <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>  
  
    <servlet>        
	    <servlet-name>dispatcherServlet</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>            
	        <param-name>contextConfigLocation</param-name>  
            <param-value>classpath:SpringMVC.xml</param-value>  
        </init-param>    
	</servlet>    
	<servlet-mapping>        
		<servlet-name>dispatcherServlet</servlet-name>  
        <url-pattern>/</url-pattern>  
    </servlet-mapping>  
  
</web-app>

配置了一个servlet(dispatcherServlet)和两个过滤器characterEncodingFilter,HiddenHttpMethodFilter

编写SpringMVCConfig

需要配置如下内容
包扫描
注解驱动
静态资源处理
视图解析器
拦截器
异常处理器
上传文件解析器
消息转换器
view-controller

package com.zh.config;  
  
import com.zh.interceptor.MyInterceptor;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.ComponentScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.http.MediaType;  
import org.springframework.http.converter.HttpMessageConverter;  
import org.springframework.http.converter.StringHttpMessageConverter;  
import org.springframework.web.multipart.commons.CommonsMultipartResolver;  
import org.springframework.web.servlet.HandlerExceptionResolver;  
import org.springframework.web.servlet.ViewResolver;  
import org.springframework.web.servlet.config.annotation.*;  
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;  
import org.springframework.web.servlet.view.InternalResourceViewResolver;  
  
import java.nio.charset.Charset;  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Properties;  
  
@Configuration  
//包扫描  
@ComponentScan("com.zh")  
//开启注解驱动  
@EnableWebMvc  
public class SpringMVCConfig implements WebMvcConfigurer {  
  
    //静态资源处理  
    @Override  
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {  
        configurer.enable();  
    }  
  
    //视图解析器  
    @Bean  
    public ViewResolver viewResolver(){  
        InternalResourceViewResolver resourceViewResolver = new InternalResourceViewResolver();  
        resourceViewResolver.setPrefix("/WEB-INF/jsp/");  
        resourceViewResolver.setSuffix(".jsp");  
        return resourceViewResolver;  
    }  
  
    //配置拦截器  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");  
    }  
  
    //配置异常映射  
    @Override  
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {  
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();  
        Properties prop = new Properties();  
        prop.setProperty("java.lang.ArithmeticException", "error");  
        //设置异常映射  
        exceptionResolver.setExceptionMappings(prop);  
        //设置共享异常信息的键  
        exceptionResolver.setExceptionAttribute("ex");  
  
        resolvers.add(exceptionResolver);  
    }  
  
    //文件上传解析器,需要其他依赖commons-fileupload  
    @Bean  
    public CommonsMultipartResolver multipartResolver(){  
        return new CommonsMultipartResolver();  
    }  
  
    //配置消息转换器  
    @Override  
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {  
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();  
  
        //设置应用类型和字符编码  
        List<MediaType> mediaTypeList = new ArrayList<>();  
        mediaTypeList.add(new MediaType(MediaType.APPLICATION_JSON,Charset.forName("utf-8")));  
        mediaTypeList.add(new MediaType(MediaType.TEXT_HTML,Charset.forName("utf-8")));  
  
        //设置次转换器支持的类型  
        stringHttpMessageConverter.setSupportedMediaTypes(mediaTypeList);  
        converters.add(stringHttpMessageConverter);  
    }  
  
    //相当于<mvc:view-controller path="/" view-name="index"/>  
    @Override  
    public void addViewControllers(ViewControllerRegistry registry) {  
        registry.addViewController("/").setViewName("index");  
    }  
}

13、SpringMVC执行流程

1、SpringMVC常用组件

  • DispatcherServlet:前端控制器,不需要工程师开发,由框架提供

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

  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

作用:根据请求的url、method等信息查找Handler,即控制器方法

  • Handler:处理器,需要工程师开发

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

  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

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

  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView

  • View:视图

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

2、DispatcherServlet调用组件处理请求

a>processRequest()

FrameworkServlet重写HttpServlet中的service()和doXxx(),这些方法中调用了processRequest(request, response)

所在类:org.springframework.web.servlet.FrameworkServlet

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
		// 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}
b>doService()

所在类:org.springframework.web.servlet.DispatcherServlet

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    logRequest(request);

    // Keep a snapshot of the request attributes in case of an include,
    // to be able to restore the original attributes after the include.
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap<>();
        Enumeration<?> attrNames = request.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String) attrNames.nextElement();
            if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }
    }

    // Make framework objects available to handlers and view objects.
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    RequestPath requestPath = null;
    if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
        requestPath = ServletRequestPathUtils.parseAndCache(request);
    }

    try {
        // 处理请求和响应
        doDispatch(request, response);
    }
    finally {
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Restore the original attribute snapshot, in case of an include.
            if (attributesSnapshot != null) {
                restoreAttributesAfterInclude(request, attributesSnapshot);
            }
        }
        if (requestPath != null) {
            ServletRequestPathUtils.clearParsedRequestPath(request);
        }
    }
}
c>doDispatch()

所在类:org.springframework.web.servlet.DispatcherServlet

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            /*
            	mappedHandler:调用链
                包含handler、interceptorList、interceptorIndex
            	handler:浏览器发送的请求所匹配的控制器方法
            	interceptorList:处理控制器方法的所有拦截器集合
            	interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
            */
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
           	// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
			
            // 调用拦截器的preHandle()
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            // 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
            // 调用拦截器的postHandle()
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 后续处理:处理模型数据和渲染视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                               new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
d>processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                   @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        // 处理模型数据和渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        // 调用拦截器的afterCompletion()
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

3、SpringMVC的执行流程

此图自己总结的:

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

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

  • 不存在

    i. 再判断是否配置了mvc:default-servlet-handler

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

    iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误

  • 存在则执行下面的流程

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

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

  3. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】

  4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

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

    • 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等

    • 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

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

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

  6. 此时将开始执行拦截器的postHandle(...)方法【逆向】。

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

  8. 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。

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

热门内容