【SpringMVC】SpringMVC学习-2022详细学习笔记

SpringMVC

这是我学习尚硅谷2022版ssm框架做的笔记
欢迎访问我的个人博客

相关文章🌟

1 SpringMVC简介

1.1 MVC

是一种架构思想,不是框架或者设计模式

1.2 SpringMVC

是Spring为表述层开发提供的一整套完备的解决方案,封装的就是servlet。

三层架构,是指的表述层、业务逻辑层、数据访问层,表述层表示前台页面和后台servlet

1.3 SpringMVC的特点

  • Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
  • 基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理
  • 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
  • 代码清新简洁,大幅度提升开发效率
  • 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
  • 性能卓著,尤其适合现代大型、超大型互联网项目要求

2 入门案例💃

2.1 创建Maven工程

  1. 添加web模块

这里是以另外一个模块为例

image-20220728191705230

  1. 打包方式设置为war
  2. 引入依赖
 <dependencies>
        <!-- SpringMVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!--thymeleaf需要slf4j日志接口,这里引入他的一个实现logback-->
        <!-- 日志 -->
        <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>

注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。

image-20220728185146508

2.2 在web.xml中配置DispatcherServlet

2.2.1 配置匹配请求路径

匹配请求时:写/,匹配浏览器向服务器发送的所有请求,但是不包括以.jsp结尾的请求

