Ⅰ. 拦截器
一、快速入门
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求 ,在指定方法前后,根据业务需要执行预先设定的代码 。
也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行。也可以在用户请求前阻止其执行。
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断
Session中是否有登录用户的信息。如果有就可以放行,如果没有就进行拦截。
比如我们去银行办理业务,在办理业务前后,就可以加一些拦截操作:
办理业务之前,先取号,如果带身份证了就取号成功;
业务办理结束,给业务办理人员的服务进行评价。
这些就是"拦截器"做的工作。
拦截器的使用步骤分为两步:
-
定义拦截器
-
注册配置拦截器
定义拦截器: 实现 HandlerInterceptor接口 , 并重写其指定方法
@Slf4j
@Component
public class LoginInterceptor **implements HandlerInterceptor** {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行..");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
}
}SpringMVC 的拦截器有 三个回调方法 :
-
preHandle-
在 Controller 方法执行 之前 调用。
-
返回
true→ 继续往下走;返回false→ 拦截请求,不会再进入 Controller。
-
-
postHandle-
在 Controller 方法执行 之后 ,但是 视图渲染之前 调用。
-
如果 Controller 抛了异常,这里不会执行。
-
-
afterCompletion-
在整个请求执行完成以后调用。
-
不管 Controller 正常执行还是抛了异常,都会执行 (类似 Java 的
finally)。 -
可以在这里做收尾清理,比如日志打印、资源释放、
ThreadLocal.remove()。 -
👉 请求已经处理完了,响应也准备好返回客户端了 ,这时候 Spring 才会触发
afterCompletion。
-
注册配置拦截器: 实现 WebMvcConfigurer接口 , 并重写 addInterceptors()方法
@Configuration
public class WebConfig **implements WebMvcConfigurer** {
// 注入自定义的拦截器对象
**@Autowired**
** private LoginInterceptor loginInterceptor;**
**@Override**
** public void addInterceptors(InterceptorRegistry registry)** {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**"); // 设置拦截器拦截的请求路径,其中 /** 表示拦截所有请求
}
}启动服务,试试访问任意请求,观察后端日志。

可以看到 preHandle 方法执行之后就放行了,开始执行目标方法,目标方法执行完成之后执行 postHandle 和 afterCompletion 方法。
我们把拦截器中 preHandle 方法的返回值改为 false,再观察运行结果。

