前言

​ Filter、Interceptor、AOP都是用于实现应用横切关注点的技术手段,通过这些技术,可以将横切关注点的代码从核心业务逻辑中解耦,使得代码更加清晰和可维护,同时也提高了代码的复用性。但是三者的应用场景还是有些区别的,Filter主要用于处理HTTP请求和响应,在Servlet容器中工作,可以实现如日志记录、安全性过滤、跨域请求处理等;Interceptor主要用于在Spring MVC中拦截方法调用,允许在方法执行前后添加额外逻辑,用于实现权限拦截、日志记录、事务管理等;AOP用于处理复杂的横切关注点,在不修改核心业务逻辑的情况下增加或调整功能,用于日志记录、事务管理、性能监控等。

一、Filter(过滤器)

1.说明

​ Filter主要用于Web应用开发中,基于Servlet规范工作,处理HTTP请求和响应,可以在请求到达Servlet之前进行预处理,也可以在响应返回给客户端之前进行后处理。Filter可以组成过滤链,按照配置的顺序依次处理请求,每个Filter可以在请求进入Servlet之前进行拦截,也可以在响应返回之前对响应进行处理。需要实现Filter接口,重写init()、doFilter()和destroy()方法,其中init()和destroy()分别是初始化和销毁方法,重点是doFilter()方法,实现对请求的处理和转发或对响应的处理。

2.实现

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
/**
* 过滤器
*/
@Component
public class WsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String url = request.getRequestURI(); // 相对方法名路径
System.out.println("\n【过滤器】请求到达:" + url);
// 如果没有filterChain.doFilter方法,请求的方法就直接被过滤了,执行不到下面的Controller方法中
filterChain.doFilter(request, response);
System.out.println("【过滤器】请求结束:" + url);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

filterChain.doFilter()

项目启用了过滤器,doFilter方法下如果没有filterChain.doFilter(request, response)方法,则程序请求的方法就直接被过滤了,执行不到下面的Controller方法中。

3.order优先级

配置多个Filter,根据不同的order来决定过滤执行的顺序,order越小优先级越高,越大优先级越低。

4.解决跨域

与前端项目交互时,尤其是前后端分离项目,经常会出现跨域问题,这个时候需要配置跨域参数,设置Access-Control允许各类请求通过,解决跨平台交互的跨域问题。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String url = request.getRequestURI(); // 相对方法名路径
System.out.println("\n【过滤器】请求到达:" + url);

// 跨域配置
if (request.getHeader("Origin") != null) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
}
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");

// 如果没有filterChain.doFilter方法,请求的方法就直接被过滤了,执行不到下面的Controller方法中
filterChain.doFilter(request, response);
System.out.println("【过滤器】请求结束:" + url);
}

5.拦截返回错误信息JSON

如果是void无返回值的方法,直接拦截是没有问题的,直接不执行filterChain.doFilter(request, response)方法即可。但是如果是有返回值的,需要在过滤器上构造JSON进行返回,如下:

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
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String url = request.getRequestURI(); // 相对方法名路径
System.out.println("\n【过滤器】请求到达:" + url);

returnJson(response, 401, "Filter拦截了");

// 如果没有filterChain.doFilter方法,请求的方法就直接被过滤了,执行不到下面的Controller方法中
// filterChain.doFilter(request, response);
// System.out.println("【过滤器】请求结束:" + url);
}

private void returnJson(HttpServletResponse response, int code, String msg) throws IOException {
// 设置响应的内容类型为JSON
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 创建JSON字符串
Map<String, Object> map = new HashMap<>();
map.put("code", code);
map.put("msg", msg);
String json = JSON.toJSONString(map);
// 获取PrintWriter来写入响应
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
}