/*,可以匹配.jsp的请求,但是DispatcherServlet不能处理这种请求,需要Tomcat中专门的的处理.jsp请求的servlet

<servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <!--这里写/,匹配浏览器向服务器发送的所有请求,但是不包括以.jsp结尾的请求
        而/*,可以匹配.jsp的请求
        而DispatcherServlet不能处理这种请求,Tomcat中有专门的的处理.jsp的请求-->
    <url-pattern>/</url-pattern>
</servlet-mapping>
2.2.2 配置初始化时机

由于DispatcherServlet初始化时间很久,所以非常有必要提前初始化

<load-on-startup>1</load-on-startup>
2.2.3 配置文件的路径

这里使用类路径

类路径:是指java和resources下的路径

<init-param>
    <param-name>contextConfigLocation</param-name>
    <!--类路径:是指java和resources下的路径-->
    <param-value>classpath:springmvc.xml</param-value>
</init-param>

总文件:

<?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的前端控制器DispatcherServlet

        (以后不会吧配置文件放在这个位置下,都是放在resource下)
        SpringMVC的配置文件默认的位置和名称:
        位置:WEB-INF下
        名称: <servlet-name>-servlet.xml,当前配置下的配置文件为SpringMVC-servlet.xml
-->

    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!--类路径:是指java和resources下的路径-->
            <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的请求
            而DispatcherServlet不能处理这种请求,Tomcat中有专门的的处理.jsp的请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

2.3 创建请求控制器

由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的请求有不同的处理过程,因此需要创建处理具体请求的类,即请求控制器

SpringMVC的控制器由一个POJO担任,因此需要通过@Controller注解将其标识为一个控制层组件,交给Spring的IOC容器管理。

控制器方法匹配请求

斜线就代表着绝对路径,在服务器中和在浏览器中解析出来的不一样

在服务器中斜线被解析之后就是localhost:8080+上下文路径:http://localhost:8080/SpringMVC/

在浏览器中,不会有上下文路径,就是http://localhost:8080/

@Controller
public class HelloController {

    //  在服务器中斜线代表着绝对路径
    // 在服务器中斜线被解析之后就是localhost:8080+上下文路径:http://localhost:8080/SpringMVC/
    @RequestMapping("/")
    public String portal(){
        //将逻辑视图返回,进行视图转发,加上thymeleaf中配置的前缀和后缀
        return "index";
    }

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

2.4 创建SpringMVC的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启扫描组件-->
    <context:component-scan base-package="com.zylai.controller"/>

    <!-- 配置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">
                        <!--
                            物理视图路径:/WEB-INF/templates/index.html
                            逻辑视图:index
                            去掉前缀和后缀
                            物理视图:前缀+逻辑视图+后缀
                            -->
                        <!-- 视图前缀 -->
                        <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>

2.5 请求访问页面和跳转页面

2.5.1 创建的位置

/WEB-INF/templates/创建index.html

注意:浏览器无法直接访问WEB-INF下的资源,只能通过服务器访问

2.5.2 访问页面
  1. 在前端控制器中具体的方法返回逻辑视图的名称,比如返回index
  2. thymeleaf视图解析器会解析逻辑视图,加上前缀/WEB-INF/templates/和后缀.html
  3. 最后根据总的完整路径/WEB-INF/templates/index.html进行转发
  4. 转发到index.html页面,thymeleaf先回渲染这个页面中的thymeleaf表达式
2.5.3 页面中的路径解析

浏览器会解析为绝对路径,localhost:8080/hello

<!--使用thymeleaf解析,拼接上上下文路径,最后路径为localhost:8080/SpringMVC/hello-->
<a th:href="@{/hello}">测试SpringMVC</a>

<!--这里斜线是绝对路径,是浏览器解析的,会被解析为 localhost:8080/hello-->
<a href="/hello">测试绝对路径</a>

总结

流程:

  1. 浏览器发送请求
  2. 若请求地址符合前端控制器DispatcherServlet中url-pattern,该请求就会被DispatcherServlet处理
  3. 前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器controller
  4. 将请求地址和控制器中方法上的@RequestMapping注解的value属性值进行匹配,匹配成功就会执行对应的方法
  5. 方法会返回一个字符串类型的视图名称,比如返回index,是逻辑地址。thymeleaf视图解析器会解析逻辑视图,加上前缀/WEB-INF/templates/和后缀.html
  6. 根据总的完整路径/WEB-INF/templates/index.html进行转发,转发到index.html页面,thymeleaf先回渲染这个页面中的thymeleaf表达式

3 RequestMapping注解🚀

3.1 标识的位置

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

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

即,类上的路径加上方法上的路径

3.2 value属性⭐️

作用:通过请求的请求路径匹配请求

value属性是数组类型,即当前浏览器所发送请求的路径匹配value属性中的任何一个值,则当前请求就会被这个控制器方法进行处理

    @RequestMapping(value = {"/hello","/abc"})
    public String hello(){
        return "success";
    }
<a th:href="@{/abc}">测试@RequestMapping注解的value属性</a>

3.3 method属性⭐️

作用:通过请求的请求方式匹配请求

method属性是RequestMethod类型的数组,即若当前浏览器所发送请求的请求方式匹配method属性数组中的任何一种请求方式,则当前请求就会被注解所标识的方法进行处理

若浏览器所发送的请求的路径和value属性匹配,但是请求方式不匹配,则页面报错:HTTP状态 405 - 方法不允许

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

如果不指定method属性,那就可以匹配任意类型的请求

3.4 params属性(了解)

* 作用:通过请求的请求参数匹配请求,即浏览器发送的请求的请求参数必须满足params属性的设置
* 可以使用四种表达式:
* "param":表示当前所匹配请求的请求参数中必须携带param参数,不管他的值是啥
* "!param":表示当前所匹配请求的请求参数中一定不能携带param参数
* "param=value":表示当前所匹配请求的请求参数中必须携带param参数且值必须为value
* "param!=value":表示当前所匹配请求的请求参数中可以不携带param参数,若携带,值一定不等于value
*

3.5 headers属性(了解)

* 作用:通过请求的请求头匹配请求,即浏览器发送的请求的请求头必须满足headers属性的设置
* "header":要求请求映射所匹配的请求必须携带header请求头信息
* "!header":要求请求映射所匹配的请求必须不能携带header请求头信息
* "header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value
* "header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value
* 注意:若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,
* 此时页面显示404错误,即资源未找到

3.6 SpringMVC支持ant风格的路径

在@RequestMapping注解的value属性中设置一些特殊字符
?:任意的单个字符(不包括?和/)
*:表示0个或多个任意字符(不包括?和/)
**:表示任意层数的任意目录,注意使用方式只能将** 写在双斜线中,前后不能有任何的其他字符

3.7 使用路径中的占位符(重点)🌟

对比:

  • 传统:/deleteUser?id=1
  • rest: /deleteUser/1

使用:

需要在@RequestMapping注解的value属性中所设置的路径中,使用{xxx}的方式表示路径中的数据

再通过@PathVariable注解,将占位符的值和控制器方法的形参进行绑定

<a th:href="@{/test/rest/2374234/admin}">测试value属性中的占位符</a>
@RequestMapping("/test/rest/{id}/{username}")
public String testRest(@PathVariable(value = "id") Integer id,@PathVariable(value = "username")String username){

    System.out.println("id:"+id+",username:"+username);
    return "success";
}
11:01:50.534 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to com.zylai.controller.TestRequestMappingController#testRest(Integer, String)
id:2374234,username:admin

4 SpringMVC获取请求参数📟

4.1 通过servletAPI获取

很少用

只需要在控制其方法的形参位置设置HttpServletRequest类型的形参,就可以在控制器方法中使用request对象获取请求参数。

这里DispatcherServlet会根据request参数去匹配,会把request对象传过来(具体实现可以参考JavaWeb中的myssm实现)

    @RequestMapping("/servletapi")
//    直接在参数中写HttpServletRequest即可,DispatcherServlet会根据参数去匹配
    public String getParamByServletAPI(HttpServletRequest request){

        String username = request.getParameter("username");
        String password = request.getParameter("password");
        System.out.println("username:"+username+",password:"+password);
        return "success";
    }

4.2 通过控制器方法的形参获取

只需要在控制器方法的形参位置设置好形参,其名字和请求参数的名字一致即可。

表单:

<form th:action="@{/param/params}" method="post">
    用户名:<input type="text" name="username"><br/>
    密码:<input type="password" name="password"><br/>
    <input type="submit" value="登录"><br/>
</form>

控制器方法:

@RequestMapping("/params")
public String getParams(String username,String password){
 System.out.println("username:"+username+",password:"+password);
    return "success";
}

4.3 @RequestParam注解🌊

将请求参数和控制器方法的形参绑定,三个属性为:

  • value:设置和形参绑定的请求参数的名字
  • required:设置是否必须传输value设置的请求参数,默认值为true,表示必须传输,否则报错400
  • defaultValue:当未传输value对应的请求参数时,为形参设置默认值,此时和required的值无关

比如,这里把username改为userName,和控制器方法的形参名字不一致

<form th:action="@{/param/params}" method="post">
    用户名:<input type="text" name="userName"><br/>
    密码:<input type="password" name="password"><br/>
    <input type="submit" value="登录"><br/>
</form>

这时只需要在控制器方法的形参上加上注解即可。

实际中,一般只用到value这一个属性就行,这里为了演示

@RequestMapping("/params")
public String getParams( @RequestParam(value = "userName",required = false,defaultValue = "hello") String username, String password){
 System.out.println("username:"+username+",password:"+password);
    return "success";
}

4.4 @RequestHeader注解

参考@RequestParam注解,这里是获取请求头

4.5 @CookieValue注解

参考@RequestParam注解,这里是根据名称获取cookie

@RequestMapping("/params")
public String getParams(
        @RequestParam(value = "userName",required = false,defaultValue = "hello") String username,
        String password,
        @RequestHeader("referer") String referer,
        @CookieValue("JSESSIONID") String jsessionId){
    System.out.println("username:"+username+",password:"+password);
    System.out.println("referer:"+referer+"\ncookie-jsessionId:"+jsessionId);
    return "success";
}

4.6 通过控制器方法的实体类类型的形参获取✌️

需要保证实体类中的属性的属性名和请求参数的名字一致,才可以通过实体类类型的形参获取请求参数。

实体类User:

public class User {
    private  Integer id;

    private String username;

    private String password;
}

表单:

<form th:action="@{/param/pojo}" method="post">
    用户名:<input type="text" name="userName"><br/>
    密码:<input type="password" name="password"><br/>
    <input type="submit" value="登录"><br/>
</form>

控制器方法:

@RequestMapping("/pojo")
public String getParamByPojo(User user){
    System.out.println(user);
    return "success";
}

4.7 设置编码 🏮

web.xml中设置

设置两个属性encodingforceEncoding

注意:在SpringMVC中,编码过滤器一定配置到其他过滤器之前

<!--配置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>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
源码解析

这是倒着的关系。CharacterEncodingFilter中设置编码的方法是doFilterInternal。CharacterEncodingFilter的父类OncePerRequestFilter重写了doFilter方法,在doFilter方法中,调用了设置编码的方法doFilterInternal

image-20220729154853195

调用这个构造器

public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
   this(encoding, forceEncoding, forceEncoding);
}

this用三个参数的构造器,给三个成员变量赋值

public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
   Assert.hasLength(encoding, "Encoding must not be empty");
   this.encoding = encoding;
   this.forceRequestEncoding = forceRequestEncoding;
   this.forceResponseEncoding = forceResponseEncoding;
}

在这个方法中,设置request和response的参数

@Override
protected void doFilterInternal(
      HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {

   String encoding = getEncoding();
   if (encoding != null) {
      if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
         request.setCharacterEncoding(encoding);
      }
      if (isForceResponseEncoding()) {
         response.setCharacterEncoding(encoding);
      }
   }
   filterChain.doFilter(request, response);
}

5 域对象共享数据🥘

SpringMVC往域对象中共享数据,最终都是通过ModelAndView实现

5.1 通过ModelAndView向请求域共享数据

  • 用其Model功能向请求域中共享数据
  • 使用View功能设置逻辑视图,但是控制器方法一定要将ModelAndView作为方法的返回值
@RequestMapping("/test/mav")
public ModelAndView testMAV(){
    /**
     * ModelAndView包含Model和View的功能
     * Model:向请求域中共享数据
     * View:设置逻辑视图实现页面跳转
     */
    ModelAndView mav = new ModelAndView();
    //向请求域中设置数据
    mav.addObject("testRequestScope","hello,ModelAndView");
    //设置视图名称
    mav.setViewName("success");
    return mav;
}

