序言

个人深知对这三个概念的理解不够清晰,特此开一篇文章来记录学习过程,以下陈述及总结大部分来源于网络资料,我在文章结尾会标注出来,若存在错误描述,欢迎各位的指正,转载请标注出处!

过滤器

1.1 简介

过滤器基于 Servlet 规范,属于 Web 层的机制,是一种 容器级别 的请求和响应处理组件。

过滤器拦截的是URL(即HTTP请求,一次HTTP请求中包含请求和响应),作用于整个 Web 应用的请求/响应生命周期,可以对所有请求或某些特定 URL 模式生效。

Spring中自定义过滤器(Filter)一般只有一个方法,返回值是void,当请求到达web容器时,会探测当前请求地址是否配置有过滤器,有则调用该过滤器的方法(可能会有多个过滤器),然后才调用真实的业务逻辑,至此过滤器任务完成。过滤器并没有定义业务逻辑执行前、后等,仅仅是请求到达就执行。

请求执行流程如下图——源自该篇文章过滤器、拦截器和AOP的分析与对比

image

1.2 场景

  • 自动登录
  • 统一编码格式
  • 访问权限控制,如 IP 白名单过滤
  • 安全过滤,对请求参数进行校验或清理、敏感字符过滤
  • 日志记录
  • 跨域处理(CORS)
  • 内容压缩,GZIP 压缩等

1.3 源码分析

1
2
3
4
5
6
7
8
9
public interface Filter {

public void init(FilterConfig filterConfig) throws ServletException;

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;

public void destroy();
}

