JavaWeb_15_SpringMVC


本文介绍SpringMVC框架。

1. SpringMVC概述

1.1 SpringMVC简介

SpringMVC是基于Spring的一个框架,实际上就是Spring的一个模块,专门来做Web开发的,可以看成是Servlet的一个升级。因为Servlet的功能比较弱,在使用上比较繁琐,一切都得自己开发,因此急需一个框架让Web开发更加流畅简单。

Web开发底层都是Servlet,框架是在Servlet的基础上增加一些功能,使得开发更加方便,一些底层实现不需要重复写了。

SpringMVC也是一个Spring,是一个容器,能够创建对象,存放的一般是控制器对象,我们知道Spring采用标签以及注解来创建对象,因此SpringMVC也可以采用这两种方法来创建对象。

我们要做的就是使用@Controller注解创建控制器对象,将对象放入到SpringMVC容器中,把创建的对象当做控制器使用。这个控制器能够接收用户的请求,显示处理结果,虽然是普通的类对象,但是SpringMVC赋予了该控制器对象一些额外的功能,因此可以看成是Servlet对象,可以像Servlet一样接收请求返回结果等等。

我们知道Web开发底层是Servlet,用户和服务器之间的通信都是由Servlet来中转的。因此SpringMVC中有一个DispatcherServlet(中央调度器,前端控制器),该对象是Servlet对象,负责接收用户的所有请求。然后该对象将用户请求转发给我们的控制器对象@Controller,最后由控制器对象处理用户请求,并将处理结果返回给中央调度器,由中央调度器返回给客户端浏览器。

所以,中央调度器的作用就是负责转发,一定程度上相当于代理。此时,我们只需要面向这个代理编程即可。而SprinvMVC则是提供了这个代理,我们只需要实现具体的控制器对象来处理用户请求即可。

另外,中央调度器在创建的时候会自动创建SpringMVC容器(WebApplicationContext),并扫描配置文件springmvc.xml生成控制器对象,将控制器对象放入到该容器中。并将SpringMVC容器放入到Web容器的ServletContext中,方便我们直接获取。

1.2 SpringMVC优点

SpringMVC有以下几个优点:

  1. 基于MVC架构

    基于MVC架构,功能分工明确。解耦合

  2. 容易理解,上手快;使用简单

  3. 作为Spring框架的一部分,能够使用Spring的IoC和AOP,方便整合其他框架。

  4. SpringMVC强化注解的使用,在控制器、Service、Dao等都可以使用注解,方便灵活。
    使用@Controller创建处理器对象,@Service创建业务对象,@Autowired或者@Resource在控制器类中注入Service,在Service类中注入Dao。

或者说,SpringMVC就是Spring,只不过专门来开发Web项目。

1.3 第一个注解的SpringMVC程序

需求如下所示:

用户在页面发起一个请求,请求交给SpringMVC的控制器对象。并显示请求的处理结果(在结果页面显示一个欢迎语句)。