5.2 通过Model向请求域共享数据

注意:5.2 5.3 5.4 这三个选择一个用就可以,一般选择Model

将Model作为方法的形参,之后设置域共享数据即可

@RequestMapping("/test/model")
public String testModel(Model model){
    System.out.println(model.getClass().getName());
    //org.springframework.validation.support.BindingAwareModelMap
    model.addAttribute("testRequestScope","hello,Model");
    return "success";
}

5.3 通过ModelMap向请求域共享数据

@RequestMapping("/test/modelMap")
public String testModel(ModelMap modelMap){
    System.out.println(modelMap.getClass().getName());
    //org.springframework.validation.support.BindingAwareModelMap
    modelMap.addAttribute("testRequestScope","hello,ModelMap");
    return "success";
}

5.4 通过Map向请求域共享数据

@RequestMapping("/test/map")
public String testMap(Map<String,Object> map){
    System.out.println(map.getClass().getName());
    //org.springframework.validation.support.BindingAwareModelMap
    map.put("testRequestScope","hello,Map");
    return "success";
}

5.5 Model和ModelMap和Map的关系

其实在底层中,这些类型的形参最终都是通过BindingAwareModelMap创建的

几个类的关系:

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

image-20220729170924654

最终都是返回一个ModelAndView实例

image-20220729174045137

5.6 共享session域和application域中的数据

使用servletAPI即可

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

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

6 SpringMVC视图🌅

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

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

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

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

6.1 Thymeleaf视图

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

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

image-20220729200023623

6.2 转发视图

转发和重定向:

  1. 转发浏览器发送一次请求,在服务器中处理一次,重定向浏览器发送两次请求。
  2. 转发由于只发送一次请求,地址栏地址不变。重定向改变
  3. 转发由于只发送一次request请求,可以使用request共享域中的数据,而重定向不可

SpringMVC中默认的转发视图是InternalResourceView

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

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

例如"forward:/",“forward:/employee”。

@RequestMapping("/testForward")
public String testForward(){
    return "forward:/testHello";//跳转到路径为/testHello的控制器方法中,之后再执行这个方法
}

image-20220729200138481

6.3 重定向视图

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

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

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

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

image-20220729200147432

6.4 视图控制器

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

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

7 RESTful😪

7.1 RESTful简介

将服务器看做由很多资源组成,一切皆资源。对于同一个资源,要求其访问路径是一样的,不论是增删改查,路径都是一样。

但是如何区别增删改查呢?使用请求方式修改。

下面是比较详细的介绍

image-20220730111356174

7.2 RESTful的实现

具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE用来删除资源。

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

以操作用户对象为例:

image-20220730111553310

@Controller
public class TestRestController {

//    @RequestMapping(value = "/user",method = RequestMethod.GET)
    @GetMapping("/user")
    public String getAllUser(){
        System.out.println("查询所有的用户信息-->/user-->get");
        return "success";
    }

//    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    @GetMapping("/user/{id}")
    public String getUserById(@PathVariable("id")Integer id){
        System.out.println("根据id查询用户信息-->/user/"+id+"-->get");
        return "success";
    }


//    @RequestMapping(value = "/user",method = RequestMethod.POST)
    @PostMapping("/user")
    public String insertUser(){
        System.out.println("添加用户信息-->/user-->post");
        return "success";
    }

//    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    @PutMapping("/user")
    public String updateUser(){
        System.out.println("修改用户信息-->/user-->put");
        return "success";
    }

//    @RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
    @DeleteMapping("/user/{id}")
    public String updateUser(@PathVariable("id") Integer id){
        System.out.println("删除用户信息-->/user/"+id+"-->delete");
        return "success";
    }
}

7.3 处理PUT和DELETE请求

浏览器目前只能发送get和post请求

若要发送put和delete请求,需要在web.xml中配置一个过滤器HiddenHttpMethodFilter,配置了过滤器之后,发送的请求要满足两个条件,才能将请求方式转换为put和delete

1、当前请求的请求方式必须为post

2、当前请求必须传输请求参数_method,_method的值才是最终的请求方式

源码分析

HiddenHttpMethodFilter:

请求方式必须是post,获取参数_method的值

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {

   HttpServletRequest requestToUse = request;

    //如果当前请求是post且没有异常就进入
   if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
       //得到_method参数的值,然后再根据值指定的方法创建一个对应方法的request
      String paramValue = request.getParameter(this.methodParam);
       //判断是否有值
      if (StringUtils.hasLength(paramValue)) {
         String method = paramValue.toUpperCase(Locale.ENGLISH);
         if (ALLOWED_METHODS.contains(method)) {
            requestToUse = new HttpMethodRequestWrapper(request, method);
         }
      }
   }

   filterChain.doFilter(requestToUse, response);
}