过滤器方法的入参有request,response,FilterChain,其中FilterChain是过滤器链,使用比较简单,而request,response则关联到请求流程,因此可以对请求参数做过滤和修改,同时FilterChain过滤链执行完,并且完成业务流程后,会返回到过滤器,此时也可以对请求的返回数据做处理(如果Filter的urlPatterns设置为“/*”的话,doFilter()方法会执行两次,向服务器发起请求执行一次,服务器返回结果再执行一次)。

1.4 URL匹配模式

  1. 以指定资源匹配。例如”/index.jsp”
  2. 以目录匹配。例如”/servlet/*”
  3. 以后缀名匹配。例如”*.jsp”
  4. 通配符,拦截所有web资源。”/*”

1.5 例子(记录请求日志的过滤器)

  1. 创建过滤器类

实现 javax.servlet.Filter 接口,编写日志记录逻辑。

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
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class LoggingFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("LoggingFilter initialized");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将请求转换为 HttpServletRequest 以便访问更多信息
HttpServletRequest httpRequest = (HttpServletRequest) request;

// 记录请求的 URI 和时间
long startTime = System.currentTimeMillis();
System.out.println("Request URI: " + httpRequest.getRequestURI() + " | Start Time: " + startTime);

// 继续处理请求(传递给下一个过滤器或最终的目标资源)
chain.doFilter(request, response);

// 记录处理结束时间
long endTime = System.currentTimeMillis();
System.out.println("Request URI: " + httpRequest.getRequestURI() + " | Time Taken: " + (endTime - startTime) + " ms");
}

@Override
public void destroy() {
System.out.println("LoggingFilter destroyed");
}
}
  1. 注册过滤器

在 Spring Boot 中,可以通过以下两种方式注册过滤器。

方式 1:通过 FilterRegistrationBean 注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

@Bean
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
registrationBean.addUrlPatterns("/*"); // 过滤所有请求
registrationBean.setOrder(1); // 设置过滤器执行顺序,数字越小优先级越高
return registrationBean;
}
}

方式 2:通过 @WebFilter 注解注册
如果使用 Servlet 3.0 注解,可以直接用 @WebFilter 注解:

1
2
3
4
5
6
import javax.servlet.annotation.WebFilter;

@WebFilter(urlPatterns = "/*") // 过滤所有 URL
public class LoggingFilter implements Filter {
// 实现同上
}

注意:使用 @WebFilter 时,需要在主类上添加 @ServletComponentScan 注解以启用扫描。

  1. 测试运行

当你运行 Spring Boot 应用并访问任意 URL 时,例如 http://localhost:8080/api/hello,过滤器会自动记录以下日志:

1
2
3
LoggingFilter initialized
Request URI: /api/hello | Start Time: 1694688460223
Request URI: /api/hello | Time Taken: 50 ms

拦截器

2.1 简介

拦截器是 Spring 框架 提供的一种机制,基于 HandlerInterceptor 接口,属于 框架级别 的组件。

拦截器只能拦截部分web请求(URL)。(拦截器是基于反射机制实现的,拦截的对象只能是实现了接口的类,而不能拦截url这种连接)

Java里的拦截器提供的是非系统级别的拦截,也就是说,就覆盖面来说,拦截器不如过滤器强大,但是更有针对性。Java中的拦截器是基于Java反射机制实现的,更准确的划分,是基于JDK实现的动态代理。

单个拦截器执行流程如下图——源自该篇文章过滤器、拦截器和AOP的分析与对比

image

  1. 程序先执行preHandle()方法,如果该方法的返回值为true,则程序会继续向下执行处理器中的方法,否则将不再向下执行。
  2. 在业务处理器(即控制器Controller类)处理完请求后,会执行postHandle()方法,然后会通过DispatcherServlet向客户端返回响应。
  3. 在DispatcherServlet处理完请求后,才会执行afterCompletion()方法。

多个拦截器执行流程如下图——源自该篇文章过滤器、拦截器和AOP的分析与对比

image

  1. 当有多个拦截器同时工作时,它们的preHandle()方法会按照配置文件中拦截器的配置顺序执行,而它们的postHandle()方法和afterCompletion()方法则会按照配置顺序的反序执行。

2.2 场景

  • 日志记录:记录请求信息的日志
  • 权限检查:如登录检查
  • 性能检测:检测方法的执行时间
  • 用户认证与授权
  • 参数校验
  • 拦截业务逻辑

2.3 源码分析

1
2
3
4
5
6
7
8
9
10
11
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 {
}
}
  1. preHandle() 方法:该方法会在控制器方法前执行,其返回值表示是否中断后续操作。当其返回值为true时,表示继续向下执行;当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
  2. postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
  3. afterCompletion()方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。

在Spring框架中,我们可以直接继承HandlerInterceptorAdapter.java这个抽象类,来实现我们自己的拦截器。

2.4 例子(基于认证令牌的拦截器)

  1. 创建拦截器类

实现 HandlerInterceptor 接口,并在 preHandle 方法中添加认证逻辑。

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
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

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

@Component
public class AuthInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 从请求头中获取认证令牌
String authToken = request.getHeader("Authorization");

// 检查令牌是否有效
if (authToken == null || !isValidToken(authToken)) {
// 如果令牌无效,返回 401 未授权响应
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized");
return false; // 中断请求
}

// 令牌有效,继续处理请求
return true;
}

private boolean isValidToken(String token) {
// 模拟令牌验证逻辑
return "VALID_TOKEN".equals(token);
}
}
  1. 注册拦截器

通过 WebMvcConfigurer 注册拦截器并指定其作用范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 {

private final AuthInterceptor authInterceptor;

public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并指定拦截路径
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截所有 /api 开头的请求
.excludePathPatterns("/api/public/**"); // 排除公共路径
}
}
  1. 测试运行

假设有以下两个 API:

  • 受保护的 API:/api/secure/data
  • 公共 API:/api/public/info

场景 1:访问受保护的 API 且无认证令牌

GET /api/secure/data
Authorization: <无>

响应:
HTTP 401 Unauthorized
内容: Unauthorized

场景 2:访问受保护的 API 且有无效令牌

GET /api/secure/data
Authorization: INVALID_TOKEN

响应:
HTTP 401 Unauthorized
内容: Unauthorized

场景 3:访问受保护的 API 且有有效令牌

GET /api/secure/data
Authorization: VALID_TOKEN

响应:
HTTP 200 OK
内容: <正常响应数据>

场景 4:访问公共 API

GET /api/public/info

响应:
HTTP 200 OK
内容: <正常响应数据>

面向切面编程(AOP)

3.1 简介

面向切面编程 是基于 Spring AOP,动态代理方法级别的增强逻辑。

面向切面拦截的是类的元数据(包、类、方法名、参数等)

相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。

AOP是对OOP的一种补充和完善,将代码中重复的工作抽取出来成为一个切面,减少代码的耦合性,例如事务、日志。

3.2 场景

  • 事务管理
  • 日志记录(方法级别)、打印日志
  • 性能监控
  • 异常处理等

3.3 AOP术语

  1. 方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。
  2. 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
  3. 通知(Advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。
  4. 切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。
  5. 引入(Introduction):添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现IsModified接口,来简化缓存。
  6. 目标对象(Target Object):包含连接点的对象,也被称作被通知或被代理对象。
  7. AOP代理(AOP Proxy):AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
  8. 编织(Weaving):组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

各种通知类型包括:

  1. Around通知:包围一个连接点的通知,如方法调用。这是最强大的通知。Aroud通知在方法调用前后完成自定义的行为,它们负责选择继续执行连接点或通过返回它们自己的返回值或抛出异常来短路执行。
  2. Before通知:在一个连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
  3. AfterThrowing通知:在方法抛出异常时执行的通知。Spring提供强制类型的Throws通知,因此你可以书写代码捕获感兴趣的异常(和它的子类),不需要从Throwable或Exception强制类型转换。
  4. AfterReturning通知:在连接点正常完成后执行的通知,例如,一个方法正常返回,没有抛出异常。

如同AspectJ,Spring提供所有类型的通知,推荐使用最为合适的通知类型来实现需要的行为。例如,如果只是需要用一个方法的返回值来更新缓存,最好实现一个AfterReturning通知,而不是Around通知,虽然Around通知也能完成同样的事情。使用最合适的通知类型使编程模型变得简单,代码可读性更高,并能减少潜在错误。

3.4 源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}

@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
}

3.5 例子(对服务层方法进行日志记录)

  1. 服务类

目标方法是服务类 UserService 中的 addUser 方法。

1
2
3
4
5
6
7
8
9
10
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
public void addUser(String username) {
System.out.println("Executing addUser logic for: " + username);
}
}
  1. 切面类

切面类定义日志逻辑,使用 Spring AOP 的注解实现。

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
package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

// 定义切入点:匹配 com.example.service 包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

// 前置通知:在目标方法执行之前执行
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
System.out.println("Arguments: ");
for (Object arg : args) {
System.out.println(" - " + arg);
}
}
}

// 后置通知:在目标方法执行之后执行
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}

// 返回通知:获取目标方法的返回值(如果有)
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Method returned value: " + result);
}

// 异常通知:捕获目标方法抛出的异常
@AfterThrowing(pointcut = "serviceMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
System.out.println("Method threw an exception: " + error.getMessage());
}
}
  1. 启用 AOP

在主应用类中启用 AOP 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy // 启用 AOP 功能
public class AopExampleApplication {
public static void main(String[] args) {
SpringApplication.run(AopExampleApplication.class, args);
}
}
  1. 测试代码

使用 Spring 的上下文测试切面逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example;

import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

@Autowired
private UserService userService;

@Override
public void run(String... args) throws Exception {
userService.addUser("Alice");
}
}
  1. 运行结果

当调用 UserService.addUser(“Alice”) 时,切面逻辑自动在方法前后记录日志

1
2
3
4
5
Before method: addUser
Arguments:
- Alice
Executing addUser logic for: Alice
After method: addUser

如果 addUser 抛出异常,则会触发异常通知,记录类似:

1
Method threw an exception: Some error

三者对比

三者功能类似,但各有优势,从过滤器–》拦截器–》AOP,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

下图——源自该篇文章过滤器、拦截器和AOP的分析与对比

image

Spring的拦截器与Servlet的Filter有相似之处,比如二者都是AOP编程思想的体现,都能实现权限检查、日志记录等。不同的是:

  1. 使用范围不同:Filter是Servlet规范规定的,只能用于Web程序中。而拦截器既可以用于Web程序,也可以用于Application、Swing程序中。

  2. 规范不同:Filter是在Servlet规范中定义的,是Servlet容器支持的。而拦截器是在Spring容器内的,是Spring框架支持的。

  3. 使用的资源不同:同其他的代码块一样,拦截器也是一个Spring的组件,归Spring管理,配置在Spring文件中,因此能使用Spring里的任何资源、对象,例如Service对象、数据源、事务管理等,通过IOC注入到拦截器即可;而Filter则不能。

  4. 深度不同:

    • Filter在只在Servlet前后起作用。实际上Filter和Servlet极其相似,区别只是Filter不能直接对用户生成响应。实际上Filter里doFilter()方法里的代码就是从多个Servlet的service()方法里抽取的通用代码,通过使用Filter可以实现更好的复用。Filter是一个可以复用的代码片段,可以用来转换Http请求、响应和头信息。Filter不像Servlet,它不能产生一个请求或者响应,它只是修改对某一资源的请求,或者修改从某一资源的响应。
    • 而拦截器能够深入到方法前后、异常抛出前后等,因此拦截器的使用具有更大的弹性。所以在Spring构架的程序中,要优先使用拦截器。
    • AOP相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。

参考 & 鸣谢

  1. 过滤器、拦截器和AOP的分析与对比 - 🐫沙漠骆驼 - 博客园
  2. 面试突击90:过滤器和拦截器有什么区别?-腾讯云开发者社区-腾讯云
  3. Spring 过滤器 拦截器 AOP区别_spring拦截器与aop的区别-CSDN博客