Spring Boot自定义拦截器

Spring MVC中的拦截器( Interceptor )类似于Servlet中的过滤器( Filter ),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

自定义一个拦截器

自定义一个拦截器非常简单,只需要实现 HandlerInterceptor 这个接口即可,该接口有三个可以实现的 方法,如下:

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

在Spring Boot中生效

定义一个配置类,实现 WebMvcConfigurer 这个接口, 并且实现其中的 addInterceptors() 方法即可,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
MyInterceptor myInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 不拦截的uri
final String[] commonExclude = {};
registry.addInterceptor(myInterceptor).excludePathPatterns(commonExclude);
}
}

举个栗子

开发中可能会经常遇到短时间内由于用户的重复点击导致几秒之内重复的请求,可能就是在这几秒之内由于各种问题,比如网络事务的隔离性等等问题导致了数据的重复等问题,因此在日常开发中必须规避这类的重复请求操作,今天就用拦截器简单的处理一下这个问题。

思路

在接口执行之前先对指定接口(比如标注某个注解的接口)进行判断,如果在指定的时间内(比如5 秒 )已经请求过一次了,则返回重复提交的信息给调用者。

根据什么判断这个接口已经请求了?

根据项目的架构可能判断的条件也是不同的,比如 IP地址用户唯一标识请求参数请求URI 等等其中的某一个或者多个的组合。

这个具体的信息存放在哪里?

由于是 短时间 内甚至是瞬间并且要保证 定时失效 ,肯定不能存在事务性数据库中了,因此常用的几种数据库中只有 Redis 比较合适了。

实现

自定义一个注解

第一步,先自定义一个注解,可以标注在类或者方法上,如下:

1
2
3
4
5
6
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
// 默认失效时间5秒
long seconds() default 5;
}

配置Redis

第二步,配置Redis

添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms

创建拦截器并注入到IOC容器

第三步,创建一个拦截器,注入到IOC容器中 ,实现的思路很简单,判断controller的类或者方法上是否标注了 @RepeatSubmit 这个注解,如果标注了,则拦截判断,否则跳过,代码如下:

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
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

/**
* Redis API
*/
@Autowired
StringRedisTemplate stringRedisTemplate;

/**
* 判断条件仅仅是用了uri,实际开发中根据实际情况组合一个唯一识别的条件。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
// 只拦截标注了@RepeatSubmit该注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 标注在方法上的@RepeatSubmit
RepeatSubmit repeatSubmitByMethod =
AnnotationUtils.findAnnotation(handlerMethod.getMethod(), RepeatSubmit.class);
// 标注在Controller类上的@RepeatSubmit
RepeatSubmit repeatSubmitByClass =
AnnotationUtils.findAnnotation(handlerMethod.getMethod().getDeclaringClass(), RepeatSubmit.class);
// 没有限制重复提交,跳过
if (Objects.isNull(repeatSubmitByMethod) && Objects.isNull(repeatSubmitByClass)) {
return true;
}
// todo: 组合判断条件,这里仅仅是演示,实际项目中根据架构组合条件
// 请求的URI
String uri = request.getRequestURI();
// redis中存在即返回false,不存在即返回true
// 注意:标注在方法上的超时时间会覆盖掉类上的时间
Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "",
Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod.seconds() : repeatSubmitByClass.seconds(),
TimeUnit.SECONDS);
//如果存在,表示已经请求过了,直接抛出异常,由全局异常进行处理返回指定信息
if (ifAbsent != null && !ifAbsent) {
throw new RepeatSubmitException();
}
return true;
}
return true;
}

}

在WebConfig中配置拦截器

第四步,在WebConfig中配置这个拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
RepeatSubmitInterceptor repeatSubmitInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 不拦截的uri
final String[] commonExclude = {"/error", "/files/**"};
registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns(commonExclude);
}
}

创建Controller

OK,拦截器已经配置完成,只需要在需要拦截的接口上标注 @RepeatSubmit 这个注解即可

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("interceptor")
public class InterceptorController {

@GetMapping("hello")
@RepeatSubmit
public String hello() {
return "Hello";
}

}
1
GET http://localhost:8080/interceptor/hello

源码

https://github.com/zhaoyangmushiyi/spring-boot-advance/tree/master/sprint-boot-interceptor