//这个内部类,传入request和method,重写了getMethod方法,之后获取method都是PUT或者DELETE
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {

   private final String method;

   public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
      super(request);
      this.method = method;
   }

   @Override
   public String getMethod() {
      return this.method;
   }
}

8 RESTful案例😑

这一部分就是实现一个增删改查,使用RESTful风格实现

8.1 功能

  • 查询所有的员工信息 -->/employee–>get
  • 跳转到添加页面 -->/to/add–>get
  • 新增员工信息 -->/employee–>post
  • 跳转到修改页面 -->/employee/1–>get
  • 修改员工信息 -->/employee–>put
  • 删除员工信息 -->/employee/1–>delete

8.2 处理静态资源

由css文件引入

在员工列表的页面引入css文件

<link rel="stylesheet" th:href="@{/static/css/index_work.css}">

DispatcherServlet会拦截除了jsp之外的所有请求,当请求静态资源时,该请求也会被DispatcherServlet处理,然而他处理不了,需要交给Tomcat处理。

DispatcherServlet和Tomcat的DefaultServlet

当前工程的web.xml配置的前端控制器DispatcherServlet的url-pattern是/

而Tomcat的web.xml配置的默认的控制器DefaultServlet的url-pattern也是/

此时,浏览器发送的请求优先会被DispatcherServlet进行处理,但是DispatcherServlet无法处理静态资源。因此需要把静态资源的处理交给默认的servlet,进行如下配置:

若配置了<mvc:default-servlet-handler/>,此时浏览器发送的所有请求都会被DefaultServlet处理,DispatcherServlet无法进行处理

因此还需要开启mvc的注解驱动<mvc:annotation-driven />,这样浏览器发送的请求会先被DispatcherServlet处理,无法处理再交给DefaultServlet处理

如果Tomcat中的web.xml的配置和工程中的web.xml配置冲突,会以当前工程的配置为准。

8.3 功能实现

这一部分没啥的,正常的crud

控制器:

@Controller
public class EmployeeController {

    @Autowired
    private EmployeeDao employeeDao;

    @GetMapping("/employee")
    public String getAllEmployee(Model model){
        //获取所有的员工信息
        Collection<Employee> employeeList = employeeDao.getAll();
        //将所有的员工信息在请求域中共享
        model.addAttribute("employeeList",employeeList);
        //跳转到列表页面
        return "employee_list";
    }

    @PostMapping("/employee")
    public String addEmployee(Employee employee){
        //保存员工信息
        employeeDao.save(employee);
        //访问列表功能,调用查询数据
        //重定向
        return "redirect:/employee";
    }

    @GetMapping("/employee/{id}")
    public String toUpdate(@PathVariable("id") Integer id,Model model){
        //根据id查询信息
        Employee employee = employeeDao.get(id);
        //将员工信息共享到请求域中
        model.addAttribute("employee",employee);
        //跳转
        return "employee_update";
    }

    @PutMapping("/employee")
    public String updateEmployee(Employee employee){
        //更新信息
        employeeDao.save(employee);
        //重定向
        return "redirect:/employee";
    }

    @DeleteMapping("/employee/{id}")
    public String deleteEmployeeById(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/employee";
    }

}

前端页面,就以员工列表页为例

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>employee list</title>
    <link rel="stylesheet" th:href="@{/static/css/index_work.css}">
</head>
<body>
<div id="app">
    <table>
        <tr>
            <th colspan="5">employee list</th>
        </tr>
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>options(<a th:href="@{/to/add}">add</a> )</th>
        </tr>
        <tr th:each="employee : ${employeeList}">
            <td th:text="${employee.id}"></td>
            <td th:text="${employee.lastName}"></td>
            <td th:text="${employee.email}"></td>
            <td th:text="${employee.gender}"></td>
            <td>
                <a @click="deleteEmployee()" th:href="@{|/employee/${employee.id}|}">delete</a>
                <a th:href="@{|/employee/${employee.id}|}">update</a>
            </td>
        </tr>
    </table>
    <form method="post">
        <input type="hidden" name="_method" value="delete"/>
    </form>

</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script type="text/javascript">
    var vue = new Vue({
        el:"#app",
        methods:{
            deleteEmployee(){
                //获取表单
                var form = document.getElementsByTagName("form")[0]
                //将超链接的href属性赋值给form表单的action属性
                form.acton = event.target.href;
                //表单提交
                form.submit();
                //阻止超链接的默认行为
                event.preventDefault();
            }
        }
    })
