Spring MVC 复盘

概念

Spring MVC 中:

M(Model)      : 模型层,负责主要的业务逻辑,负责与数据库进行交互,向控制层提供服务;
V(View)       :视图层,主要负责与用户进行交互,向用户展示业务相关内容;
C(Controller) :控制层,主要负责处理用户提交的请求,调用模型层的服务,选定视图并将结果返回给用户;

重构《Spring MVC 复盘录前言》中的示例

接下来我们使用 Spring MVC 的相关知识来将上一篇中的示例一个一个实现。

知识铺垫

集权 Servlet

在 Spring MVC 中仅使用了一个 Servlet 来统一管理所有的请求和响应,称之为集权 Servlet。这个 Servlet 就是 org.springframework.web.servlet.DispatcherServlet,于是我们只需要在 web.xml 文件中配置这个 DispatcherServlet 即可:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
  <display-name>Archetype Created Web Application</display-name>
  
  <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:springmvc-servlet.xml</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>springmvc</servlet-name>

    <!--  标识这个 Servlet 能匹配的路径, “/” 表示所有路径,也就是说这个 Servlet 能够接管所有的请求 -->
    <!--  但并不意味着其余的 Servlet 就不能运行了,Servlet 匹配规则是从具体到通配 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

来看一下 DispatcherServlet 构造方法中声明的解释:

 * Create a new {@code DispatcherServlet} that will create its own internal web
 * application context based on defaults and values provided through servlet
 * init-params. Typically used in Servlet 2.5 or earlier environments, where the only
 * option for servlet registration is through {@code web.xml} which requires the use
 * of a no-arg constructor.
 * <p>Calling {@link #setContextConfigLocation} (init-param 'contextConfigLocation')
 * will dictate which XML files will be loaded by the
 * {@linkplain #DEFAULT_CONTEXT_CLASS default XmlWebApplicationContext}
 * <p>Calling {@link #setContextClass} (init-param 'contextClass') overrides the
 * default {@code XmlWebApplicationContext} and allows for specifying an alternative class,
 * such as {@code AnnotationConfigWebApplicationContext}.
 * <p>Calling {@link #setContextInitializerClasses} (init-param 'contextInitializerClasses')
 * indicates which {@code ApplicationContextInitializer} classes should be used to
 * further configure the internal application context prior to refresh().

大概意思是在 DispatcherServlet 实例化时会创建一个 ApplicationConotext,默认是 XmlWebApplicationContext 实现,但是也可以使用其他的 ApplicationConotext,比如说使用注解的 AnnotationConfigWebApplicationContext ,只需要在 web.xml 文件中指定 init-param 'contextClass',就像这样:

<init-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>

同时,还可以接受一个 init-param 'contextInitializerClasses' ,值可多个,以逗号分隔。

DispatcherServlet 作为一个 Servlet ,自然是由 WEB 容器负责实例化,而 DispatcherServlet 在实例化时会创建一个 ApplicationConotext,init-param 'contextConfigLocation' 和 init-param 'contextInitializerClasses' 由 ApplicationConotext 使用,以便创建 Spring 环境,前者是 Bean 定义所在文件,后者是 Spring 容器初始化时的初始化类。

因此在 WEB 容器启动后,就已经实例化了一个接管所有请求的集权 Servlet -- DispatcherServlet ,同时也搭建好了 Spring 容器环境。

视图解析器配置

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceViewResolver"/> <!--可以省略-->
    <!--前缀-->
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <!--后缀-->
    <property name="suffix" value=".jsp"/>  
 </bean>

上面那个常用(我们也将用这个),下面这个不常见:

<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <property name="prefix" value="fm_"/>
    <property name="suffix" value=".ftl"/>
</bean>

FreeMarkerViewResolver 除了需要声明外,还需要给 FreeMarkerViewResolver 设置一个 FreeMarkerConfig 的 bean 对象来定义 FreeMarker 的配置信息,代码如下:

<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
    <property name="templateLoaderPath" value="/WEB-INF/ftl" />
</bean>

InternalResourceViewResolver 能自动将返回的视图名称解析为 InternalResourceView 类型的对象。InternalResourceView 会把 Controller 处理器方法返回的模型(ModelAndView)属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 转发到目标 URL(prefix + viewName + suffix)。也就是说,使用 InternalResourceViewResolver 视图解析时,无需再单独指定 viewClass 属性。

package org.example.controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

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

public class LoginController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {

        ModelAndView modelAndView = new ModelAndView();
        // 添加了一个属性(键值对)
        modelAndView.addObject("Key", "An Impressive Value");

        // 设置返回的视图名称
        modelAndView.setViewName("login");

        return modelAndView;
    }
}

以此为例,那么首先 InternalResourceViewResolver 会将 ModelAndView 的属性 {"Key" : "An Impressive Value"}放到 request 的属性中去,然后重定向到 prefix + viewName + suffix = "/WEB-INF/jsp/" + "login" + ".jsp" = "/WEB-INF/jsp/login.jsp" 这个 url 去。

在 Spring MVC 框架中,不管是重定向或转发,都需要符合视图解析器的配置,如果直接转发到一个不需要 DispatcherServlet 的资源,例如那些静态资源,诸如图片和 html 页面等:

// 直接转发到静态资源 my.html 页面
return "forward:/html/my.html";

// 直接访问到静态资源
http://localhost:8080/html/my.html

由于 DispatcherServlet 接管了所有的请求,所以即使是对静态资源的访问也被其接管了,但是静态资源的处理并不需要 DispatcherServlet ,所以需要对其放行,怎么做呢?有三种方法:

  • 使用 mvc:resources 配置静态资源路径,以便 DispatcherServlet 能够处理静态资源:

    // 注意 WEB-INF 不属于静态资源所在目录
    <mvc:resources location="/html/" mapping="/html/**" />
    
  • 将其交给 WEB 容器中的默认 Servlet 处理(推荐使用这种方法):

        <!-- web.xml -->
        <servlet-mapping>
    
          <!--  这个是 WEB 容器默认的 Servlet,这里目的是让它来处理静态资源,事实上,凡是找不到匹配 Servlet 的都会交给它处理  -->
          <servlet-name>default</servlet-name>
          <url-pattern>/static/**</url-pattern>
        </servlet-mapping>
    
        <!-- 以下为 spring-mvc.xml -->
        <!-- 处理不了的请求就交给默认处理器的意思 -->
        <mvc:default-servlet-handler/>
    
        <!-- 配置了上面的内容,必须添加下面的内容以确保 Controller 不会失效 -->
        <mvc:annotation-driven/>
    
  • 修改 DispatcherServlet 的匹配路径,不让其接管所有的请求,只让它接管自己写的 Controller 方法:

      <servlet-mapping>
        <!-- DispatcherServlet 这个 Servlet 的名字 --> 
        <servlet-name>springmvc</servlet-name>
    
        <!--  只匹配处理 .do 结尾(什么结尾都可以,只要与静态资源区分开来即可)的请求,这样对于图片、html,css,js 等静态资源就不会被 Spring MVC 所处理 -->
        <url-pattern>*.do</url-pattern>
      </servlet-mapping>
    

如何集权

从这张这里盗过来的图上,我们能可以看出,DispatcherServlet 是作为整个架构的中心的核心部件,可以说举足轻重。做事要做全面,这图既来之,则安之,再将其过程引入:

SpringMVC 的执行流程如下:

1. 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
2. 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
8. ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
10. 视图负责将结果显示到浏览器(客户端)。

Controller 的变化发展

从上图中已经说明了 Controller 就是 Handler 处理器,它也是要由 Spring 容器所管理的,它是一个功能接口:

@FunctionalInterface
public interface Controller {

    // 处理请求的逻辑,并返回 ModelAndView (保存有多个属性和一个视图名)
	@Nullable
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

从前如果要编写一个 Controller,必须继承这个接口,像这样:

package org.example.controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

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

public class LoginController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("Key", "An Impressive Value");
        modelAndView.setViewName("login");

        return modelAndView;
    }
}

但可以看到,一个 Controller 接口的实现类只能处理一个请求,但有时我们经常会遇到这样的情况,比方说对一本书的增删改查,一般而言会对应四个 URL,这意味着什么?我们要写四个 Controller 的子类,但它们明显都是属于对同一资源的操作,而且分布过于宽松,不利于维护,也不符合高内聚松耦合的原则。

于是就出现了 @Controller 注解及配套的 @RequestMapping 、 @RequestParam 和 @PathVariable,于是画风变成这样:

package org.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

// 图书的增删改查
@Controller
@RequestMapping("/book")
public class BookController {

    // 增
    @RequestMapping(path = "/{bookName}", method = RequestMethod.POST)
    public String add(@PathVariable String bookName){

        return "ok";
    }

    // 删
    @RequestMapping(path = "/{bookName}", method = RequestMethod.DELETE)
    public String delete(@PathVariable String bookName){

        return "ok";
    }

    // 改
    @RequestMapping(path = "/{bookName}", method = RequestMethod.PUT)
    public String update(@PathVariable String bookName, @RequestParam String newBookName){

        return "ok";
    }

    // 查
    @RequestMapping(path = "/{bookName}", method = RequestMethod.GET)
    public String find(@PathVariable String bookName){

        return "ok";
    }
}

这里不再赘述这些注解的使用方法,欲知详情者可自行查找。这里要说的是,被 @Controller 注解标注的类中的每一个方法都是 Controller,@RequestMapping 表示这个 Controller 的映射信息。既然是用方法来表示 Controller ,那么想必方法的各个组成部分都是有意义的,首先,方法名没有意义,方法参数可以是请求参数的映射,除此之外还可以是这些值: Servlet API 参数类型、Model类型,还有输入输出流、表单实体类、注解类型、与 Spring 框架相关的类型等,传参方法可以有很多种:

  • 适用于 GET 和 POST 的通过实体 Bean 接收请求参数
  • 适用于 GET 和 POST 的通过处理方法的形参接收请求参数
  • 适用于 GET 和 POST 的通过 HttpServletRequest 接收请求参数
  • 适用于 GET 和 POST 的通过 @RequestParam 接收请求参数
  • 通过 @PathVariable 接收 URL 中的请求参数
  • 通过 @ModelAttribute 接收请求参数

而方法返回值可以有这些类型:

  • ModelAndView
  • Model
  • 包含模型属性的 Map
  • View
  • 代表逻辑视图名的 String
  • void
  • 其它任意Java类型

介绍完这些之后,不知道有没有小伙伴有疑问,之前的配置文件都是 xml 文件,这突然多出一个注解来又没有在哪里声明了它,它怎么会被 Spring 加载进来?要知道世间万物都是事物普遍联系中一环或者一个成分,Spring 这么抽象的东西,尽管无色无味,它也是物质,既然是物质,必然会产生联系,而 @Controller 之类的注解是这样与 Spring 产生联系的,在 Spring 的配置文件中加入这样一句声明:

<?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
       http://www.springframework.org/schema/context/sping-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/sping-mvc.xsd">

    <!--  @Controller 注解标记类所在包  -->
    <context:component-scan base-package="org.example.controller"/>

    <!--  静态资源所在位置,这里并不需要指定,做个标记而已  -->
    <mvc:resources mapping="/static/**" location="/static/"/>
</beans>

类型转换器

顾名思义,就是将一个类型的数据转变为另一个类型的数据,比如说,将 String 类型的数据转变为数组类型,Spring MVC 框架也提供了许多不同的类型转换器,但最主要的还是 String 类型与其余类型的互转,为什么呢?因为对于一个请求及其响应来说,无疑最终都是 String 类型,数据从一个请求中出来时是 String 类型,经过处理之后,返回给用户的响应体也是 String 类型,但是在中间的处理时,数据不可能一直是 String 类型,所以就需要 String - Object -String 这样往返变化。

所有的类型转换器都需要实现下面这个接口:

package org.springframework.core.convert.converter;

import org.springframework.lang.Nullable;

@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}

尽管 Spring MVC 框架已经内置了不少的类型转化器,但那都是通用性的,往往无法全面覆盖,因此对于特殊的类型转化,比如将 String 转化为自己项目中的某一个特殊数据,假设是要将格式为 "1,Spring 从入门到..." 的字符串转化为一个 Book 实例对象:

public class Book{

    private String bookId;

    private String bookName;

    // 省略 getter 和 setter 方法
    ...
}

那这时应该怎么办?一个办法是自己在 Controller 里面进行处理,但是这样耦合度难免会有所提高,也没有充分发挥框架的作用;另一个办法自然是利用框架所提供的自动转换功能了,怎么做呢?

  • 首先,定义自己的类型转换器:

      package org.example.convert;
    
      import org.example.bean.Book;
      import org.springframework.core.convert.converter.Converter;
    
    
      public class StringToBookConvert implements Converter<String, Book> {
    
          @Override
          public Book convert(String source) {
    
              Book book = new Book();
    
              String[] types = source.split(",");
    
              if (types.length <= 1){
                  throw new IllegalArgumentException("类型转化失败:格式错误");
              }
    
              book.setBookId(types[0]);
              book.setBookName(types[1]);
              return book;
          }
      }
    
  • 然后,将这个类型转化器注册到 Spring MVC 中:

      <mvc:annotation-driven conversion-service="conversionService"/>
      <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
          <property name="converters">
              <set>
                  <bean class="org.example.convert.StringToBookConvert"/>
              </set>
          </property>
      </bean>
    
  • 最后,编写代码测试

      package org.example.controller;
    
      import org.example.bean.Book;
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.ModelAttribute;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestParam;
    
      @Controller
      public class TestConvertController {
    
          @RequestMapping("/register")
          @ModelAttribute
          public Book test(@RequestParam String name, String id, @RequestParam Book book, 
                           @ModelAttribute Book b){
              // convert 从 String 转化为 book 的,请求参数: book=1,Spring从入门精通
              System.out.println(book);
              
              // @ModelAttribute 处理的传入的参数,要求请求参数与属性名一样,请求参数:bookName=SpringMVC从入门到精通&bookId=100
              System.out.println(b);
              
              Book newBook = new Book();
              newBook.setBookName(name);
              newBook.setBookId(id);
    
              // 这个最新实例化的是要传给 view 的
              System.out.println(newBook);
              return newBook;
          }
      }
    

    编写 register.jsp 文件:

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <html>
      <head>
          <title>注册</title>
      </head>
      <body>
      注册页面${book}
      </body>
      </html>
    

    启动WEB容器之后,在地址栏输入测试路径:

      http://localhost:xxxx/register?book=1,Spring从入门到精通&bookName=Spring MVC从入门到精通&bookId=100&name=Spring Boot从入门到精通&id=999
    

    运行结果:

      控制台:
      Book{bookName='Spring从入门到精通', bookId='1'}
      Book{bookName='Spring MVC从入门到精通', bookId='100'}
      Book{bookName='Spring Boot从入门到精通', bookId='999'}
    
      视图:
    

在这个示例中,不仅展示了类型转换器的使用,而且需要注意这几点:

  • Controller (Handle 处理器)必须要返回一个视图,除非显式使用 @ResponseBody 注解,但此例中没有使用此注解,因此必须要返回一个视图,但是它的返回类型不是 String ,意味着不能通过返回值指定视图,那么这个时候,@RequestMapping 上的 path/value 就是默认的指定视图,所以说这个 register 不仅是一个 Servlet 同时也会被视图解析器所解析,最终会给用户返回 /WEB-INF/jsp/register.jsp 视图;
  • @ModelAttribute 这个注解也有奇效,它可以将标注的参数或者返回值保存到 Model 属性中,进而保存到 request 属性中,而由于请求转发到视图,故视图会持有这个 request,进而实现了值传递。我们知道第一个 Book book 是通过类型转换器得来的,那么第二个 Book b 哪里来的?是因为只要请求参数中的 Key 与 Book 中的成员属性名一致就可以自动将其封装到 Book 实例中去,那加了 @ModelAttribute 后,这个值就可以被传递到视图中去,默认 Key = 返回值类型(首字母小写),而前者不行。还有一个点是与 @ResponseBody 注解有关的,我们知道加了这个注解本意上是想不给用户返回视图,但是一旦有 @ModelAttribute 这个注解在方法上,就必须要指定视图,因为方法的返回值都会变成它的属性,默认也是 Key = 返回值类型(首字母小写),值就是返回值。另外,@ModelAttribute 注解的键是不重复不覆盖的,@ModelAttribute 注解标注的参数或返回值最终会注入到请求中,成为请求的属性。
  • @ResponseBody ,如果我们不想要返回任何视图给用户,我们只返回有效数据,从而实现前后端分离,注意视图其实更倾向于前端,但又不全是,因为视图中存在某些与后台相关的值,但自从有了 Ajax 等技术后,前端就能够自行请求数据并处理,不用再靠视图解析器来进行中转,那么此时,视图是可以被省略的,也就是我可以不再指定视图返回给用户,但 Controller (Handle 处理器)又必须要返回一个视图,这就很矛盾了,这个时候加上 @ResponseBody 注解,就可以不用返回视图而可以直接返回任何数据,包括 Java 对象实例,但是又不能直接返回 Java 对象,因为最终都要返回字符串(这时就需要使用类型转换器了),下面这个可以将任意 Java 对象转变为 JSON 对象,我们只需要进行这样的步骤:
    1. 导入 JSON 依赖包,有很多家的,这里随便找了一家的:

       <dependency>
           <groupId>com.fasterxml.jackson.core</groupId>
           <artifactId>jackson-databind</artifactId>
           <version>2.9.8</version>
       </dependency>
      
       <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
       <dependency>
           <groupId>com.fasterxml.jackson.core</groupId>
           <artifactId>jackson-core</artifactId>
           <version>2.9.8</version>
       </dependency>
      
    2. 配置 Spring MVC配置文件

       <mvc:annotation-driven conversion-service="conversionService">
           <mvc:message-converters>
               <!--      解决响应流乱码      -->
               <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                   <property name="defaultCharset" value="UTF-8"/>
               </bean>
      
               <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                   <property name="objectMapper">
                       <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
                           <property name="failOnEmptyBeans" value="false"/>
                       </bean>
                   </property>
               </bean>
           </mvc:message-converters>
       </mvc:annotation-driven>
      

当然了,如果不想转化为 JSON 格式的数据,可以使用其他的 HttpMessageConverter ,只需要更改 MappingJackson2HttpMessageConverter 为相应的 bean 即可,比如改成 ObjectToStringHttpMessageConverter ,这样就允许你使用自己写的类型转换器或者格式化转换器:

        <mvc:annotation-driven conversion-service="conversionService">
            <mvc:message-converters>

                <bean class="org.springframework.http.converter.ObjectToStringHttpMessageConverter">
                    <constructor-arg name="conversionService" ref="conversionService"/>
                    <constructor-arg name="defaultCharset" value="UTF-8"/>
                </bean>
            </mvc:message-converters>
        </mvc:annotation-driven>

与类型转化器有类似功能的,还有一个 org.springframework.format.Formatter ,它是将字符串转化为其他类型,再将其他类型转化为字符串,也就是一个字符串类型与其余各种类型之间的互转,而类型转换器是多对多的,它可以将任意类型进行互换,Formatter 接口主要有以下两个方法,简单了解一下即可:

// 利用 locale 将字符串转为其他类型 
public T parse(String s, java.util.Locale locale)

// 利用 locale 将其他类型转为字符串
public String print(T object, java.util.Locale locale)

比如改写 StringToBookConvert :

package org.example.convert;

import org.example.bean.Book;
import org.springframework.format.Formatter;

import java.text.ParseException;
import java.util.Locale;

public class BookFormatter implements Formatter<Book> {

    @Override
    public Book parse(String text, Locale locale) throws ParseException {
        Book book = new Book();

        String[] types = text.split(",");

        if (types.length <= 1){
            throw new IllegalArgumentException("类型转化失败:格式错误");
        }

        book.setBookId(types[0]);
        book.setBookName(types[1]);
        return book;
    }

    @Override
    public String print(Book object, Locale locale) {
        return object.toString();
    }
}

此时,conversionService 就要这样写:

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="org.example.convert.BookFormatter"/>
        </set>
    </property>
</bean>

拦截器

拦截器与过滤器并不是同一个东西,过滤器属于 WEB 容器的范畴,而拦截器是 Spring MVC 框架所提供的机制,拦截器和过滤器(doFilter 链执行完后会递归返回,所以也能对响应进行处理)都能用于对请求和响应的预处理和后处理。

实现一个拦截器有两种方法:

  • 通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类(例如 HandlerInterceptorAdapter)来定义;
  • 通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义。

下面我们来看 HandlerInterceptor 接口:

        public interface HandlerInterceptor {

                // 在方法执行前调用 相当于 @Before
        	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        			throws Exception {

        		return true;
        	}

                // 在方法执行后调用,相当于 @AfterReturing
        	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        			@Nullable ModelAndView modelAndView) throws Exception {
        	}

                // 在方法完成后调用,相当于 @After
        	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
        			@Nullable Exception ex) throws Exception {
        	}

        }

那如何定义一个拦截器呢?

  • 首先,创建一个拦截器:

          package org.example.interceptor;
    
          import org.springframework.web.servlet.HandlerInterceptor;
          import org.springframework.web.servlet.ModelAndView;
    
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
    
          public class MyInterceptor implements HandlerInterceptor {
    
              @Override
              public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                  System.out.println("请求方法即将执行");
    
                  return true;
              }
    
              @Override
              public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
                  System.out.println("这个请求方法已经执行完了,准备去解析视图");
              }
    
              @Override
              public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                  System.out.println("视图渲染结束了");
              }
          }
    
  • 然后,将此拦截器注册进 Spring MVC 中:

          <mvc:interceptors>
              <mvc:interceptor>
                  <!--     拦截器匹配路径,匹配成功的都拦截       -->
                  <mvc:mapping path="/**"/>
    
                  <!--     拦截器不匹配路径,匹配成功的都不拦截       -->
                  <mvc:exclude-mapping path="/register"/>
                  
                  <!--     拦截器       -->
                  <bean class="org.example.interceptor.MyInterceptor"/>
              </mvc:interceptor>
          </mvc:interceptors>
    

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

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

异常处理

Spring MVC 有三种处理异常的方式:

  1. 在一个 @Controller 类中使用 @ExceptionHandler 处理此类中的异常信息;
  2. 使用 Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver。
  3. 实现 Spring 的异常处理接口 HandlerExceptionResolver,自定义自己的异常处理器。

第一种是属于局部的异常处理,而第二三种则属于全局的异常处理。下面分别来简单看看这三种方式:

  • @ExceptionHandler 局部异常处理:

          /**
           * 可以指定多个可以捕获的异常类型
           *
           * @param e 异常信息,除此之外不要再添加其他参数
           * @return 与 @RequestMapping 所修饰的方法返回值一样
           */
          @ExceptionHandler({ArithmeticException.class, NullPointerException.class})
          @ResponseBody
          public String testArithmeticException(Exception e, HttpServletRequest request) {
              System.out.println("打印错误信息: " + e);
    
              // 如没有 @ResponseBody 则跳转到指定页面
              return "出错了";
          }
    
  • SimpleMappingExceptionResolver

          <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
              <!-- 定义默认的异常处理页面,当该异常类型注册时使用 -->
              <property name="defaultErrorView" value="error"/>
              
              <!-- 定义异常处理页面用来获取异常信息的变量名,默认名为exception -->
              <property name="exceptionAttribute" value="ex"/>
              
              <!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常页名作为值 -->
              <property name="exceptionMappings">
                  <props>
                      <prop key="ArithmeticException">error</prop>
                      <!-- 在这里还可以继续扩展对不同异常类型的处理 -->
                  </props>
              </property>
          </bean>
    
  • 实现 HandlerExceptionResolver 接口

          package org.example.ex;
    
          import org.springframework.web.servlet.HandlerExceptionResolver;
          import org.springframework.web.servlet.ModelAndView;
    
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
    
          public class MyExceptionHandler implements HandlerExceptionResolver {
              @Override
              public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
                                                   Object handler, Exception ex) {
                  return new ModelAndView("error");
              }
          }
    

    再在 spring-mvc.xml 文件中添加:

          <!--  注册全局异常处理器  -->
          <bean class="org.example.ex.MyExceptionHandler"/>
    
posted @ 2022-03-30 23:59  lizhpn  阅读(36)  评论(0编辑  收藏  举报