步骤如下所示:

  1. 新建web maven工程

  2. 加入依赖

    1. spring-mvc依赖,(会自动加入spring的相关依赖)
    2. jsp、servlet依赖

    可以看到springmvc依赖已经包括了spring必需的依赖。如下所示:

    springmvc_001.png (905×362) (gitee.io)

    具体的依赖如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <dependencies>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    </dependency>

    <!-- servlet依赖 -->
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
    </dependency>

    <!-- jsp依赖 -->
    <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/jsp-api -->
    <dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
    <scope>provided</scope>
    </dependency>

    <!-- spring mvc 依赖 -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.18</version>
    </dependency>

    </dependencies>
  3. 重点在web.xml中注册springmvc框架的核心对象:DispatcherServlet

    1. 该对象叫做中央调度器,是一个Servlet,它的父类是HttpServlet
    2. 该对象也被称为前端控制器(front controller)
    3. 该对象负责接收用户提交的请求,调用其他的控制器对象,并把请求的处理结果显示给用户
    4. 可以看到该对象和我们之前手写的Servlet功能一样,只不过这是Spring实现的,我们只需要拿来用即可。

    注意,自动生成的web.xml中的版本比较低,需要重新生成一下,或者拷贝现有的其他文件。

    注意,启动web服务器后,springmvc创建对象时,要读取的配置文件默认是/WEB-INF/<servlet-name>-servlet.xml(即使创建的对象个数为0,也会读取),此时因为并不存在这个文件会报错,我们可以手动设置读取的配置文件位置,一般情况下是在根路径下,即resources/springmvc.xml。配置文件如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <?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 -->
    <!-- 注意,我们一般情况下,Servlet对象是在被访问的时候才会被创建,所以一般都是写映射路径。
    但是我们期望该中央调度器对象是在web服务器被启动的时候就要创建出来,因为该对象在创建的
    时候会自动创建springmvc容器,读取springmvc的配置文件,把这个配置文件中的对象都自动创建好,

    同时,会将该容器对象放入到web服务器中的ServletContext中,我们可以直接获取该容器。

    而我们后续会用到该容器中的对象,当用户发起请求时就可以直接使用容器中的对象了。

    因此,我们希望爱服务器启动的时候,就要创建该对象,而不是等待外部访问该注册路径的时候再创建对象
    -->
    <servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <!-- 自定义springmvc读取的配置文件的位置,中央调度器需要读取该文件创建对象,并将其放入到springmvc容器中 -->
    <init-param>
    <!-- springmvc的配置文件的位置的属性 -->
    <param-name>contextConfigLocation</param-name>
    <!-- 指定自定义文件的位置,一般放置在类路径下 -->
    <param-value>classpath:springmvc.xml</param-value>
    </init-param>

    <!-- 表示服务器启动的时候就创建该对象,数字表示优先级,越小优先级越高,大于等于0的整数 -->
    <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
    <servlet-name>springmvc</servlet-name>

    <!-- 注意,使用框架的时候,url-pattern可以使用两种值 -->
    <!-- 一是使用扩展名的方法,语法:*.xxx,xxx是自定义的扩展名,常用的有:*.do,*.action, *.mvc等等 -->
    <!-- 如:http://localhost:8080/myweb/some.do -->
    <!-- 如:http://localhost:8080/myweb/other.do -->
    <!-- 等等,以.do结尾的请求都会映射到springmvc框架中 -->

    <!-- 第二种方式则是采用 '/',后续再说 -->
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>

    </web-app>

    总体来说,声明中央调度器还需要声明以下三个内容:

    1. 该对象创建时需要读取的配置文件位置:springmvc.xml(需要提前创建好)。因为中央调度器在创建时会进行一些列的附属操作。
    2. 该对象的创建时间:服务器启动的时候就要创建。
    3. 在映射的时候,注意请求路径,一般都是通配符批量将请求映射到SpringMVC框架中。
  4. 创建一个发起请求的页面,如index.jsp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <p>第一个springmvc项目</p>
    <p><a href="some.do">发起some.do请求</a> </p>
    </body>
    </html>
  5. 创建控制器类(普通类,只不过在类上面加入注解创建对象,也被称为处理器类,用于处理用户请求)

    1. 在类的上面加入@Controller注解,创建对象,并放入到SpringMVC容器中;
    2. 在类中的方法上面加入@RequestMapping注解。(采用注解方式开发
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    package org.hianian.controller;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.ModelAndView;

    /**
    * @Author : hianian
    * @Time : 2022/4/12 10:46
    * @File : MyController.java
    */

    /**
    * @Controller: 该注解用于创建处理器对象,对象放在SpringMVC容器中
    * 位置:在类的上面
    *
    * 这个注解和Spring中的@Service@Component类似
    *
    * 属性为value,值为对象的名称。但是一般情况下不需要起名字。
    */
    @Controller
    public class MyController {

    // 处理用户提交的请求,springmvc中是用方法来处理用户请求
    // 方法是自定义的,可以有多种返回值、多种参数,方法名称自定义。

    /**
    * 准备使用doSome()方法处理some.do请求。
    *
    * 我们知道在web.xml文件中,是将所有的do请求映射给了中央控制器对象。那么怎么通知时候该方法处理do请求呢?
    *
    * 可以采用@ResultMapping注解。(本质上就是一个方法来处理一个请求)
    *
    * @ResultMapping:请求映射注解,作用是把一个请求地址和一个方法绑定在一起。
    * 该注解有以下几个属性:
    * 1. value,表示请求的uri地址,值必须是唯一的。是一个字符串的数组,表示可以将多个uri都映射到该方法上来处理
    * 位置一般是在方法上使用,(在类上面使用也可以。),推荐以"/"开始。
    *
    * 说明:使用RequestMapping修饰的方法叫做处理器方法或者控制器方法。使用该注解修饰的方法可以处理请求的,类似Servlet中
    * 的doGet、doPost等方法。
    *
    * @return:返回值类型是ModelAndView类型,包括:
    * 1. Model:数据,请求处理完成后,要显示给用户的数据
    * 2. View:视图,比如JSP等等。
    * 总的来说,返回值就是本次请求的处理结果。这个类是mvc框架中的类。
    */

    @RequestMapping(value = "/some.do")
    public ModelAndView doSome(){
    // 处理用户请求
    // 其实这里的处理和Servlet中的一样,调用service方法,以及访问数据库操作数据等等。
    // 这里为了方便,假设已经处理好了,接下来就是将处理结果(数据和视图)放入到ModelAndView中

    // 创建ModelAndView类对象
    ModelAndView mav = new ModelAndView();

    // 添加数据(可以使用key,value形式,其中key是字符串,value是object类型)
    mav.addObject("msg", "欢迎使用SpringMVC做web开发");
    mav.addObject("fun", "处理函数是dosome()方法");

    // 本质上,框架会将上述的<key, value>放入到request作用域中。
    // 即request.setAttribute("msg", "欢迎使用SpringMVC做web开发")

    // 指定视图,指定视图的完整路径
    // 框架对视图的使用,本质上是执行forward转发操作,即request.getRequestDispatcher("/show.jsp").forward(...)
    mav.setViewName("/show.jsp");

    // 返回ModelAndView对象
    // 即我们只需要将数据和视图放入到ModelAndView对象里面即可,框架会自动做setAttribute和forward操作。
    // 因此,我们还需要的就是把数据取出来,放到jsp页面合适的位置中。
    // 可以看到,框架是帮我们做了一些典型的操作(使得开发更具有流程化,代码更加简洁清晰),具体的个性化设置需要我们自己来做。
    return mav;
    }
    }
  6. 创建一个显示结果的页面,显示请求的处理结果。

    show.jsp页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <h3>show.jsp从request作用域中获取数据</h3>
    <!-- 这里我们只需要从request作用域中取数据即可 -->
    <h3>msg数据:${msg}</h3>
    <h3>fun数据:${fun}</h3>
    </body>
    </html>
  7. 创建SpringMVC的配置文件(和Spring的配置一样)

    1. 声明组件扫描器,指定@Controller注解所在的包名;
    2. 声明视图解析器,帮助处理视图。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?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">

    <!-- springmvc配置文件,用于声明springmvc容器中所需的一些对象,如控制器 -->
    <!-- 另外,即使没有要创建的对象,也需要这个配置文件,因为规定了中央调度器会读取这个文件,所以如果没有就会报错 -->
    <!-- 因为采用的是注解生成对象,所以需要声明组件扫描器 -->

    <!-- 声明组件扫描器 -->
    <context:component-scan base-package="org.hianian.controller"></context:component-scan>
    </beans>
  8. 结果如下所示:

    springmvc_002.png (599×152) (gitee.io)

    springmvc_003.png (637×227) (gitee.io)

1.4 SpringMVC中的MVC组件

SpringMVC框架组织对象的时候实际上采用了MVC架构模式来组织的。如下所示,其中Front Controller就是前端控制器,即中央调度器DispatcherServlet;然后将请求转发给实际的控制器对象Controller(后端控制器,Back Controller),由它来处理用户请求;之后,Controller对象将结果(ModelAndView)返回给中央调度器,中央调度器调用View,将数据添加到视图中。

上面的中央调度器以及控制器就是MVC中的C,是一个控制器的角色;Controller的处理请求生成的数据就是MVC中的M;View视图显示的结果就是MVC中的V。

springmvc_005.png (1046×725) (gitee.io)

1.5 SpringMVC执行流程

SpringMVC处理流程如下所示:

  1. 用户发起请求:some.do
  2. 这个请求会到达服务器,交给web服务器(Tomcat)。
  3. Tomcat服务器读取web.xml配置文件中的Servlet映射,发现该请求对应的时中央调度器,于是创建中央调度器以及SpringMVC容器。
  4. 另外,在创建中央调度器的同时,会读取springmvc.xml配置文件,进而创建组件扫描器,创建@Controller注解修饰的对象。在创建对象的时候,扫描方法,发现@RequestMapping修饰的方法,并映射了some.do。因此找到了处理该请求的具体方法。
  5. 框架执行该方法,得到了ModelAndView对象,并转发到show.jsp页面中。

从上面可以看出,实际上中央调度器就是进行自动映射,将用户请求映射到具体的处理方法中。此时我们不再需要手动在配置文件中写<servlet>以及<servlet-mapping>了,使得配置文件更加简洁。大致的流程图如下所示:

springmvc_004.png (1746×730) (gitee.io)

可以看到,这里实际上和动态代理比较相似,每增加一个用户请求,不再需要手动在配置文件中进行servlet映射了,只需要在处理方法中加上映射注解即可,具体的映射操作可由中央调度器进行管理。

SpringMVC执行过程源代码分析如下所示(主要分为两部分):

  1. Web服务器启动,创建容器的过程。

    我们知道配置文件中有DispatcherServlet标签,而该标签内<load-on-start>标签指定的优先级为1,即在服务器启动的时候就创建DispatcherServlet对象。由于该类是继承HttpServlet的,是一个Servlet,所以在创建时会执行init()方法。(注意,因为是设置的是服务器启动就创建该对象,所以不是当用户发起请求时才创建的。

    在init()方法中,会执行类似以下代码(创建SpringMVC容器):

    1
    2
    3
    4
    5
    // 创建容器对象,读取配置文件
    WebApplicationContext ctx = new ClassPathXmlApplicationContext("springmvc.xml");

    // 把容器对象放入到ServletContext中
    this.getServletContext.setAttribute(key, ctx);

    在创建SpringMVC对象的时候,会执行组件扫描器,创建@Controller注解修饰的类对象。并将这些Controller对象放入到SpringMVC容器(key, value)中。

    中央调度器类的继承关系大致如下所示(可以关联一下源码查看具体步骤):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class DispatcherServlet extends FrameworkServlet{}

    public abstract class FrameworkServlet extends HttpServletBean{}

    public abstract class HttpServletBean extends HttpServlet {
    public final void init() throws ServletException {
    ...

    this.initServletBean();
    }
    }

    上述过程是Web服务器在启动的时候执行的操作,简单地说,就是创建SpringMVC容器以及@Controller对象,并将对象放入到容器中,将该容器放入到Web服务器的全局作用域中。

  2. 请求的处理过程

    因为本质上就是Servlet,所以,这里将本质上会调用service方法。执行的方法大概如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected void service(HttpServletRequest request, HttpServletResponse response){}

    protected void doService(HttpServletRequest request, HttpServletResponse response){}

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response){
    ...
    // 调用并执行@Controller处理器中的对应方法,获得返回值ModelAndView类型
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    ...
    }

    上面就是大致的执行过程。本质上说,都是中央调度器DispatcherServlet中的方法,即该中央调度器负责匹配请求和Servlet的对应关系,并负责调用该方法处理用户请求。

配置视图解析器:

我们知道,在Servlet中可以设置过滤器来防止用户通过URL来访问不可直接访问的资源,比如必须通过点击按钮来跳转到某一个页面,此时用户可直接输入对应的URL来访问跳转后的页面。但是过滤器一般情况下是用来设置用户权限的。

因此,我们可以将页面资源放置到WEB-INF目录下,这样就不会通过URL来访问到了。因此,对应的在@Controller中的处理方法中需要设置路径,如:mav.setViewName("/WEB-INF/view/show.jsp");,但是如果有多个页面,其路径前缀往往是重复的,因此可以配置视图解析器

配置文件springmvc.xml中的配置如下所示:

1
2
3
4
5
6
7
8
<!-- 声明SpringMVC框架中的视图解析器,帮助开发人员设置视图文件的路径 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 声明视图文件的前缀 -->
<property name="prefix" value="/WEB-INF/view/"></property>

<!-- 声明视图文件的后缀,即扩展名 -->
<property name="suffix" value=".jsp"></property>
</bean>

控制器中的代码如下所示:

1
2
3
4
5
6
7
// 指定视图,指定视图的完整路径
// 框架对视图的使用,本质上是执行forward转发操作,即request.getRequestDispatcher("/show.jsp").forward(...)
// mav.setViewName("/WEB-INF/view/show.jsp");

// 声明视图解析器之后,这里设置视图的路径可以变得简单。可以采用逻辑名称(文件名)来指定视图
// 框架会使用视图解析器的 前缀 + 逻辑名称 + 后缀 来组成完成路径,即字符串连接操作。
mav.setViewName("show");

这样,可以比较方便地设置视图了。

另外,前面提到过,可以有多个Controller,也可以有多个RequestMapping,可以将多个请求映射到一个RequestMapping中,即value属性值有多个。

(讲解完全部知识,这里再来回顾一下SpringMVC的执行流程)

springmvc_012.png (787×425) (gitee.io)

SpringMVC内部请求的处理流程如下所示:

  1. 用户发起请求 some.do
  2. DispatcherServlet接收请求 some.do,之后把请求转交给处理器映射器,这是一种实现了HandlerMapping接口的类对象,用于根据请求从SpringMVC容器中获取到处理该请求的Controller处理器对象。
  3. 映射器将找到的控制器对象,放入到处理器执行链中的类对象中保存(HandlerExecutionChain,这个对象有两个属性,一个是控制器,一个是拦截器数组以及List集合)
  4. 中央调度器根据上步得到的处理器执行链,将其交给处理器适配器类对象(HandlerAdapter),这个适配器主要用来执行控制器的处理方法。在获得处理器方法的返回结果后,将结果(如ModelAndView)返回给中央调度器。
  5. 中央调度器将得到的处理结果,交给视图解析器(ViewResolver)。该对象负责组成视图的完整路径,并创建View类对象(View接口),用于表示视图,返回给中央调度器。
  6. 最后,中央调度器调用视图对象View的方法,将结果数据放入到request作用域,执行forward,响应给用户浏览器。

2. SpringMVC注解式开发(重点)

通过上面讲解,可以看到,最重要的就是@Controller注解和@RequestMapping注解,一个是用于创建控制器,一个是用于映射并处理用户请求。可以说,这两个是SpringMVC中最重要的部分,其中@RequestMapping最为重要。

2.1 @RequestMapping定义请求规则

前面提到过,可以有多个Controller、多个ResultMapping。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Controller
public class MyController {

@RequestMapping(value = {"/test/some.do", "/test/first.do"})
public ModelAndView doSome(){

ModelAndView mav = new ModelAndView();

mav.addObject("msg", "第二个项目some方法");
mav.addObject("date", "20220418");

mav.setViewName("show");

return mav;
}

@RequestMapping(value = {"/test/other.do", "/test/second.do"})
public ModelAndView doOther(){

ModelAndView mav = new ModelAndView();

mav.addObject("msg", "第二个项目other方法");
mav.addObject("date", "20220418");

mav.setViewName("show");

return mav;
}
}

其实上面有一个不足,就是这些路径有共同的前缀,因此,可以将前缀抽取出来,放到类上面进行定义。也就是说,一般情况下,一个模块或者一批类似的请求(拥有相同的前缀)映射到一个Controller对象中,之后,根据细分的请求路径来进一步映射到具体的方法中。

@RequestMapping可以放到类上面,也可以放到方法上面。

  1. 放到类上面,就是上面说的,匹配URL前缀,value属性值即为前缀,也被称为模块名称

    如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Controller
    @RequestMapping(value = "/test")
    public class MyController {

    @RequestMapping(value = {"/some.do", "/first.do"})
    public ModelAndView doSome(){

    ModelAndView mav = new ModelAndView();

    mav.addObject("msg", "第二个项目some方法");
    mav.addObject("date", "20220418");

    mav.setViewName("show");

    return mav;
    }

    @RequestMapping(value = {"/other.do", "/second.do"})
    public ModelAndView doOther(){

    ModelAndView mav = new ModelAndView();

    mav.addObject("msg", "第二个项目other方法");
    mav.addObject("date", "20220418");

    mav.setViewName("show");

    return mav;
    }
    }
  2. 放到方法上面,就是匹配具体的请求路径。

该注解可以控制处理的请求方式是get还是post,可通过属性method来控制处理的请求选择,属性值是RequestMethod类枚举值。如get方式,则需要指明RequestMethod.GET注意,如果不声明,则任意请求方式均可。简单代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value = {"/some.do", "/first.do"}, method = RequestMethod.GET)
public ModelAndView doSome(){

ModelAndView mav = new ModelAndView();

mav.addObject("msg", "第二个项目some方法");
mav.addObject("date", "20220418");

mav.setViewName("show");

return mav;
}