</script>
</body>
</html>

9 SpringMVC处理ajax请求🌟

更详细的介绍见https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture/chapter12/

9.1 ajax介绍🤣

服务端渲染与ajax渲染

服务端渲染

image-20220730212558630

ajax渲染(局部更新)

image-20220730212626998

前后端分离

彻底舍弃服务器端渲染,数据全部通过Ajax方式以JSON格式来传递。

异步和同步

Ajax本身就是Asynchronous JavaScript And XML的缩写,直译为:异步的JavaScript和XML。在实际应用中Ajax指的是:不刷新浏览器窗口不做页面跳转局部更新页面内容的技术。

image-20220730213049049

image-20220730213101052

axios

发送请求的方式之一:(一般不使用这个,这里用这个详细介绍一下axios)

参数用params方式,参数拼接到地址栏中。参数用data方式,参数放在请求体中。

axios({
               url:"",//请求路径
               method:"",//请求方式
               //请求参数,以name=value&name=value的方式,
               //不管使用的请求方式是get或者post,请求参数都会被拼接到请求地址后
               // 此种方式可以通过request.getParameter()获取
               params:{},
               //请求参数,以json格式发送,
               // 请求参数会被保存到请求报文的请求体传输到服务器,因此不能使用get,get没有请求体
               // 需要获取请求体,之后把请求体中的json数据进行处理,会使用一些处理json的jar包处理
               data:{}
           }).then(res=>{
               console.log(res.data)
           })

发送请求的方式二:

axios.post(
    //url,要加上上下文路径
    "/SpringMVC/test/ajax?id=1001",
    //json类型的数据,会被封装在请求体中
    {username:"admin",password:"123456"}
).then(res=>{//请求成功的响应
    //服务器返回的结果封装到data属性中
    console.log(res.data)
})

9.2 @RequestBody注解

该注解将请求体中的内容和形参进行绑定。由于axios发送请求把请求参数放到了请求体中,所以需要这个注解来获取请求体中的内容。

testAjax(){
    axios.post(
        //url,要加上上下文路径
        "/SpringMVC/test/ajax?id=1001",
        //json类型的数据,会被封装在请求体中
        {username:"admin",password:"123456"}
    ).then(res=>{//请求成功的响应
        //服务器返回的结果封装到data属性中
        console.log(res.data)
    })
},
@RequestMapping("/test/ajax")
public void testAjax(Integer id, @RequestBody String requestBody, HttpServletResponse response) throws IOException {
    System.out.println("id:"+id);
    System.out.println("requestBody:"+requestBody);
    response.getWriter().write("hello.axios");
}

控制台打印:

id:1001
requestBody:{"username":"admin","password":"123456"}

9.3 @RequestBody注解将json格式的数据转为java对象🍰

由于axios的请求体中是json格式,我们需要进行转换

步骤:

1、导入jackson的依赖
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.1</version>
</dependency>
2、在SpringMVC配置文件中设置开启mvc的注解驱动
<mvc:annotation-driven />
3、在控制器方法的形参前设置@RequestBody注解
@RequestMapping("/test/requestbody/json")
public void testRequestBody(@RequestBody User user, HttpServletResponse response) throws IOException {
    System.out.println(user);
    response.getWriter().write("hello,requestBody");
}

9.4 @ResponseBody注解

就是把所标识的方法的返回值作为响应报文的响应体响应到浏览器。

@RequestMapping("/test/responsebody")
//这个注解就是把方法的返回值作为请求的返回体返回给浏览器
@ResponseBody
public String testResponseBody(){
    return "success";
}

这里直接将内容显示到浏览器中

image-20220730215003861

9.5 @ResponseBody注解响应给浏览器json格式数据

java --> json

如何将java对象响应过去?如果直接如果直接response.getWriter().write,加入java对象,是返回的内存地址或者返回了一个toString方法的返回值,再者,如果将java类型的对象返回了,那么浏览器也不能处理java类型的数据。

一般都是奖java转换为json值返回

步骤:(和上面的差不多)
  1. 导入jackson的依赖
  2. 在SpringMVC配置文件中设置开启mvc的注解驱动
  3. 将需要转换为json字符串的Java对象作为控制器方法的返回值,使用@ResponseBody注解标识控制器方法就可以将java对象直接转化为json字符串,并响应到浏览器

非常方便,省略了将返回的java对象转换为json的过程,由框架完成

//非常方便,省略了将返回的java对象转换为json的过程,由框架完成
@RequestMapping("/test/responsebody/json")
@ResponseBody
public User testResponseBodyJson(){
    User user = new User(1001,"admin","23232",20,"男");
    return user;
}

image-20220730215806682

转换关系

常用的java对象转化为json的结果:

  • 实体类–>json对象
  • map–>json对象
  • List–>json数组

image-20220730184555834

9.6 @RestController注解

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

10 文件上传和下载

10.1 文件下载

ResponseEntity:可以作为控制器方法的返回值,表示响应到浏览器的完整的响应报文

如果不设置返回,返回值为void,那么会默认将请求地址作为逻辑视图

下面这个,一般当成一个模板用就可以了

    @RequestMapping("/test/down")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws
            IOException {
//获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
//获取服务器中文件的真实路径,其实就是直接把这个路径拼接到项目的后面
        String realPath = servletContext.getRealPath("img");
        //E:\code_workspace\study_SSM\spring_mvc04_ajax\target\spring_mvc04_ajax-1.0-SNAPSHOT\
        realPath = realPath + File.separator+"avatar.jpg";
//创建输入流
        InputStream is = new FileInputStream(realPath);
//创建字节数组,is.available()获取输入流所对应的文件的字节数。
//        对于流,可以设置响应的读取策略,可以适量多次读取
        byte[] bytes = new byte[is.available()];
//将流读到字节数组中
        is.read(bytes);
//创建HttpHeaders对象设置响应头信息
        MultiValueMap<String, String> headers = new HttpHeaders();
//设置要下载方式以及下载文件的名字,attachment:以附件的方式下载
        headers.add("Content-Disposition", "attachment;filename=avatar.jpg");
//设置响应状态码
        HttpStatus statusCode = HttpStatus.OK;
//创建ResponseEntity对象
        ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers,
                statusCode);
//关闭输入流
        is.close();
        return responseEntity;
    }

10.2 文件上传

要求:

  1. form表单的请求方式必须是post
  2. form表单必须设置属性enctype=“multipart/form-data”,保证以二进制方式上传

步骤:

  1. 添加依赖

    <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
    
  2. 配置bean

    注意设置id,id名固定

    <!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象
        这里的id必须是multipartResolver,因为这里是根据id获取bean的-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--<property name="defaultEncoding" value="UTF-8"/>-->
    </bean>
    
  3. 控制器方法

    注意点:使用MultipartFile类型参数接收,注意把他当成一个普通参数就可,需要匹配请求的参数

    @RequestMapping("/test/up")
    //注意:这里的MultipartFile不是由框架自动填充(就像request和session那样赋值),
    // 而是像普通参数一样从请求参数中获取
    public String testUp(@RequestParam("photo") MultipartFile photo, HttpSession session) throws IOException {
        //获取上传的文件的文件名
        String filename = photo.getOriginalFilename();
        //处理文件重名问题
        //获取上传的文件的后缀名
        String fileType = filename.substring(filename.lastIndexOf("."));
        filename = UUID.randomUUID().toString() + fileType;
        System.out.println(filename);
        //获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
    
        //获取当前工程下photo目录的真实路径
        String photoPath = servletContext.getRealPath("photo");
    
        //创建photoPath所对应的File对象
        File file = new File(photoPath);
        //判断file对应的目录是否存在
        if(!file.exists()){
            file.mkdir();
        }
    
        String finalPath = photoPath + File.separator + filename;
        //文件上传
        photo.transferTo(new File(finalPath));
        return "success";
    }
    

10.3 文件上传的重命名问题

使用UUID命名,UUID加上文件后缀名即可。

//处理文件重名问题
//获取上传的文件的后缀名
String fileType = filename.substring(filename.lastIndexOf("."));
filename = UUID.randomUUID().toString() + fileType;
System.out.println(filename);

下面这些知识点用的不多,了解即可

11 拦截器

请求经过过滤器,过滤器放行之后DispatcherServlet处理请求,而拦截器用于拦截控制器方法的执行

11.1 拦截器的配置

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

SpringMVC中的拦截器需要实现HandlerInterceptor接口

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

<mvc:interceptor>
	<bean class="com.atguigu.interceptor.FirstInterceptor">	</bean>
	<ref bean="firstInterceptor"></ref>
</mvc:interceptor>

<!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 -->


<mvc:interceptor>
    <mvc:mapping path="/**"/>
    <mvc:exclude-mapping path="/testRequestEntity"/>
    <ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!-- 
	以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求(/**匹配所有的请求),通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
-->

总结:bean和ref的配置方式都是对所有请求进行拦截,而mvc:mapping可以设置拦截的请求

11.2 拦截器的三个抽象方法

SpringMVC中的拦截器有三个抽象方法:

preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法

postHandle:控制器方法执行之后执行postHandle()

afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()

拦截器的类要实现HandlerInterceptor接口,并重写其中的三个默认的抽象方法(使用Ctrl+O快捷键)

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");
    }
}

11.3 多个拦截器的执行顺序

a>若每个拦截器的preHandle()都返回true

此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:

preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

b>若某个拦截器的preHandle()返回了false

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

12 异常处理器

1、基于配置的异常处理

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

HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver

SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver,使用方式:

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

2、基于注解的异常处理

//@ControllerAdvice将当前类标识为异常处理的组件
@ControllerAdvice
public class ExceptionController {

    //@ExceptionHandler用于设置所标识方法处理的异常
    @ExceptionHandler(ArithmeticException.class)
    //ex表示当前请求处理中出现的异常对象
    public String handleArithmeticException(Exception ex, Model model){
        model.addAttribute("ex", ex);
        return "error";
    }

}

13 注解配置SpringMVC

使用配置类和注解代替web.xml和SpringMVC配置文件的功能

1、创建初始化类,代替web.xml

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

总结一句话:如果用类代替web.xml文件,需要这个类去继承AbstractAnnotationConfigDispatcherServletInitializer

public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

    /**
     * 添加过滤器
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8");
        encodingFilter.setForceRequestEncoding(true);
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
    }
}

2、创建SpringConfig配置类,代替spring的配置文件

@Configuration
public class SpringConfig {
	//ssm整合之后,spring的配置信息写在此类中
}

3、创建WebConfig配置类,代替SpringMVC的配置文件

/**
 * @Author: zyl
 * @Date: 2021/08/24/17:14
 * @Description: 代替SpringMVC的配置文件
 *  1、扫描组件
 *  2、视图解析器
 *  3、view-controller
 *  4、default-servlet-handler
 *  5、mvc注解驱动
 *  6、文件上传解析器
 *  7、异常处理
 *  8、拦截器
 */
//将当前类表示为一个配置类
@Configuration
//1、扫描组件
@ComponentScan("com.atguigu.mvc")
//5、mvc注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {


    //4、default-servlet-handler
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    //8、拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TestInterceptor())
                .addPathPatterns("/**")   //配置拦截的路径
                .excludePathPatterns("/");//排除的的路径
    }

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

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

    //7、异常处理
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {

        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        //不能使用put方法,使用setProperty,使键值对都是字符串
        prop.setProperty("java.lang.ArithmeticException","error");//异常类型和跳转的页面名称
        exceptionResolver.setExceptionMappings(prop);
        exceptionResolver.setExceptionAttribute("exception");//把异常信息放到request共享域
        resolvers.add(exceptionResolver);
    }

    //========配置Thymeleaf视图解析器==============
    //配置生成模板解析器
    @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;
    }
    //====================================


}

4、测试功能

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

14 SpringMVC执行流程

1、SpringMVC常用组件

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

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

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

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

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

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

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

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

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

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

  • View:视图

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

2、DispatcherServlet初始化过程

DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。

image-20220801153212004

在FrameworkServlet的initServletBean()方法中,调用了initWebApplicationContext()方法,下面就需要看如何初始化IOC容器

a>初始化WebApplicationContext

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

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        // 创建WebApplicationContext
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            // 刷新WebApplicationContext
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        // 将IOC容器在应用域共享
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
b>创建WebApplicationContext

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

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
            "Fatal initialization error in servlet with name '" + getServletName() +
            "': custom WebApplicationContext class [" + contextClass.getName() +
            "] is not of type ConfigurableWebApplicationContext");
    }
    // 通过反射创建 IOC 容器对象
    ConfigurableWebApplicationContext wac =
        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    // 设置父容器
    wac.setParent(parent);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
c>DispatcherServlet初始化策略

FrameworkServlet创建WebApplicationContext后,刷新容器,调用onRefresh(wac),此方法在DispatcherServlet中进行了重写,调用了initStrategies(context)方法,初始化策略,即初始化DispatcherServlet的各个组件

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

protected void initStrategies(ApplicationContext context) {
   initMultipartResolver(context);
   initLocaleResolver(context);
   initThemeResolver(context);
   initHandlerMappings(context);
   initHandlerAdapters(context);
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   initViewResolvers(context);
   initFlashMapManager(context);
}

3、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);
    }
}

4、SpringMVC的执行流程

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

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

a) 不存在

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

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

image-20220801163513358

image-20210709214947432

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

image-20210709215255693

image-20210709215336097

b) 存在则执行下面的流程

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

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

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

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

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

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

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

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

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

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

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

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

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

感谢你的阅读
本篇文章是我学习尚硅谷2022版ssm框架做的笔记
欢迎访问我的个人博客

相关文章🌟

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值