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 对象,我们只需要进行这样的步骤:
-
导入 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>
-
配置 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
// 利用 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 有三种处理异常的方式:
- 在一个 @Controller 类中使用 @ExceptionHandler 处理此类中的异常信息;
- 使用 Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver。
- 实现 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"/>