2.2 处理器方法的参数

上面讲解了接收用户的请求,那么接下来讲解如果获取请求中的具体数据(比如Servlet运行过程中的通用数据、用户提交的数据等等)。处理器方法可以包含以下四类参数,这些参数会在系统调用时由系统自动赋值,即程序员可以在方法内直接使用。

  1. HttpServletRequest
  2. HttpServletResponse
  3. HttpSession
  4. 请求中所携带的请求参数(用户提交的数据)

上述参数主要放在@RequestMapping所修饰方法的形参位置,这样,我们才能够在处理用户请求时获取到这些变量。对于前三个参数,如果需要在方法中使用,直接在方法形参列表中声明即可,框架会自动完成赋值。简单代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping(value = {"/other.do", "/second.do"}, method = RequestMethod.POST)
public ModelAndView doOther(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
HttpSession httpSession){

ModelAndView mav = new ModelAndView();

// 获取参数的相关内容,框架会自动赋值,我们只需要拿来用即可。
mav.addObject("msg", "第二个项目other方法" + "--->" + httpServletRequest.getParameter("name"));
mav.addObject("date", "20220418");

mav.setViewName("show");

return mav;
}

2.2.1 接收用户提交的数据

主要有以下几种方式:

  1. 逐个接收

    该方法需要在控制器方法中添加形参,且形参名和用户提交的参数名(name)必须保持一致,即同名的请求参数赋值给同名的形参

    1
    2
    3
    4
    5
    <form action="test/third.do" method="post">
    姓名:<input type="text" name="name">
    年龄:<input type="text" name="age">
    <input type="submit" value="提交third请求">
    </form>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 接收用户数据
    @RequestMapping(value = "/third.do", method = RequestMethod.POST)
    public ModelAndView doSecond(String name, int age){

    // 采用逐个接收的方式来接收用户请求参数
    // 要求形参中的形参名和请求中的参数名(name)必须一致。(同名的请求参数赋值给同名的形参)

    ModelAndView mav = new ModelAndView();

    // 获取参数的相关内容
    mav.addObject("msg", "name=" + name);
    mav.addObject("age", "age=" + age);

    mav.setViewName("show");

    return mav;
    }

    框架底层的操作大致如下所示:

    1. 使用request对象接收请求参数

      1
      2
      String strName = request.getParameter("name");
      String strAge = request.getParameter("age");
    2. SpringMVC框架通过中央调度器DispatcherServlet来调用控制器@Controller的doThird()方法,**在调用方法的时候,按照名称对应关系将参数赋值给形参。即doThird(strName, Integer.valueOf(strAge)**。自动进行类型转换等操作。

    另外要注意,因为自动进行类型转换,所以提交的数据必须满足类型转换要求。比如age如果是空的话,那么就会出现类型转换异常,因为Integer.valueOf()不会把空字符串转换为整型,也不会把“asdf”等类型的字符串转换为整型。因此,这种数据类型限制比较大,一般采用Integer包装类型或者String类型后续手动转换,而不是int。

    对于参数乱码问题,一般采用过滤器进行额外设置字符编码,使其对所有请求均生效。可以自定义过滤器,也可以采用框架提供的过滤器CharacterEncodingFilter。(本质上还是通过request.setCharacterEncoding()来设置编码的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!-- 注册声明过滤器,解决post请求乱码的问题 -->
    <filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>

    <!-- 对类中的属性赋值 -->
    <!-- 1. 设置字符编码方式 -->
    <init-param>
    <param-name>encoding</param-name>
    <param-value>utf-8</param-value>
    </init-param>

    <!-- 2. 强制请求对象HttpServletRequest使用上述encoding编码方式 -->
    <init-param>
    <param-name>forceRequestEncoding</param-name>
    <param-value>true</param-value>
    </init-param>

    <!-- 3. 强制响应对象HttpServletResponse使用上述encoding编码方式 -->
    <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>

    但是有时候用户参数名和方法形参名不是同名,这时候该怎么办呢?可以采用@RequestParam注解,该注解用在方法形参名的前面,有一个value属性,值为用户请求参数名。另外还有一个required属性,默认为true,表示必须有该参数。

    1
    2
    3
    4
    5
    <form action="test/param.do" method="post">
    姓名:<input type="text" name="rname">
    年龄:<input type="text" name="rage">
    <input type="submit" value="提交param请求">
    </form>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 接收用户数据
    @RequestMapping(value = "/param.do", method = RequestMethod.POST)
    public ModelAndView doParam(@RequestParam(value = "rname") String name, @RequestParam(value = "rage") int age){

    System.out.println("从用户端接收到的数据name为" + name + ",年龄为" + age);

    // 采用逐个接收的方式来接收用户请求参数
    // 要求形参中的形参名和请求中的参数名(name)必须一致。(同名的请求参数赋值给同名的形参)

    ModelAndView mav = new ModelAndView();

    // 获取参数的相关内容
    mav.addObject("msg", "name=" + name);
    mav.addObject("age", "age=" + age);

    mav.setViewName("show");

    return mav;
    }
  2. 对象接收

    上述方法适合参数比较少的情况,如果参数特别多,那么方法的形参列表就会比较长,这样书写代码容易出错。因此可以将处理器方法的参数定义为一个对象,只要保证请求参数名和这个对象的属性同名即可。框架会进行数据封装。

    首先,如果形参名是Java对象,那么框架就会创建形参的对象,给属性赋值。即如果请求参数的名称为name,那么框架就会调用类对象的setName()方法,给属性赋值。

    为了方便测试流程,这里在Student类中输出和一些语句。代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 保存请求参数值的一个普通类
    public class Student {

    private String name;
    private Integer age;

    public Student(){
    System.out.println("无参构造方法执行,创建Student类对象");
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    System.out.println("setName()方法执行:" + name);
    this.name = name;
    }

    public Integer getAge() {
    return age;
    }

    public void setAge(Integer age) {
    System.out.println("setAge()方法执行:" + age);
    this.age = age;
    }

    @Override
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 接收用户数据
    @RequestMapping(value = "/object.do", method = RequestMethod.POST)
    public ModelAndView doObject(Student myStudent){

    System.out.println("从用户端接收到的数据name为" + myStudent.getName() + ",年龄为" + myStudent.getAge());

    // 采用逐个接收的方式来接收用户请求参数
    // 要求形参中的形参名和请求中的参数名(name)必须一致。(同名的请求参数赋值给同名的形参)

    ModelAndView mav = new ModelAndView();

    // 获取参数的相关内容
    mav.addObject("msg", "name=" + myStudent.getName());
    mav.addObject("age", "age=" + myStudent.getAge());
    mav.addObject("student", myStudent);

    mav.setViewName("show");

    return mav;
    }

    经过测试,构造方法以及set方法确实会依次执行。

2.3 处理器方法的返回值

上节介绍了方法的形参设置,这节介绍方法的返回值。使用@Controller注解修饰的处理器的处理器方法,其常用的返回值类型有以下四种类型:

  1. ModelAndView
  2. String
  3. 无返回值void
  4. 自定义类型对象(Object)

可以根据不同的情况,使用不同的返回值。本质上说,这里的返回值只是让MVC框架知道返回值类型,方便框架进一步转换操作。其实我们也可以不借助框架,自己通过response的输出流来返回值。 比如返回一个文件【用于文件下载】。

2.3.1 返回ModelAndView

若处理器方法处理完后,需要跳转到其他资源,并且需要在跳转的资源之间传递数据,此时处理器方法返回ModelAndView类型比较好。当然,若要返回ModelAndView,则处理器方法中需要定义ModelAndView。

在使用时,若该处理器方法只是进行跳转而不传递数据,或者只是传递数据而不向任何资源跳转(如对页面的AJAX异步响应),此时若返回ModelAndView,则将总有一部分是多余的,要么是Model多余,要么是View多余。此时返回ModelAndView则不太合适。

2.3.2 返回String

如果只需要视图之间的跳转,而不需要传递数据,那么可以采用String返回值类型。处理器方法返回的字符串可以指定逻辑视图名(或者物理视图地址),通过视图解析器可以将其转换为物理视图地址。

代码如下所示:

1
2
3
4
5
6
7
8
@RequestMapping(value = "/dome.do", method = RequestMethod.POST)
public String doSome(String name, String age){

System.out.println("获取到的参数为:name=" + name + ", age=" + age);

// 返回值类型为字符串,直接指明要跳转资源的逻辑名称即可
return "show";
}

当然,如果需要在资源之间传递数据,我们可以手动的添加HttpServletRequest形参,并将数据添加到请求域中,这样,可以在页面中直接取数据,和ModelAndView达到一样的效果。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/dome.do", method = RequestMethod.POST)
public String doSome(HttpServletRequest request, String name, String age){

System.out.println("获取到的参数为:name=" + name + ", age=" + age);

request.setAttribute("name", name);
request.setAttribute("age", age);

// 返回值类型为字符串,直接指明要跳转资源的逻辑名称即可
return "show";
}

2.3.3 返回void(了解)

因为返回值是void,所以该返回值实际上没什么用,即不能表示数据,也不能表示视图,可以用于AJAX请求,在HttpServletResponse中传输响应内容(注意,AJAX请求服务器端返回的就是数据,和视图无关,且AJAX需要JSON格式的数据,此时需要服务器端将数据转换为JSON格式)。对象有属性,因此属性值就是具体的数据。

这里不再展示代码部分。

可以看到,对JSON返回数据所做的处理主要分为三部分:

  1. 创建对象,给属性赋值
  2. 将对象转换为JSON对象
  3. 将JSON格式数据通过HttpServletResponse输出到用户端。

那么其中2、3步骤是通用的,关键在于第一部分。所以可以进一步简化上述操作,由框架来执行2、3步骤,见2.3.4。

如果是文件下载,即返回值是文件类型,可以设置为void类型,自己在内部通过response来通过输出流来设置。

2.3.4 返回Object

处理器方法也可以返回Object对象。这个Object可以是Integer、String、自定义对象、Map、List等等。但是返回的对象不是作为逻辑视图出现的,而是作为直接在页面显示的数据出现的。主要处理AJAX请求。返回对象,需要使用@ResponseBody注解,将转换后的JSON数据放入到响应体中。

可以看出来,这里的Object不是真正的Object,而是Object所包括的任意数据类型【就是说,虽然返回值类型是Object,但是可以返回任意类型的对象】。响应AJAX(针对json格式数据)的相关步骤如下所示:

  1. 加入处理json的工具库的依赖,springmvc默认使用jackson。
  2. 在springmvc的配置文件中加入<mvc:annotation-driven>注解驱动,功能负责上面的第二步骤。
  3. 在处理器方法上面加入@ResponseBody注解,功能负责上面的第三步骤。

那么只返回Object类对象,为何就会返回给用户端了呢,怎么转换的格式呢?大致原理如下所示:

springmvc处理器方法返回Object,可以转换为json格式输出到浏览器、响应AJAX的内部原理:

  1. <mvc:annotation-driven>注解驱动

    注解驱动实现的功能是:完成Java对象到 json,xml,text,二进制等数据格式的转换。这个驱动涉及到HttpMessageConverter接口(消息转换器),这个接口定义了 Java对象转换为 json等格式数据的方法,有很多的实现类(常用的有StringHttpMessageConverter、MappingJackson2HttpMessageConverter),且实现了上述方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface HttpMessageConverter<T> {
    ...

    // 针对控制器方法返回的Object,为了将其输出到浏览器,会用到该方法
    // 该方法会检查返回值Object能不能转换为MediaType类型,如果能就返回true
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    // 针对控制器方法返回的Object,为了将其输出到浏览器,会用到该方法
    // 将返回值Object转换成对应的MediaType类型数据
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
    }

    实际上,在加入该注解后,会自动创建该接口的7个实现类对象,用于后续的数据类型转换。

    springmvc_006.png (1066×698) (gitee.io)

  2. @ResponseBody注解

    这个注解用于放置在处理器方法的上面,通过HttpServletResponse输出数据,响应AJAX请求。

JSON相关的依赖如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- json依赖 -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>

配置文件springmvc.xml中的注解驱动为:

1
<mvc:annotation-driven></mvc:annotation-driven>

控制器方法具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 处理器方法返回一个Student,通过框架转换为JSON格式,响应AJAX请求
// ResponseBody注解会把处理器方法返回的对象转换为json,通过HttpServletResponse输出给浏览器
@RequestMapping(value = "/resutrnStudentJson.do", method = RequestMethod.POST)
@ResponseBody
public Student doStudentJsonObject(String name, Integer age) {

// 假设数据处理完成
Student student = new Student();
student.setName(name);
student.setAge(age);

// 返回Student类型,会被框架转换为json数据
return student;
}

大致流程如下所示:

  1. 该方法会将数据封装成返回的类型,之后框架会根据返回的Student类型,调用框架的ArrayList集合中的HttpMessageConverter实现类对象,调用canWrite()方法,来检测可以转换成哪个类型。
  2. 根据找到的类型,调用write()方法,把该Student类型对象转换为对应的类型。
  3. 框架会调用@ResponseBody注解的功能,将转换后的类型数据输出到浏览器,ajax请求处理完成。

有时候,我们返回的不是一个对象,而是对象集合List、Map等集合,那么此时该怎么做呢?

其实步骤和上面一样,框架会先将List中的每个对象转化为json对象,再将json对象封装为json数组。(不需要我们进行额外操作)

另外,Object类型也可以是String,因为有了注解@ResponseBody修饰,此时String不再表示视图,而是数据。注意,此时返回的类型就不是json格式了,在用户端接收的时候需要注意。此时可能出现乱码问题,可以在@RequestMapping注解中加入produces="test/plain;charset=utf-8"属性。注意,这里是通过专有的渠道返回给AJAX,并不走过滤器,所以过滤器对此时的乱码无效。

1
2
3
4
5
@RequestMapping(value = "/resutrnStudentJsonArray.do")
@ResponseBody
public String doStudentJsonObjectArray(String name, Integer age) {
return name + "-->是" + age;
}

这部分主要讲解了返回Object类型数据的相关处理,如何转换成json数据类型,如何接收等等。这部分主要用于处理ajax异步请求,比void更加方便。

2.4 解读<url-pattern>(静态资源访问)

这个标签主要用于用户请求映射到哪个Servlet中,通过前面的讲解,可以发现,我们是将*.do(以.do结尾的请求)映射到SpringMVC的中央调度器中。那么对于其他请求呢?比如html、jsp、jpg等静态资源,我们可以直接输入URl。

其实对于其他我们没有在web.xml配置文件中映射的请求,都会映射到Web服务器默认的Servlet中。以Tomcat为例,其/conf/web.xml配置文件中默认的servlet如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

其描述信息如下,可以看到这个默认的Servlet,是在服务器启动的时候就创建了,为静态资源请求提供服务,并且对于没有指明映射到具体Servlet的所有资源请求,均默认映射到此Servlet。虽然在servlet-mapping中的路径为“/”,但是优先级较低,即如果指明了映射servlet,就不采用本映射关系。

1
The default servlet for all web applications, that serves static resources. It processes all requests that are not mapped to other servlets with servlet mappings (defined either here or in your own web.xml file). 

此时可以注意到,如果我们在SpringMVC的中央调度器的映射中,如果写“/”会怎样呢?这就会覆盖掉Tomcat原有的Default映射。由于中央调度器没有处理静态资源的能力,所以会出现404错误。

但是,写“/”比较简便,而且不再需要特意写专门的后缀名了。那么该怎么办?怎么做才能既写“/”又能使有web服务器默认的Servlet功能呢?有如下两种方式

  1. 在springmvc配置文件中加入 <mvc: default-servlet-handler />

    这种方法的原理是:加入这个标签后,框架会创建控制器对象DefaultServletHttpRequestHandler(类似我们自己创建的Controller对象),这个对象可以把接收的请求转发给 Tomcat的default这个Servlet,让它来处理请求。(可以看一下源码,有转发的过程)。注意,这个控制器会将所有的请求都转发给默认的Servlet,但这样就使得@RequestMapping映射失败,此时需要添加2.3.4中的<mvc:annotation-driven>注解驱动,使其正常映射。

    1
    2
    3
    4
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 声明Handler,用于将请求转发到Tomcat的默认Servlet中,注意要保留上面的注解驱动,因为这个Handler会将所有的请求都转发到默认的Servlet中,导致@RequestingMapping注解映射失败 -->
    <mvc:default-servlet-handler></mvc:default-servlet-handler>
    1
    2
    3
    4
    5
    <servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <!-- <url-pattern>*.do</url-pattern>-->
    <url-pattern>/</url-pattern>
    </servlet-mapping>

    可以看到这种方式依赖于Web服务器,看它是否具有解决静态资源请求的能力。

  2. 在springmvc配置文件中加入<mvc: resources>(掌握

    这种方法会创建Spring自带的ResourceHttpRequestHandler,这个处理器会处理静态资源请求,且这个处理器是Spring框架自己实现的,不再依赖于Web服务器。(注意,该方法仍然需要添加注解驱动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 第二种方式解决静态资源访问 -->
    <!-- 这种方法会创建ResourceHttpRequestHandler处理器,这个处理器是Spring框架自己实现的,用于处理静态资源访问 -->
    <!-- mapping表示请求的uri,可以用通配符 ** 表示全部。 比如image/**,表示image下的所有静态资源 -->
    <!-- location表示静态资源在项目中的位置 -->
    <mvc:resources mapping="/image/**" location="/image/"></mvc:resources>
    <mvc:resources mapping="/html/**" location="/html/"></mvc:resources>
    <mvc:resources mapping="/js/**" location="/js/"></mvc:resources>

    当然,为了减少标签数量以及便于管理,一般我们将静态资源统一放置到一个根目录下(上述目录作为子目录出现)。如下所示:

    1
    2
    3
    4
    5
    6
    7
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 第二种方式解决静态资源访问 -->
    <!-- 这种方法会创建ResourceHttpRequestHandler处理器,这个处理器是Spring框架自己实现的,用于处理静态资源访问 -->
    <!-- mapping表示请求的uri,可以用通配符 ** 表示全部。 比如image/**,表示image下的所有静态资源 -->
    <!-- location表示静态资源在项目中的位置 -->
    <mvc:resources mapping="/static/**" location="/static/"></mvc:resources>

2.5 路径问题

这里解释一下前端发送请求的路径问题。比如当前所在页面为http://localhost:8080/springmvc01/index.jsp,在页面中有两个链接,一个连接是show.jsp,另一个连接是/another.jsp。那么一个开头带“/”,一个不带,二者有什么区别呢?不带“/”表示是在当前资源的父路径下寻址,也就是http://localhost:8080/springmvc01/show.jsp,带斜杠则表示在当前主机上寻址(注意不是当前项目路径下),即http://localhost:8080/another.jsp

也就是说,不带斜杠的可以看成是相对路径,带斜杠的则相当于是绝对路径。相对路径存在一定的缺陷,但是带斜杠了,我们需要额外的加上项目名称才能访问,此时不能将项目名写死,可采用EL表达式:${pageContext.request.contextPath}/another.jsp

除了采用绝对路径之外,也可以采用<base>标签,默认为不带斜杠的地址加上其指明的base地址。

3. SSM整合开发(重点)

其实第2部分讲解的就是控制器方法的定义:@RequestMapping注解匹配用户请求,形参列表接收用户参数,方法的返回值类型用于规定框架的后续操作(如转发资源,传递数据等等)。本节讲解SSM三个框架整合开发项目。

其实通过前面的讲解,我们可以发现:

  1. SpringMVC负责接收用户端的输入,并将处理结果返回给用户端。(也就是界面层
  2. Spring负责处理核心业务逻辑,管理service、dao、工具类对象,使得整体代码更加逻辑清晰。(也就是业务层
  3. MyBatis负责访问数据库,比JDBC更加简洁。(也就是持久层

所以说,这三者在逻辑上是可以融合到一起来开发项目的。总体上来说,用户发起请求--->SpringMVC接收--->调用Spring中的Service对象处理请求--->调用MyBatis处理数据,然后在得到处理结果后,Spring的Service对象将结果发送--->SpringMVC接收--->用户得到结果

三个框架共有两个容器:

  1. SpringMVC容器,管理Controller控制器对象
  2. Spring容器,管理Service,Dao,工具类对象的

我们要做的是把使用的对象交给合适的容器来创建和管理。把Controller还有web开发的相关对象交给SpringMVC容器,这些对象写在SpringMVC配置文件中。把Service、Dao对象交给Spring容器,这些对象写在Spring配置文件中

其中一个难点就是:两个容器是独立的,而在实际工作过程中,SpringMVC中的控制器对象肯定要调用Spring中的Service来处理用户请求,那么这该怎么做呢?实际二者之间在Spring框架开发的时候已经确定好了关系,SpringMVC容器是Spring容器的子容器,类似继承。而子可以访问父的内容,所以SpringMVC容器中的控制器对象可以访问Spring容器中的Service对象,从而解决了上述问题。

实现步骤如下所示:

  1. 新建maven web项目

  2. 加入依赖

    1. springmvc

      1
      2
      3
      4
      5
      6
      7
      <!-- spring mvc 依赖 -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.3.18</version>
      </dependency>
    2. spring

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      <!-- Spring核心 IoC -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.16</version>
      </dependency>

      <!-- AspectJ 外部 AOP -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.3.15</version>
      </dependency>

      <!-- Spring事务 -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.3.14</version>
      </dependency>

      <!-- Spring事务(访问数据库) -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.3.14</version>
      </dependency>
    3. mybatis

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <!-- mybatis依赖 -->
      <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
      <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
      </dependency>

      <!-- mybatis和spring集成的依赖 -->
      <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
      <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.6</version>
      </dependency>
    4. jackson

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- json依赖 -->
      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
      <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.13.1</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
      <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.13.1</version>
      </dependency>
    5. mysql驱动

      1
      2
      3
      4
      5
      6
      7
      <!-- mysql驱动依赖 -->
      <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.27</version>
      </dependency>
    6. druid连接池

      1
      2
      3
      4
      5
      6
      7
      <!-- 阿里提供的数据库连接池 -->
      <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.8</version>
      </dependency>
    7. servlet、jsp

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      <!-- servlet依赖 -->
      <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
      <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
      </dependency>

      <!-- jsp依赖 -->
      <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/jsp-api -->
      <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
      </dependency>
    8. 注意资源文件的编译位置,以及编译插件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      <resources>
      <resource>
      <!-- 告诉指定的目录 -->
      <directory>src/main/java</directory>

      <!-- 拷贝的资源文件类型 -->
      <includes>
      <include>**/*.properties</include>
      <include>**/*.xml</include>
      </includes>

      <!-- 设置过滤器,这里不用设置了,上面相当于设置了 -->
      <filtering>false</filtering>
      </resource>

      <!-- 避免上述操作覆盖默认设置,这里将默认设置的目录添加 -->
      <resource>
      <!-- 告诉指定的目录 -->
      <directory>src/main/resources</directory>

      <!-- 拷贝的资源文件类型 -->
      <includes>
      <include>*.*</include>
      </includes>

      <!-- 设置过滤器,这里不用设置了,上面相当于设置了 -->
      <filtering>false</filtering>
      </resource>
      </resources>

      <!-- 指定jdk的版本 -->
      <plugins>
      <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.1</version>
      <configuration>
      <source>1.8</source>
      <target>1.8</target>
      </configuration>
      </plugin>
      </plugins>
  3. 写web.xml

    1. 注册DispatcherServlet目的是创建SpringMVC容器对象以及接受用户请求。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <!-- SpringMVC相关配置-->
      <!-- 中央调度器 -->
      <servlet>
      <servlet-name>springmvc</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

      <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:dispatcherServlet.xml</param-value>
      </init-param>

      <load-on-startup>1</load-on-startup>
      </servlet>

      <servlet-mapping>
      <servlet-name>springmvc</servlet-name>
      <url-pattern>*.do</url-pattern>
      </servlet-mapping>
    2. 注册Spring的监听器ContextLoaderListener目的是由框架创建Spring的容器对象,这样才能创建Service、Dao等对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!-- Spring相关配置 -->
      <!-- 注册Spring监听器 ,并指定自定义的配置文件位置-->
      <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:applicationContext.xml</param-value>
      </context-param>

      <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
    3. 注册字符集过滤器,解决post请求乱码的问题

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <!-- 注册字符集过滤器,解决post请求乱码的问题 -->
      <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>forceRequestEncoding</param-name>
      <param-value>true</param-value>
      </init-param>

      <init-param>
      <param-name>forceResponseEncoding</param-name>
      <param-value>true</param-value>
      </init-param>
      </filter>

      <filter-mapping>
      <filter-name>characterEncodingFilter</filter-name>
      <url-pattern>/*</url-pattern>
      </filter-mapping>

    其实通过这个配置文件可以发现,我们是通过Web服务器的这个配置文件来告诉Web服务器,在启动的时候创建SpringMVC容器、Spring容器,并将各种请求先经过字符集过滤器,然后再全部映射到SpringMVC容器中。(注意,创建这两个容器所需要的配置文件在后续第5部分。)

  4. 创建包,Controller包、Service包、Dao包、实体类包

    org.hianian.controllerorg.hianian.serviceorg.hianian.daoorg.hianian.entity

  5. 写SpringMVC、Spring、MyBatis的配置文件

    1. springMVC配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      <?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">

      <!-- springmvc的配置文件,用于声明Controller和其他web相关的对象-->

      <!-- 声明组件扫描器,创建Controller对象 -->
      <context:component-scan base-package="org.hianian.controller"></context:component-scan>


      <!-- 声明视图解析器,方便资源之间的路径书写 -->
      <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <!-- 声明视图文件的前缀 -->
      <property name="prefix" value="/WEB-INF/jsp/"></property>

      <!-- 声明视图文件的后缀,即扩展名 -->
      <property name="suffix" value=".jsp"></property>
      </bean>

      <!-- 添加注解驱动(方便返回Object等数据类型以及处理静态资源) -->
      <mvc:annotation-driven></mvc:annotation-driven>

      </beans>
    2. spring配置文件

    3. mybatis主配置文件

    4. 数据库的属性配置文件

  6. 写业务逻辑实现代码,dao接口和mapper文件,service和实现类,controller,实体类等等

  7. 写jsp页面

最终的目录结构如下所示:

springmvc_007.png (292×613) (gitee.io)

4. SpringMVC核心技术

4.1 请求重定向和转发

在Servlet中,我们介绍过转发和重定向两个操作,Servlet中的代码分别是request.getRequestDispatcher("xxx.jsp").forward()response.sendRedirect("xxx.jsp")

SpringMVC框架将上述两个操作进行了封装,可以使用比较简单的方式实现转发和重定向,直接使用forward和redirect两个关键字即可。这两个关键字有一个共同的特点:不和视图解析器一同工作(就是说,视图解析器对forward中的路径不起作用)。

4.1.1 转发

转发forward的语法为:setViewName("forward:视图文件的完整路径")注意,因为该关键字对视图解析器无效,所以必须写视图资源的完整路径。其实对于ModelAndView来说,setViewName()方法本质上就是转发,只不过是自动执行的,即隐式转发,因为存在视图解析器的原因,所以隐式转发的路径相当于限制死了,不太灵活,如果想要转发到路径之外的资源,则只能采用forward显式转发,无需考虑视图解析器。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
@RequestMapping(value = "/user")
public class MyController {

@RequestMapping(value = "/doForward.do")
public ModelAndView doSome() {

ModelAndView mav = new ModelAndView();

mav.addObject("msg", "转到到本页面");

// mav.setViewName("show"); // 这里是隐式转发

// 显式转发,注意,此时可以转发到WEB-INF目录下的资源
mav.setViewName("forward: WEB-INF/view/show.jsp");
return mav;
}
}

4.1.2 重定向

重定向redirect的语法为:setViewName("redirect: 视图文件的完整路径"),一些细节和forward一样。注意,因为重定向是两次请求,两次request作用域,因此重定向后的页面使用EL表达式从request作用域是取不到数据的。框架为了实现两次请求中传递数据,提供了一个功能,就是将ModelAndView中的数据以get请求形式附加到重定向的URL中,此时可以先将第一次的请求数据加入到ModelAndView中,然后在重定向页面中利用EL表达式从URL中取数据。

如果返回值是String类型,也可以return "redirect: 视图文件的完整路径"

注意,重定向仍然不能访问WEB-INF目录下的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
@RequestMapping(value = "/user")
public class MyController {

@RequestMapping(value = "/doRedirect.do")
public ModelAndView doRedirect() {

ModelAndView mav = new ModelAndView();

// 注意,addObject是将数据放到了request作用域中,而重定向是客户端再次发起请求,两次不是同一个作用域,
// 在hello.jsp中,使用转发页面中的EL表达式只能从request作用域中取数据(而重定向是两次请求request作用域)
// 因此在重定向的页面中使用EL表达式是请求不到第一次请求的数据的。
// 但是,框架会提供一个功能,就是将本次addObject中添加的数据,作为请求的参数附加到重定向的再次请求中。
// 可以在url中看到,
// http://localhost:8080/springmvc05/hello.jsp?msg=重定向到本页面
// 所以这个目的就是为了两次请求之间可以传递数据
// 注意,需要使用param.EL表达式从url中取数据。
mav.addObject("msg", "重定向到本页面");

// 重定向
mav.setViewName("redirect:/hello.jsp");
return mav;
}
}

4.2 异常处理

任何程序都有可能发生异常,因此,任何程序都要处理异常。一个项目在代码方面是比较复杂的,所以要处理异常的位置也是比较多的;另一方面,项目是不断更新迭代的,那么异常也就需要可能时刻改变。这样一来,异常这种和项目功能业务逻辑并无太多关系的代码,从某种角度来说,是没有必要的,相当于附加功能。此时可以利用切面的思想,即AOP。将异常代码抽取出来,作为切面加入到项目中。从另一个角度来说,就是将异常代码统一管理,方便修改。也被称为统一全局异常处理。

SpringMVC框架提供了@ExceptionHandler@ControllerAdvice两个注解来实现异常代码统一管理。其实从注解的名字就可以看出来,@ControllerAdvice注解,就是控制器增强,相当于给控制器增加额外的功能,即切面;@ExceptionHandler注解,则是具体的异常处理功能。

异常处理有如下步骤:

  1. 新建maven web项目

  2. 加入依赖

  3. 新建一个自定义异常类 MyUserException,再定义它的子类NameException,AgeException

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 自定义异常,用于其他异常继承
    public class MyUserException extends Exception {

    public MyUserException() {
    super();
    }

    public MyUserException(String message) {
    super(message);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 自定义异常,当用户姓名有异常,抛出NameException
    public class NameException extends MyUserException {

    public NameException() {
    super();
    }

    public NameException(String message) {
    super(message);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 自定义异常类,当Age出现异常时,抛出AgeException
    public class AgeException extends MyUserException {

    public AgeException() {
    super();
    }

    public AgeException(String message) {
    super(message);
    }
    }
  4. 在Controller类抛出NameException、AgeException异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Controller
    @RequestMapping(value = "/user")
    public class MyController {

    @RequestMapping(value = "/Some.do")
    public ModelAndView doSome(String name, Integer age) throws MyUserException {

    ModelAndView mav = new ModelAndView();

    // 根据请求参数的值,抛出异常。

    // 如果姓名不是张三,抛出异常
    if(!"zhangsan".equals(name)) {
    throw new NameException("姓名不正确!!!");
    }

    // 如果年龄为null或者大于80,抛出AgeException
    if( age == null || age > 80 ) {
    throw new AgeException("年龄比较大!!!");
    }


    mav.addObject("myname", name);
    mav.addObject("myage", age);

    mav.setViewName("show"); // 这里是隐式转发

    return mav;
    }
    }

    注意,我们只需抛出异常即可,无需处理异常。

  5. 创建一个普通类,作为全局异常处理类。

    1. 这个类的上面要加入@ControllerAdvice注解

    2. 类中定义方法,方法的上面要加入@Exceptionhandler注解

      这个方法,和Controller处理用户请求的方法的定义一样,返回值以及参数列表等等。该注解用于声明发生哪类异常时,采用本方法来处理。属性值即为规定的异常类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    // 自定义全局异常处理类,需要添加两个注解

    @ControllerAdvice
    public class GlobalExceptionHandler {

    // 定义方法,处理发生的异常
    // 形参为Exception,表示Controller中抛出的异常对象;方法体可以通过形参获取发生的异常信息。
    // 注解属性值为哪个异常类型,表示发生该类异常时,交给该方法来处理
    @ExceptionHandler( value = NameException.class)
    public ModelAndView doNameException(Exception ex){
    // 处理NameException的异常

    // 异常发生处理逻辑:
    // 1. 需要把异常记录下来,记录到数据库,日志文件。记录日志发生的时间,哪个方法发生的,异常的错误内容等等。
    // 2. 发送通知,把异常的信息通过邮件,短信,微信等发送给相关人员。
    // 3. 给用户友好的提示

    // 这里为了简单起见,仅做第三步
    ModelAndView mav = new ModelAndView();

    mav.addObject("msg", "姓名必须是zhangsan,其他用户不能访问");
    mav.addObject("ex", ex);

    mav.setViewName("nameError");

    return mav;
    }


    @ExceptionHandler( value = AgeException.class)
    public ModelAndView doAgeException(Exception ex){
    // 处理NameException的异常

    // 异常发生处理逻辑:
    // 1. 需要把异常记录下来,记录到数据库,日志文件。记录日志发生的时间,哪个方法发生的,异常的错误内容等等。
    // 2. 发送通知,把异常的信息通过邮件,短信,微信等发送给相关人员。
    // 3. 给用户友好的提示

    // 这里为了简单起见,仅做第三步
    ModelAndView mav = new ModelAndView();

    mav.addObject("msg", "年龄不能大于80");
    mav.addObject("ex", ex);

    mav.setViewName("ageError");

    return mav;
    }

    // 处理其他类型的未知异常
    // 后面不需要添加value,即去除掉前面匹配的异常方法之后,如果都没有匹配,那么就会到本方法中来处理该异常
    // 因此,如果@ExceptionHandler注解,没有value值的方法,只能有一个。
    @ExceptionHandler
    public ModelAndView doOtherException(Exception ex){
    // 处理NameException的异常

    // 异常发生处理逻辑:
    // 1. 需要把异常记录下来,记录到数据库,日志文件。记录日志发生的时间,哪个方法发生的,异常的错误内容等等。
    // 2. 发送通知,把异常的信息通过邮件,短信,微信等发送给相关人员。
    // 3. 给用户友好的提示

    // 这里为了简单起见,仅做第三步
    ModelAndView mav = new ModelAndView();

    mav.addObject("msg", "发生其他异常");
    mav.addObject("ex", ex);

    mav.setViewName("defaultError");

    return mav;
    }

    }

    注意,为了方便,尽量设置一个默认异常,用于处理所有未声明处理的异常类型。

  6. 创建处理异常的视图页面(用于向用户展示出现了异常)

    创建上述三类异常的视图页面(注意,已经配置了视图解析器)。

  7. 创建SpringMVC的配置文件

    1. 组件扫描器,扫描@Controller注解
    2. 组件扫描器,扫描@ControllerAdvice所在的包名
    3. 声明注解驱动
    1
    2
    3
    4
    5
    6
    <!-- 处理异常,需要的两步 -->
    <!-- 声明组件扫描器,扫描@ControllerAdvice类 -->
    <context:component-scan base-package="org.hianian.handler"></context:component-scan>

    <!-- 声明注解驱动 -->
    <mvc:annotation-driven></mvc:annotation-driven>

可以看到,我们只是抛出可能发生的异常(而以往,则是在可能发生异常的位置进行处理),然后由框架进行映射,找到处理异常的类和方法(全局统一异常处理)。使得业务逻辑代码和异常处理代码进行解耦合,从另一个角度来看,异常处理代码是作为一个切面来插入到业务逻辑代码中的。

4.3 拦截器

拦截器在框架中是一类比较重要的对象,这类对象需要实现框架中的一个接口(HandlerInterceptor),实现了这个接口的类对象都被称为拦截器。此前我们听过过滤器,监听器等等。过滤器是为了过滤请求参数,设置编码字符集等工作的;监听器是为了监听某个事件的发生来执行特定的动作,而拦截器则是为了拦截用户请求的,对请求做预先的判断处理操作。

拦截器是全局的,可以对多个Controller做拦截,一个项目中可以有0个、1个和多个拦截器,他们在一起来拦截用户的请求。拦截器常用在:用户登录处理,权限检查,记录日志等等

拦截器的使用步骤:

  1. 定义类,实现HandlerInterceptor接口,实现接口中的三个方法(三个方法表示三个执行时间点);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    // 该方法可以控制处理器方法是否被执行,return false表示处理器方发布会被执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
    1. preHandle()方法是预处理方法,即在处理请求之前执行的操作,即控制器方法执行之前执行。三个参数分别是请求对象、响应对象、控制器对象Controller。方法的返回值是布尔值。可以在这个方法中获取请求的信息,验证请求是否符合要求,验证用户是否登录等等。如果验证失败(return false),可以截断请求,请求不能被控制器方法处理;如果验证成功(return true),可以放行请求,此时控制器方法才能执行。

      当然,如果想要提示用户,也可利用request对象来进行转发到其他提示的资源页面。

    2. postHandle()方法是后处理方法,即在控制器方法执行之后执行的操作。参数分别是请求对象、响应对象、控制器对象Controller以及处理器方法的返回值ModelAndView对象。可以在这个方法中修改处理器方法的返回结果,即对处理器方法的执行结果进行二次修正。(了解)

    3. afterCompletion()方法是请求处理完成后方法,即视图处理完成后,即对视图执行了forward操作,已经将结果返回给了用户。参数分别是请求对象、响应对象、控制器对象以及程序中发生的异常对象Exception。这个方法是程序最后执行的方法,一般是进行资源回收操作,删除、清空内存。(了解)

  2. 在SpringMVC的配置文件中,声明拦截器,让框架知道拦截器的存在。

那拦截器什么时候起作用呢,其执行时间可以是:

  1. 在处理请求之前,也就是Controller类中的方法执行之前先被拦截;
  2. 在控制器方法执行之后,也会执行拦截器;(注意,控制器方法执行结束,并不代表请求处理完成了。
  3. 在请求处理完成后也会执行拦截器。

使用拦截器有如下步骤:

  1. 新建maven web项目

  2. 加入依赖

  3. 创建Controller类

  4. 创建一个普通类,作为拦截器使用

    1. 实现HandlerInteceptor接口
    2. 实现接口中的三个方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 自定义拦截器类,拦截用户的请求
    public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    System.out.println("拦截器MyInterceptor的preHandle方法");
    return true;

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    System.out.println("拦截器的MyInterceptor的postHandle方法");

    // 修改处理器方法返回的结果
    modelAndView.addObject("myname", "修改了处理器方法返回的结果");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    System.out.println("拦截器的MyInterceptor的afterCompletion方法");
    }
    }
  5. 创建show.jsp

  6. 创建SpringMVC的配置文件

    1. 组件扫描器,扫描@Controller注解

    2. 声明拦截器,并指定拦截的请求url地址(即指明拦截哪些请求)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <!-- 声明拦截器:拦截器可以有0个或多个 -->
      <mvc:interceptors>

      <!-- 声明一个拦截器 -->
      <mvc:interceptor>

      <!-- 声明拦截的请求uri地址,可以使用通配符 -->
      <mvc:mapping path="/**"/>

      <!-- 声明拦截器对象 -->
      <bean class="org.hianian.handler.MyInterceptor"/>
      </mvc:interceptor>
      </mvc:interceptors>

因此,添加拦截器的步骤比较简单,创建拦截器类,实现想要执行的方法,然后再配置文件中进行url和拦截器映射即可。感觉和异常处理类似,是作为切面功能加入到业务逻辑代码中的。实际上,preHandle方法是整个项目的入口,因为是先执行该方法的,只有该方法返回true,才会进而执行控制器中的方法。即拦截器可以看成是多个Controller中共用的功能,集中抽取出来统一处理。

springmvc_008.png (1159×474) (gitee.io)

springmvc_009.png (588×270) (gitee.io)

4.3.1 多个拦截器

可以声明多个拦截器,拦截器的执行顺序和声明顺序有关,先声明的拦截器,先执行。注意,这里相当于是栈,因为拦截器存在三个方法,分别在三个时间点执行。此时,又因为先声明的拦截器先执行。所以以拦截器A、B为例,A拦截器的preHandle()方法先执行,然后B拦截器的preHandle方法再执行。之后B拦截器的postHandle()方法执行,A拦截器的postHandle()方法执行。最后B拦截器的afterCompletion()方法执行,A拦截器的afterCompletion()方法执行。

如下所示:

springmvc_010.png (938×411) (gitee.io)

springmvc_011.png (633×631) (gitee.io)

4.3.2 拦截器和过滤器

二者有以下区别:

  1. 过滤器是Servlet规范中的对象,拦截器是框架中的对象;
  2. 过滤器实现了Filter接口,而拦截器实现了HandlerInterceptor
  3. 过滤器用来设置request,response的参数、属性的,侧重对数据过滤的;而拦截器则是用来验证请求的,能截断请求,以及对响应内容进行修改。
  4. 过滤器是在拦截器之前先执行的。
  5. 过滤器是Tomcat服务器创建的对象,拦截器是SpringMVC容器中创建的对象。
  6. 过滤器只有一个执行时间点,而拦截器有三个执行时间点。
  7. 过滤器可以处理jsp,js,html等等;而拦截器侧重拦截对Controller的请求(即只能处理被中央处理器接收的请求)

拦截器实现登录验证,验证登录的用户是否符合要求,符合要求则允许登录,否则,禁止登录。步骤如下所示:

  1. 新建maven项目

  2. 修改web.xml配置文件注册中央调度器

  3. 新建index.jsp发起请求

  4. 创建Controller处理请求

  5. 创建结果show.jsp

  6. 创建一个login.jsp,模拟登录(把用户的信息放入到session);创建logout.jsp,模拟退出系统(从session中删除用户信息)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    模拟登录,zhangsan登录系统
    <%
    session.setAttribute("name", "zhangsan");
    %>
    </body>
    </html>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    模拟退出,zhangsan退出系统
    <%
    session.removeAttribute("name");
    %>
    </body>
    </html>
  7. 创建拦截器,从session中获取用户的登录数据,验证能否访问系统。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 自定义拦截器类,拦截用户的请求
    public class MyInterceptor implements HandlerInterceptor {

    // 验证用户的登录信息,正确return true,否则return false
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    System.out.println("拦截器MyInterceptor的preHandle方法");

    String logName = "";

    Object name = request.getSession().getAttribute("name");

    if(name != null) {

    logName = (String) name;
    }

    if(!"zhangsan".equals(logName)) {

    request.getRequestDispatcher("/tips.jsp").forward(request, response);
    return false;
    }

    return true;

    }
    }
  8. 创建一个验证的jsp,如果验证视图,给出提示

  9. 创建springmvc配置文件,声明组件扫描器、声明拦截器。

5. 备注

参考B站《动力节点》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录