Appearance
Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
自定义拦截器
自定义一个拦截器非常简单,只需要实现 HandlerInterceptor 这个接口即可,该接口有三个可以实现的方法,如下:
- preHandle() 方法:该方法会在控制器方法前执行,其返回值表示是否知道如何写一个接口。中断后续操作。当其返回值为 true 时,表示继续向下执行;当其返回值为 false 时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
- postHandle() 方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
- afterCompletion() 方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。
如何使其在Spring Boot中生效?
其实想要在SpringBoot生效其实很简单,只需要定义一个配置类,实现 WebMvcConfigurer 这个接口, 并且实现其中的 addInterceptors() 方法即可,代码演示如下:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private XXX xxx;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//不拦截的uri
final String[] commonExclude = {}};
registry.addInterceptor(xxx).excludePathPatterns(commonExclude);
}
}
拦截器实战
开发中可能会经常遇到短时间内由于用户的重复点击导致几秒之内重复的请求,可能就是在这几秒之内 由于各种问题,比如网络,事务的隔离性等等问题导致了数据的重复等问题,因此在日常开发中必须规避这类的重复请求操作,今天就用拦截器简单的处理一下这个问题。
实现思路
在接口执行之前先对指定接口(比如标注某个注解的接口)进行判断,如果在指定的时间内(比如 5 秒)已经请求过一次了,则返回重复提交的信息给调用者。
根据什么判断这个接口已经请求了?
根据项目的架构可能判断的条件也是不同的,比如 IP地址 , 用户唯一标识 、 请求参数 、 请求URI 等等 其中的某一个或者多个的组合。由于是短时间内甚至是瞬间并且要保证定时失效 ,肯定不能存在事务性数据库中了,因此常用的几种数据库中只有 Redis 比较合适了。
实现步骤
先自定义一个注解,可以标注在类或者方法上
java@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatSubmit { /** * 默认失效时间5秒 */ long seconds() default 5; }
创建一个拦截器,注入到IOC容器中,实现的思路很简单,判断Controller的类或者方法上是否标注了 @RepeatSubmit 这个注解,如果标注了,则拦截判断,否则跳过,代码如下:
javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * 重复请求的拦截器 */ @Component public class RepeatSubmitInterceptor implements HandlerInterceptor { /** * Redis的API */ @Autowired private StringRedisTemplate stringRedisTemplate; /** * preHandler方法,在controller方法之前执行 * 判断条件仅仅是用了uri,实际开发中根据实际情况组合一个唯一识别的条件。 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { //只拦截标注了@RepeatSubmit该注解 HandlerMethod method = (HandlerMethod) handler; //标注在方法上的@RepeatSubmit RepeatSubmit repeatSubmitByMethod = AnnotationUtils.findAnnotation(method.getMethod(), RepeatSubmit.class); //标注在controler类上的@RepeatSubmit RepeatSubmit repeatSubmitByCls = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), RepeatSubmit.class); //没有限制重复提交,直接跳过 if (Objects.isNull(repeatSubmitByMethod) && Objects.isNull(repeatSubmitByCls)) return true; // todo: 组合判断条件,这里仅仅是演示,实际项目中根据架构组合条件 //请求的URI String uri = request.getRequestURI(); //存在即返回false,不存在即返回true Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "", Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod.seconds() : repeatSubmitByCls.seconds(), TimeUnit.SECONDS); //如果存在,表示已经请求过了,直接抛出异常,由全局异常进行处理返回指定信息 if (ifAbsent != null && !ifAbsent) throw new RepeatSubmitException(); // 自定义异常,配合全局异常拦截器,返回指定JSOL数据 } return true; } }
在Spring Boot中配置这个拦截器
java@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //不拦截的uri final String[] commonExclude = {"/error", "/files/**"}; registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns(commonExclude); } }
最后,拦截器已经配置完成,只需要在需要拦截的接口上标注 @RepeatSubmit 这个注解即可
java@RestController @RequestMapping("/user") @RepeatSubmit // 标注了@RepeatSubmit注解,全部的接口都需要拦截 public class LoginController { @RequestMapping("/login") public String login(){ return "login success"; } }
此时,请求这个URI: http://localhost:8080/springboot-demo/user/login
在5秒之内只能请求一次。注意,标注在方法上的超时时间会覆盖掉类上的时间,因为如下一段代码: