在我们的日常开发工作中,
Filter
(过滤器)、
Interceptor
(拦截器)和
AOP
(面向切面编程)是非常常用的 3 种请求处理技术。在不同的应用场景中,使用它们都可以在不影响主业务逻辑的前提下为系统增加额外的功能。面试官去问这个问题的时候,一般是想考察求职者的技术深度和对框架机制的理解。本篇我们从 3 者的基本概念及使用来分析解答下这道面试题。
Filter
什么是 Filter
Filter
是 Java
Servlet
规范的一部分,定义在
javax.servlet
包中,
Filter
可以对
Servlet
容器的所有 HTTP 请求(
HttpServletRequest
)和响应(
HttpServletResponse
)进行预处理或后处理操作。例如,在请求到达目标资源之前执行身份验证或设置字符编码,或者在响应返回给客户端前修改其响应内容格式。
Filter 接口
package
javax.servlet;
import
java.io.IOException;
public
interface
Filter
{
default
public
void
init
(FilterConfig filterConfig)
throws
ServletException
{}
public
void
doFilter
(ServletRequest request, ServletResponse response, FilterChain chain)
throws
IOException, ServletException
;
default
public
void
destroy
()
{}
}
项目中自定义过滤器需实现该接口,接口中的 3 个方法就是
Filter
的整个生命周期。
init()
-初始化、
doFilter()
-执行过滤逻辑、
destroy()
-销毁。
init方法
:Web 容器在启动时,会触发每个Filter
实例的 init 方法调用并传递一个FilterConfig
对象,该配置允许过滤器获取初始化参数以及ServletContext
上下文对象,从而加载任何所需的资源。该方法在Filter
的整个生命周期中仅会在初始化时被调用一次。
该方法如果抛出异常,Web 容器就会认为这个过滤器无法正常工作,因此不会将它加入到过滤器链中,无法提供后续的请求过滤工作。
doFilter方法
:该方法为Filter
的核心工作方法,每一次请求都会调用该方法。
FilterChain
接口参数由具体的 Servlet 容器实现并提供。每个过滤器的 doFilter 方法都会接收一个FilterChain
对象作为参数。在这个方法内部,过滤器可以选择:
- 直接处理请求/响应。
- 调用
chain.doFilter(request, response)
将请求传递给下一个过滤器或目标资源。
destroy方法
:Web 容器在销毁时,会触发每个Filter
实例的 destroy 方法调用,清理过滤器所有持有的资源(如内存、文件句柄、线程等)。该方法在Filter
的整个生命周期中也仅会执行一次。
Filter 的配置使用
在
SpringBoot
项目中可以使用如下几种配置方式:
- 使用
@WebFilter
注解 +@ServletComponentScan
注解
在过滤器类上使用
@WebFilter
注解来定义 URL 模式和其他属性
package
com.example.filter;
@WebFilter
(urlPatterns =
"/*"
, filterName =
"exampleFilter"
)
public
class
ExampleFilter
implements
Filter
{
@Override
public
void
init
(FilterConfig filterConfig)
throws
ServletException
{
// 初始化逻辑...
}
@Override
public
void
doFilter
(ServletRequest request, ServletResponse response, FilterChain chain)
throws
IOException, ServletException
{
// 传递给下一个过滤器或目标资源
chain.doFilter(request, response);
}
@Override
public
void
destroy
()
{
// 清理资源...
}
}
在启动类或者任意配置类上加上
@ServletComponentScan
注解来让 Spring Boot 自动扫描并注册这些过滤器。
@SpringBootApplication
@ServletComponentScan
(basePackages =
"com.example.filter"
)
// 指定扫描包路径
public
class
MyApplication
{
public
static
void
main
(String[] args)
{
SpringApplication.run(MyApplication
.
class
,
args
)
;
}
}
- 使用
FilterRegistrationBean
进行注册
@Configuration
public
class
FilterConfig
{
@Bean
public
FilterRegistrationBean<ExampleFilter>
customFilterRegistration
()
{
FilterRegistrationBean<ExampleFilter> registrationBean =
new
FilterRegistrationBean<>();
ExampleFilter customFilter =
new
ExampleFilter();
registrationBean.setFilter(customFilter);
registrationBean.addUrlPatterns(
"/*"
);
registrationBean.setOrder(
1
);
// 设置过滤器的执行顺序
// 添加初始化参数
registrationBean.addInitParameter(
"encoding"
,
"UTF-8"
);
return
registrationBean;
}
}
- 直接使用
@Component
注解:这种方式可以让 Spring 自动将过滤器组件化,默认会应用到所有请求路径。
@Component
public
class
ExampleFilter
implements
Filter
{
@Override
public
void
init
(FilterConfig filterConfig)
throws
ServletException
{}
@Override
public
void
doFilter
(ServletRequest request, ServletResponse response, FilterChain chain)
throws
IOException, ServletException
{
chain.doFilter(request, response);
}
@Override
public
void
destroy
()
{}
}
Interceptor
什么是 Interceptor
Interceptor
是 Spring MVC 框架的一部分,是位于
org.springframework.web.servlet
包中的
HandlerInterceptor
接口,用于在请求处理之前或之后执行特定逻辑。与
Filter
不同的是,
Interceptor
不依赖于
Servlet
容器,它是 Spring 框架独有的。
HandlerInterceptor 接口
/**
* A HandlerInterceptor gets called before the appropriate HandlerAdapter triggers the execution of the handler itself.
* This mechanism can be used for a large field of preprocessing aspects, e.g. for authorization checks,
* or common handler behavior like locale or theme changes.
* Its main purpose is to allow for factoring out repetitive handler code.
*/
public
interface
HandlerInterceptor
{
default
boolean
preHandle
(HttpServletRequest request, HttpServletResponse response, Object handler)
throws
Exception
{
return
true
;
}
default
void
postHandle
(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView)
throws
Exception
{
}
default
void
afterCompletion
(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex)
throws
Exception
{
}
}
接口注释意思大致是说,
HandlerInterceptor
是在通过
HandlerAdapter
执行查找到的
handler
之前被调用,这种机制主要目的是为了减少重复代码,用于大量的程序预处理工作,比如授权检查等。
preHandle方法
:在controller
方法调用之前,按照Interceptor
链 顺序执行 ,进行权限检查等请求前处理操作。
如果该方法返回了
false
,那么不仅当前Interceptor
会终止执行,整个拦截器链都会被终止。
postHandle方法
:在controller
方法调用之后返回ModelAndView
之前执行,与preHandle
不同的是,postHandle
是按照Interceptor
链 逆序执行 的。afterCompletion方法
:在整个请求完成后调用,通常用于资源清理或日志记录。
执行顺序:
下面我们通过 Spring MVC 在实际分发处理请求时的源码具体看下
Interceptor
的执行情况(源码出自 spring-framework-5.0.x):
protected
void
doDispatch
(HttpServletRequest request, HttpServletResponse response)
throws
Exceptio
{
// 此处用 processedRequest 接收了用户的 request 请求
HttpServletRequest processedRequest = request;
// HandlerExecutionChain 局部变量
HandlerExecutionChain mappedHandler =
null
;
// 标记一下是否解析了文件类型的数据,如果有最终需要清理操作
boolean
multipartRequestParsed =
false
;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try
{
// ModelAndView 局部变量
ModelAndView mv =
null
;
// 处理异常局部变量
Exception dispatchException =
null
;
try
{
/**
* 判断一下是否是文件上传请求。
* 如果请求是 POST 请求,并且 Context-Type 是以 multipart/ 开头的就认为是文件上传的请求。
* 需要注意的是,若是这里被认定为文件上传请求,processedRequest 和 request 将不再指向同一对象
* 这里返回的是 MultipartHttpServletRequest。
*/
processedRequest = checkMultipart(request);
// 两个请求不再相同,进行文件上传标记,用于后续清理操作
multipartRequestParsed = (processedRequest != request);
/**
* 向 HandlerMapping 请求查找 HandlerExecutionChain
* 找到一个处理器,如果没有找到对应的处理类的话,这里通常会返回404。
*/
mappedHandler = getHandler(processedRequest);
// 如果没找到对应的处理器,则抛出异常
// 相信大家都见过 'No mapping for GET /xxxx',就是这里抛出的
if
(mappedHandler ==
null
) {
noHandlerFound(processedRequest, response);
return
;
}
// 根据查找到的 Handler 请求查找能够进行处理的 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 判断自上次请求后是否有修改,没有修改直接返回响应
// 如果是GET请求,且内容没有变化的话,就直接返回
String method = request.getMethod();
boolean
isGet =
"GET"
.equals(method);
if
(isGet ||
"HEAD"
.equals(method)) {
long
lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if
(log.isDebugEnabled()) {
log.debug(
"Last-Modified value for ["
+ getRequestUri(request) +
"] is: "
+ lastModified);
}
if
(
new
ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return
;
}
}
/**
* <<<<<<<<<这里到了我们本节内容,拦截器的执行了>>>>>>>>>
* 这里通过 applyPreHandle 方法,按顺序依次执行 HandlerInterceptor 的 preHandle 方法
* 可以看到,如果任一 HandlerInterceptor 的 preHandle 方法返回了 false, 则整个拦截器连
* 不再继续进行处理。
*/
if
(!mappedHandler.applyPreHandle(processedRequest, response)) {
return
;
}
/**
* 通过 HandlerAdapter 执行查找到的 handler
* 这里真正执行我们 controller 中的方法逻辑,返回一个 ModelAndView
*/
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
/**
* 检查是否已经开始处理并发请求,如果并发处理已经开始,那么当前的请求线程就可以返回了,而不会等待异步操 * 作的结果,也就不会再执行拦截器 PostHandle 之类的操作了。
*/
if
(asyncManager.isConcurrentHandlingStarted()) {
return
;
}
// 如果我们没有设置 viewName,就采用默认的,否则采用我们自己的
applyDefaultViewName(processedRequest, mv);
// <<<<<<<<< 这里,通过 applyPostHandle 逆序执行 HandlerInterceptor 的 postHandle 方法>>>>>>>>>
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch
(Exception ex) {
dispatchException = ex;
}
catch
(Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException =
new
NestedServletException(
"Handler dispatch failed"
, err);
}
// 渲染视图填充 Model,如果有异常渲染异常页面
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch
(Exception ex) {
// 如果有异常按倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch
(Throwable err) {
// 如果有异常按倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new
NestedServletException(
"Handler processing failed"
, err));
}
finally
{
if
(asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if
(mappedHandler !=
null
) {
// 倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else
{
// Clean up any resources used by a multipart request.
if
(multipartRequestParsed) {
// 如果请求包含文件类型的数据则进行相关清理工作
cleanupMultipart(processedRequest);
}
}
}
}
上述源码中,其实不仅仅是拦截器的执行顺序了,而是 Spring MVC 处理客户端请求的整个过程。如下图,可以很直观的看出拦截器的执行时机与顺序。
图片来源于网络,侵权请联系删除
Interceptor 的配置使用
自定义拦截器,实现
HandlerInterceptor
接口
public
class
MyInterceptor
implements
HandlerInterceptor
{
@Override
public
boolean
preHandle
(HttpServletRequest request, HttpServletResponse response, Object handler)
throws
Exception
{
// 在这里添加拦截逻辑,例如身份验证或日志记录
System.out.println(
"MyInterceptor preHandle: "
+ request.getRequestURI());
return
true
;
// 返回 true 继续处理请求,返回 false 中断请求
}
@Override
public
void
postHandle
(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws
Exception
{
// 在视图渲染之前执行的逻辑
System.out.println(
"MyInterceptor postHandle: "
+ request.getRequestURI());
}
@Override
public
void
afterCompletion
(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws
Exception
{
// 请求完成后的逻辑,比如资源清理
System.out.println(
"MyInterceptor afterCompletion: "
+ request.getRequestURI());
}
}
实现
WebMvcConfigurer
接口并重写
addInterceptors
方法,将拦截器注册到 Spring MVC 的拦截器链中
@Configuration
public
class
WebConfig
implements
WebMvcConfigurer
{
@Override
public
void
addInterceptors
(InterceptorRegistry registry)
{
// 添加拦截器,并指定拦截路径模式
registry.addInterceptor(
new
MyInterceptor())
.addPathPatterns(
"/**"
)
// 拦截所有路径
.excludePathPatterns(
"/static/**"
,
"/login"
,
"/register"
);
// 排除静态资源和某些 URL
}
}
AOP(Aspect-Oriented Programming)
什么是 AOP
AOP(Aspect-Oriented Programming),即面向切面编程,是一种编程范式,目的是通过分离横切关注点(如事务管理、日志记录)来提高代码的模块化程度。AOP 允许开发者定义“切面”(Aspects),通过这些切面可以在不改变业务逻辑的情况下增强现有业务功能。
AOP 是一种编程思想,Spring AOP 是 Spring 框架提供的 AOP 实现。
AOP 核心概念
- 切面(Aspect) :一个模块化的特殊类,包含通知和切入点,用来实现特定的横切逻辑。
- 通知(Advice) :在特定连接点(Join Point)执行的动作,例如前置通知(在目标方法调用之前执行的通知)。
- 连接点(Join Point) :程程序执行过程中的一个点,例如方法调用或异常抛出的地方。在 Spring AOP 中,连接点指的是应用程序中所有可能被拦截的方法执行点。
- 切入点(Pointcut)
:用于匹配连接点的表达式,决定了哪些连接点会应用切面的通知。Spring AOP 使用 AspectJ 的切入点表达式语言。例如:
@Pointcut("execution(* com.example.service..*.*(..))")
- 引入(Introduction) :为现有的类添加新方法或属性的能力。
- 目标对象(Target Object) :目标对象是指被一个或多个切面所通知的对象,也就是需要对其方法调用进行增强的对象。使用 AOP 时,这些对象会被代理,以便可以在它们的方法调用前后插入额外的行为。
- 代理(Proxy) :由 AOP 框架创建的对象,用来实现对目标对象的增强。有两种主要类型的 AOP 代理:JDK 动态代理和 CGLIB 代理。
Spring AOP 如何创建代理:
默认情况下,如果目标对象实现了至少一个接口,Spring AOP 将优先选择 JDK 动态代理。
如果目标对象没有实现任何接口,则会自动切换到 CGLIB 代理。
如果要强制使用 CGLIB 代理,可以在启动类或配置类上添加
@EnableAspectJAutoProxy(proxyTargetClass = true)
注解即可。
Spring AOP 的配置使用
Spring AOP 的使用步骤
- 启用 AOP 支持
:在启动类或任意配置类上添加
@EnableAspectJAutoProxy
注解。 - 定义切面(Aspect)
:创建一个类,使用
@Aspect
注解来标记这个类为一个切面,并使用@Component
注解让 Spring 管理这个 Bean。 - 指定切入点(Pointcut)
:使用
@Pointcut
注解加AspectJ
表达式定义切入点。 - 定义通知(Advice) :定义一个通知,在特定连接点时执行特定逻辑。
Spring 提供的通知类型有如下几种:
@Before(前置通知) :前置通知是在目标方法调用 之前 执行的通知。无论目标方法是否抛出异常或正常返回,前置通知都会被执行。
@AfterReturning(后置返回通知) :后置返回通知是在目标方法 成功返回后 执行的通知。只有当目标方法没有抛出异常时,才会触发该通知。
@AfterThrowing(抛出异常通知) :抛出异常通知是在目标方法 抛出异常后 执行的通知。它只会在方法抛出指定类型的异常时触发。
@Around(环绕通知) :环绕通知是最强大的一种通知类型,它包围了目标方法的调用。你可以完全控制方法的执行,包括决定是否继续执行方法以及如何处理返回值或异常。
@After(后置最终通知) :后置最终通知是在目标方法 完成之后 执行的通知,不论方法是正常结束还是因为异常而终止。
Spring AOP 使用示例
启用 AOP 支持
@SpringBootApplication
@EnableAspectJAutoProxy
public
class
MyApp
{
public
static
void
main
(String[] args)
{
SpringApplication.run(MyApp
.
class
,
args
)
;
}
}
定义切面类
@Slf
4j
@Aspect
@Component
public
class
AllAdviceAspect
{
// 定义切入点,匹配 service 包下的所有方法
@Pointcut
(
"execution(* com.example.service..*.*(..))"
)
public
void
serviceLayerExecution
()
{}
// 前置通知,在方法调用前打印日志
@Before
(
"serviceLayerExecution()"
)
public
void
logBefore
(JoinPoint joinPoint)
{
log.info(
"Before method execution: {}"
, joinPoint.getSignature().getName());
}
// 后置返回通知,在方法成功返回后打印日志
@AfterReturning
(pointcut =
"serviceLayerExecution()"
, returning =
"result"
)
public
void
logAfterReturning
(JoinPoint joinPoint, Object result)
{
log.info(
"Method returned successfully: {}, Result: {}"
, joinPoint.getSignature().getName(), result);
}
// 抛出异常通知,在方法抛出异常后打印日志
@AfterThrowing
(pointcut =
"serviceLayerExecution()"
, throwing =
"ex"
)
public
void
logAfterThrowing
(JoinPoint joinPoint, Exception ex)
{
log.error(
"An exception was thrown in {}: {}"
, joinPoint.getSignature().getName(), ex.getMessage());
}
// 后置最终通知,在方法完成之后打印日志,无论是否抛出异常
@After
(
"serviceLayerExecution()"
)
public
void
logAfter
(JoinPoint joinPoint)
{
log.info(
"After method execution: {}"
, joinPoint.getSignature().getName());
}
// 环绕通知,包围方法调用,控制方法执行流程
@Around
(
"serviceLayerExecution()"
)
public
Object
logAround
(ProceedingJoinPoint pjp)
throws
Throwable
{
log.info(
"Starting around advice for method: {}"
, pjp.getSignature().getName());
long
start = System.currentTimeMillis();
try
{
// 执行目标方法
Object result = pjp.proceed();
log.info(
"Method {} took {} ms to execute"
, pjp.getSignature().getName(), System.currentTimeMillis() - start);
return
result;
}
catch
(Throwable e) {
log.error(
"Error during method execution: {}"
, e.getMessage());
throw
e;
// 重新抛出异常以便后续处理
}
finally
{
log.info(
"Ending around advice for method: {}"
, pjp.getSignature().getName());
}
}
}
三者之间对比
Filter
是Java Servlet
规范的一部分,它工作在Servlet
容器层面,是Servlet
容器级别的,适用于所有进入应用的 HTTP 请求。Interceptor
是 Spring MVC 框架提供的一种请求处理机制,是属于框架级别的。通过Interceptor
章节的源码可以看出,Interceptor
工作在 Spring MVC 分发处理请求时,而分发请求的类是DispatcherServlet
,它是一个Servlet
,根据Servlet
规范,Filter
是先于Servlet
执行的。所以Filter
要比Interceptor
优先执行。- Spring AOP 是 Spring 框架提供的面向切面编程的支持,允许我们在不改变原有业务逻辑的前提下,集中处理横切关注点。它的执行时机是在特定的连接点,也就是请求已经到了我们
controller
中的某个方法时才会触发,而Interceptor
在执行查找到的handler
之前就已经被调用了,所以Interceptor
要先于 Spring AOP 执行。
执行顺序如下图:
本篇主要基于 SpringBoot 介绍了过滤器、拦截器和 Spring AOP,通过学习其基本知识了解到了它们工作时的执行顺序。实际上,其实无论是过滤器还是拦截器,都可以被视为 AOP 思想的具体实现形式,尽管它们各自工作在不同的层次上。
您的鼓励对我持续创作非常关键,如果本文对您有帮助,请记得 点赞、分享、在看 哦~~~谢谢!
最后欢迎加入苏三的星球,你将获得:商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、Spring源码解读、工作经验分享、痛点问题等多个优质专栏。
还有1V1答疑、修改简历、职业规划、送书活动、技术交流。
目前星球已经更新了4500+篇优质内容,还在持续爆肝中.....