可以看到,拦截器拦截了请求,没有进行响应。
二、拦截器详解
拦截器的入门程序完成之后,接下来我们来介绍拦截器的使用细节。拦截器的使用细节我们主要介绍两个部分:
-
拦截器的拦截路径配置
-
拦截器实现原理
1. 拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效。
在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求;也可以通过excludePathPatterns() 指定不拦截哪些请求,而 /** 表示拦截所有的请求。
比如用户登录校验,我们希望可以对除了登录之外所有的路径生效。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 设置拦截器拦截的请求路径(/**表示拦截所有请求)
.excludePathPatterns("/user/login") // 设置拦截器排除拦截的路径
.excludePathPatterns("/**/*.js") // 排除前端静态资源
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
}
}下面是常见拦截路径设置:
| 拦截路径 | 含义 | 举例 |
|---|---|---|
/* |
一级路径 | 能匹配 /user、/book、/login,但不能匹配二级目录及更深的路径,比如 /user/login 访问不到 |
/** |
任意级路径 | 能匹配 /user、/user/login、/user/reg 所有路径 |
/book/* |
/book 下的一级路径 |
能匹配 /book/addBook,不能匹配 /book/addBook/1、/book |
/book/** |
/book 下的任意级路径 |
能匹配 /book、/book/addBook、/book/addBook/2,不能匹配 /user/login |
以上拦截规则可以拦截此项目中的使用URL,包括静态文件(图片文件,JS 和 CSS 等文件)
2. 拦截器执行流程
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图:

-
添加拦截器后,执行
Controller的方法之前,请求会先被拦截器拦截住。执行preHandle()方法,这个方法需要返回一个布尔类型的值。如果返回true,就表示放行本次操作,继续访问controller中的方法。如果返回false,则不会放行(controller中的方法也不会执行) -
controller当中的方法执行完毕后,再回过来执行postHandle()这个方法以及afterCompletion()方法,执行完毕之后,最终给浏览器响应数据。
3. DispatcherServlet源码分析(了解)
观察服务启动日志:

当 Tomcat启动之后,有一个核心的类 DispatcherServlet,它来控制程序的执行顺序 。所有请求都会先进到 DispatcherServlet,执行 doDispatch() 调度方法。
如果有拦截器,会先执行拦截器 preHandle() 方法的代码,如果 preHandle() 返回 true,继续访问 controller 中的方法。controller 当中的方法执行完毕后,再回过来执行 postHandle() 和 afterCompletion(),返回给 DispatcherServlet,最终给浏览器响应数据。

① 初始化(了解)
DispatcherServlet 的初始化方法 init() 在其父类 HttpServletBean 中实现的。
主要作用是加载 web.xml中 DispatcherServlet的配置,并调用子类的初始化 。
init() 具体代码如下:
@Override
public final void init() throws ServletException {
try {
// ServletConfigPropertyValues 是静态内部类,使用 ServletConfig 获取 web.xml 中配置的参数
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
// 使用 BeanWrapper 来构造 DispatcherServlet
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException ex) {}
// 让子类实现的方法,这种在父类定义在子类实现的方式叫做模版方法模式
**initServletBean()** ;
}在 HttpServletBean 的 init() 中调用了 initServletBean(),它是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器,并加载 SpringMVC 配置文件中定义的 Bean 到该容器中,最后将该容器添加到 ServletContext 中。
下面是 initServletBean() 的具体代码:
/**
* Overridden method of {@linkHttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
// 创建ApplicationContext容器
this.webApplicationContext = **initWebApplicationContext()** ;
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}此处打印的日志,也正是控制台打印出来的日志。

初始化 web 容器的过程中,会通过 onRefresh 来初始化 SpringMVC 的容器:
protected WebApplicationContext initWebApplicationContext() {
//...
if (!this.refreshEventReceived) {
//初始化Spring MVC
synchronized (this.onRefreshMonitor) {
**onRefresh(wac)** ;
}
}
return wac;
}
@Override
protected void onRefresh(ApplicationContext context) {
**initStrategies(context)** ;
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}在 initStrategies() 中进行九大组件的初始化,如果没有配置相应的组件,就使用默认定义的组件。
-
初始化文件上传解析器
MultipartResolver -
初始化区域解析器
LocaleResolver -
初始化主题解析器
ThemeResolver -
初始化处理器映射器
HandlerMappings:-
通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;
-
缓存每个请求地址
URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
-
-
初始化处理器适配器
HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默认SimpleControllerHandlerAdapter作为处理器适配器 -
初始化异常处理器解析器
HandlerExceptionResolver -
初始化
RequestToViewNameTranslator:其作用是从Request中获取viewName -
初始化视图解析器
ViewResolvers -
初始化
FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个URL重定向到另一个URL时的参数信息)
② 处理请求(核心)
DispatcherServlet接收到请求后,执行 doDispatch调度方法,再将请求转给 Controller。
下面是 doDispatch 方法的具体实现:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 1. 获取执行链
// 遍历所有的 HandlerMapping 找到与请求对应的 Handler
mappedHandler = this.**getHandler** (processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 2. 获取适配器
// 遍历所有的 HandlerAdapter,找到可以处理该 Handler 的 HandlerAdapter
HandlerAdapter ha = this.**getHandlerAdapter** (mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
// 3. 执行拦截器 preHandle 方法
if (!mappedHandler.**applyPreHandle** (processedRequest, response)) {
return;
}
// 4. 执行目标方法
mv = ha.**handle** (processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.**applyDefaultViewName** (processedRequest, mv);
// 5. 执行拦截器 postHandle 方法
mappedHandler.**applyPostHandle** (processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// 6. 处理视图, 处理之后执行拦截器 afterCompletion 方法
this.**processDispatchResult** (processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
// 7. 执行拦截器 afterCompletion 方法
this.**triggerAfterCompletion** (processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
HandlerAdapter主要用于支持不同类型的处理器 ,如Controller、HttpRequestHandler或者Servlet等,让它们能够适配统一的请求处理流程。这样,SpringMVC可以通过一个统一的接口来处理来自各种处理器的请求。
从上述源码可以看出在开始执行 Controller 之前,会先调用预处理方法 applyPreHandle,而 applyPreHandle 方法的实现源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项目中使用的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor,并执行拦截器中的 preHandle 方法,这样就会咱们前面定义的拦截器对应上了,如下图所示:

-
如果拦截器返回
true,整个发放就返回true,继续执行后续逻辑处理; -
如果拦截器返回
fasle,则中断后续操作。
Ⅱ. 统一数据返回格式
统一返回格式:后端接口不论成功或失败,都返回一个结构一致的 JSON 格式响应,让前端不必逐个接口判断数据结构,提升开发效率、减少出错。
统一的数据返回格式使用注解 @ControllerAdvice 和实现 ResponseBodyAdvice 接口的方式实现。
@ControllerAdvice:表示控制器通知类
@Slf4j
**@ControllerAdvice**
public class ResponseAdvice **implements ResponseBodyAdvice** {
private static ObjectMapper mapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
**@SneakyThrows**
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 防止包装两次
if (body instanceof Result){
return body;
}
// 如果返回结果为String类型, 使用SpringBoot内置提供的Jackson来实现信息的序列化
if (body instanceof String){
return mapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}-
supports():判断是否要执行beforeBodyWrite()方法。true为执行,false不执行。通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理。 -
beforeBodyWrite():对response方法进行具体操作处理。
需要对 String 特别处理的原因分析:因为 SpringMVC 中返回类型为 String 时,会使用 StringHttpMessageConverter,它不会走对象的 JSON 序列化流程,但是返回类型仍然是 JSON,所以一返回时就会出错。
SpringMVC默认会注册一些自带的HttpMessageConverter(从先后顺序排列分别为ByteArrayHttpMessageConverter,StringHttpMessageConverter,SourceHttpMessageConverter,SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter),如下所示:public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { //... public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } //... }其中
AllEncompassingFormHttpMessageConverter会根据项目依赖情况添加对应的HttpMessageConverter:public AllEncompassingFormHttpMessageConverter() { if (!shouldIgnoreXml) { try { addPartConverter(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } if (jaxb2Present && !jackson2XmlPresent) { addPartConverter(new Jaxb2RootElementHttpMessageConverter()); } } if (kotlinSerializationJsonPresent) { addPartConverter(new KotlinSerializationJsonHttpMessageConverter()); } if (jackson2Present) { addPartConverter(new MappingJackson2HttpMessageConverter()); } else if (gsonPresent) { addPartConverter(new GsonHttpMessageConverter()); } else if (jsonbPresent) { addPartConverter(new JsonbHttpMessageConverter()); } if (jackson2XmlPresent && !shouldIgnoreXml) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); } if (jackson2SmilePresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } }在依赖中引入
jackson包后,容器会把MappingJackson2HttpMessageConverter自动注册到messageConverters链的末尾。
Spring会根据返回的数据类型,从messageConverters**链选择合适的 **HttpMessageConverter。当返回的数据是非字符串 时,使用的
MappingJackson2HttpMessageConverter写入返回对象。当返回的数据是字符串 时,
StringHttpMessageConverter会先被遍历到,这时会认为StringHttpMessageConverter可以使用。public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { //...代码省略 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { //...代码省略 if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { //getAdvice().beforeBodyWrite 执行之后, body转换成了Result类型的结果 body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { //此时cover为StringHttpMessageConverter ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } } //...代码省略 } //...代码省略 }在
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage)的处理中,调用父类的write方法。由于
StringHttpMessageConverter重写了addDefaultHeaders方法,所以会执行子类的方法。然而子类
StringHttpMessageConverter的addDefaultHeaders方法定义接收参数为String,此时泛型t为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常。
Ⅲ. 统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的。
@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件。
具体代码如下:
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
**@ResponseBody**
**@ControllerAdvice**
public class ErrorAdvice {
**@ExceptionHandler**
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
}类名,方法名和返回值可以自定义,重要的是注解。
接口返回为数据时,需要加
@ResponseBody注解 。
我们可以针对不同的异常,返回不同的结果:
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发生NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发生ArithmeticException:"+e.getMessage());
}
}