二、Interceptor(拦截器

1.说明

​ Interceptor主要用于Spring MVC框架中拦截方法的调用,用于框架中方法调用的拦截和处理,拦截器与过滤器(Filter)类似,但更加灵活,通常用于框架级别的功能扩展和定制化。通常用于框架级别的功能扩展,如事务管理、日志记录、权限检查等,使得应用程序可以更加模块化、可维护和可扩展。

2.实现

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
/**
* 拦截器
*/
@Component
public class WsInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI(); // 相对方法名路径
System.out.println("【拦截器】到达:"+ url);
return true; // 放开,继续执行
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 当前请求进行处理之后,也就是Controller方法调用之后执行,但是它会在DispatcherServlet进行视图返回渲染之前被调用
System.out.println("【拦截器】请求处理完成,DispatcherServlet准备进行视图返回渲染");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在整个请求结束之后,也就是在DispatcherServlet渲染了对应的视图之后执行
System.out.println("【拦截器】请求结束,DispatcherServlet渲染了对应的视图");
}
}
123456789101112131415161718192021222324
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

@Bean
public WsInterceptor getWsInterceptor() {
return new WsInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getWsInterceptor());
}
}

preHandle

请求处理前preHandle,是在请求处理器(如Controller方法)执行前被调用的方法,允许开发者在实际请求处理前执行特定的预处理逻辑。必须【return true】时,才能执行到后续的controller方法中,如果【return false】则不往下继续执行。

postHandle

请求处理后postHandle,当前请求进行处理之后,也就是Controller方法调用之后执行,但是它会在DispatcherServlet进行视图返回渲染之前被调用。

afterCompletion

视图渲染后afterCompletion,在整个请求结束之后,也就是在DispatcherServlet渲染了对应的视图之后执行。

3.执行顺序图

4.排除特定路径拦截 or 加入指定路径拦截

如果什么都不处理,默认拦截所有的请求
registry.addPathPatterns(URL)为加入指定路径拦截,则其他路径都放行

1
2
3
4
5
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getWsInterceptor()).addPathPatterns("/test/test02");
}

registry.excludePathPatterns(URL)为排除特定路径拦截,其他路径都拦截

1
2
3
4
5
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getWsInterceptor()).excludePathPatterns("/test/test02");
}

5.拦截返回错误信息JSON

如果是void无返回值的方法,直接拦截是没有问题的,直接【return false】即可。但是如果是有返回值的,需要在拦截器上构造JSON进行返回,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI(); // 相对方法名路径
System.out.println("【拦截器】到达:"+ url);
returnJson(response, 402, "Interceptor拦截了");
return false;
// return true; // 放开,继续执行
}

private void returnJson(HttpServletResponse response, int code, String msg) throws IOException {
// 设置响应的内容类型为JSON
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 创建JSON字符串
Map<String, Object> map = new HashMap<>();
map.put("code", code);
map.put("msg", msg);
String json = JSON.toJSONString(map);
// 获取PrintWriter来写入响应
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
}

三、AOP(切面儿)

1.说明

​ AOP切面是一种编程范式,用于通过将横切关注点从核心业务逻辑中分离出来,使得这些关注点能够被模块化、重用,并且能够有效地降低代码的重复性。它定义了在何处(Pointcut)以及如何(Advice)应用横切关注点到目标对象的行为,增强系统的模块化程度,提高代码的可维护性和可扩展性

2.实现

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
@Aspect
@Component
public class WsAop {
// 定义切面的切入点,参数是定义在哪个包。哪个类、哪个方法切入,关于切入点如何定义(对应路径下的某类、某方法,*代表的是全部)
@Pointcut("execution(* com.jon.demo.controller.*.*(..))")
public void wsLog(){}

@Before("wsLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 在切⼊点开始处切⼊内容
System.out.println("【AOP】before 在切⼊点开始处切⼊内容");
}

@After("wsLog()")
public void doAfter() {
// 在切⼊点结尾处切⼊内容
System.out.println("【AOP】doAfter 在切⼊点结尾处切⼊内容");
}

@AfterReturning(pointcut = "wsLog()", returning = "result")
public void doAfterReturning(Object result) {
// 在切⼊点return内容之后切⼊内容,可以对返回值做⼀些加⼯处理
System.out.println("【AOP】doAfterReturning 在切⼊点return内容之后切⼊内容");
}

@AfterThrowing(pointcut = "wsLog()", throwing = "exception")
public void doAfterThrowing(Exception exception) {
// 处理当切⼊内容部分抛出异常之后的逻辑
System.out.println("【AOP】doAfterThrowing 处理当切⼊内容部分抛出异常之后的逻辑");
}

// 环绕通知wsLog方法,切面方法需要有返回值,来代替被代理方法返回结果
@Around("wsLog()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
// 方法要有返回,如果有多个参数ProceedingJoinPoint要放在第一个
System.out.println("【AOP】doAround");
return point.proceed();
}
}

Pointcut切面节点定义

定义切面的切入点,参数是定义在哪个包。哪个类、哪个方法切入,关于切入点如何定义(对应路径下的某类、某方法,*代表的是全部)

doBefore

在切⼊点开始处切⼊内容

doAfter

在切⼊点结尾处切⼊内容

doAfterReturning

在切⼊点return内容之后切⼊内容,可以对返回值做⼀些加⼯处理。

doAfterThrowing

处理当切⼊内容部分抛出异常之后的逻辑

doAround

环绕通知wsLog方法,切面方法需要有返回值,来代替被代理方法返回结果。方法要有返回,如果有多个参数ProceedingJoinPoint要放在第一个。

3.基于注解在特定方法上实现

​ 不在Pointcut指定特意的类、方法作为切入点,直接自定义注解,并且指定注解的引用地址,这样在对应的方法中加上注解就会引入切面了,这样更加方便和灵活。具体的使用方式,在之前的文章自定义注解(一)——统一请求拦截中有定义!

4.拦截返回错误信息JSON

如果是void无返回值的方法,直接拦截是没有问题的,直接around方法不执行point.proceed()即可。但是如果是有返回值的,需要在around方法中构造JSON,并且在point.proceed()之前进行返回,如下:

1
2
3
4
5
6
7
8
9
10
11
12
	// 环绕通知wsLog方法,切面方法需要有返回值,来代替被代理方法返回结果
@Around("wsLog()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
// 方法要有返回,如果有多个参数ProceedingJoinPoint要放在第一个
System.out.println("【AOP】doAround");
Map<String, Object> map = new HashMap<>();
map.put("code", 403);
map.put("msg", "AOP拦截了");
return map;
// return point.proceed();
}
1234567891011

四、三者共同点及不同点

1.共同点

三者都是处理横切关注点,即那些不能分散在核心业务逻辑中处理的功能或需求,例如日志记录、性能监控、事务管理等。它们都支持在运行时动态地将横切关注点的代码织入到目标对象的执行流程中,而不需要修改目标对象的源代码。

2.不同点

Filter工作在Servlet容器中,可以对请求链进行处理,例如处理编码、安全性、日志等。对比于,Filter更多用于整个请求和响应的处理。

Interceptor主要工作Spring容器中,用于框架中方法调用的拦截和处理,通常用于面向对象的框架中,在方法调用前后可以添加额外逻辑。对比于Filter,Interceptor更加专注于方法调用前后的处理。

AOP用于处理复杂的横切关注点,允许在运行时动态地将横切逻辑织入到应用中,可以跨越多个类和模块,不仅限于单个方法或请求处理。可以在代码中定义关注点(如事务、日志、安全性),并在需要时将其应用到目标对象中。

五、三者横切节点分析


三者横切的节点不一样,客户端端发起请求后,请求顺序为:请求入口 -> 过滤器filterChain.doFilter之前 -> 拦截器preHandle方法 -> 切面doAround方法 -> 切面before方法 -> Controller下的方法

返回顺序为:Controller下的方法执行结束 -> 切面doAfterReturning方法 -> 切面doAfter方法 -> 拦截器postHandle方法 -> 拦截器afterCompletion方法 -> 过滤器filterChain.doFilter之后 -> 请求入口

可看执行结果如下: