SpringBoot利用 * 实现避免重复请求
作者:iicode 发布时间:2022-02-07 00:21:53
*
什么是 *
Spring MVC中的 * (Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过 * 可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
如何自定义 *
自定义一个 * 非常简单,只需要实现HandlerInterceptor这个接口即可,这个接口有三个可实现的方法
preHandle()
方法:该方法会在控制器方法前执行,其返回值表示是否知道如何写一个接口。中断后续操作。当其返回值为true
时,表示继续向下执行;当其返回值为false
时,会中断后续的所有操作(包括调用下一个 * 和控制器类中的方法执行等)。postHandle()
方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。afterCompletion()
方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。
如何让 * 在Spring Boot中生效
想要在Spring Boot生效其实很简单,只需要定义一个配置类,实现WebMvcConfigurer
这个接口,并且实现其中的addInterceptors()
方法即可,代码如下:
@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比较合适了。
实现
Docker启动一个Redis
docker pull redis:7.0.4
docker run -itd \
--name redis \
-p 6379:6379 \
redis:7.0.4
创建一个Spring Boot项目
使用idea的Spring Initializr来创建一个Spring Boot项目,如下图:
添加依赖
pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot_06</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_06</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--spring redis配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 1.5的版本默认采用的连接池技术是jedis 2.0以上版本默认连接池是lettuce, 在这里采用jedis,所以需要排除lettuce的jar -->
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置Redis
application.properties
spring.redis.host=127.0.0.1
spring.redis.database=1
spring.redis.port=6379
定义一个注解
package com.example.springboot_06.intercept;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 默认失效时间5秒
*
* @return
*/
long seconds() default 5;
}
创建一个 *
package com.example.springboot_06.intercept;
import lombok.extern.slf4j.Slf4j;
import 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:该注解将其注入到IOC容器中
*/
@Slf4j
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
/**
* Redis的API
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* preHandler方法,在controller方法之前执行
* <p>
* 判断条件仅仅是用了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)) {
log.info("isNull");
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) {
String msg = String.format("url:[%s]重复请求", uri);
log.warn(msg);
// throw new RepeatSubmitException(msg);
throw new Exception(msg);
}
}
return true;
}
}
配置 *
package com.example.springboot_06.config;
import com.example.springboot_06.intercept.RepeatSubmitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 不拦截的uri
final String[] commonExclude = {"/error", "/files/**"};
registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns(commonExclude);
}
}
写个测试Controller
package com.example.springboot_06.controller;
import com.example.springboot_06.intercept.RepeatSubmit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 标注了@RepeatSubmit注解,全部的接口都需要拦截
*
*/
@Slf4j
@RestController
@RequestMapping("/user")
@RepeatSubmit
public class UserController {
@RequestMapping("/save")
public ResponseEntity save() {
log.info("/user/save");
return ResponseEntity.ok("save success");
}
}
测试
来源:https://segmentfault.com/a/1190000042858692


猜你喜欢
- 本文实例讲述了Android中ContextMenu用法。分享给大家供大家参考。具体如下:main.xml文件如下:<?xml ver
- 这两天看到Hibernate的代理部分,第一反应是底层使用了反射,针对用户实体生成了代理类,后来反应过来了,反射没有任何可以产生新类的能力,
- Cocos2d-x引擎的核心是用C++编写的,那对于所有使用该引擎的游戏开发人员来说,内存管理是一道绕不过去的坎。关于Cocos2d-x内存
- DownloadManager三大组件介绍DownloadManager类似于下载队列,管理所有当前正在下载或者等待下载的项目;他可以维持
- 在windows环境下,我们通常在IDE如VS的工程中开发C++项目,对于生成和使用静态库(*.lib)与动态库(*.dll)可能都已经比较
- JVM运行原理首先从“.java”代码文件,编译成“.class”字节码文件,然后类加载器将“.class”字节码文件中的类给加载带JVM中
- 前言尺子在客户端开发中有一定的应用场景,比如厘米尺、白板的画线尺、视频剪辑的时间尺。一般可以采用用户控件通过自绘的方式实现,但今天我要讲一个
- 今年新开Java课程第一步就是…配置环境就从Java的环境配置开始好了以下是正式的步骤首先,从Oracle的官网下载jdk的安装包点我下载J
- 上篇文章给大家介绍了浅析C# 中的类型系统(值类型和引用类型),接下来通过本文给大家介绍下c# 泛型类型,说下C#中的泛型,熟练地使用泛型能
- 接收从控制台输入的数据可以使用Scanner类实现,Scanner类在一个名为util的包中需要在程序中导入这个包, 即在程序中添加impo
- 传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人
- 本文实例讲述了应用Java泛型和反射导出CSV文件的方法。分享给大家供大家参考。具体如下:项目中有需求要把数据导出为CSV文件,因为不同的类
- 1,什么是字符编码? 字符(Character)是文字与符号的总称,包括文字、图形符号、数学符号等。一组
- 1、System.Threading.Timer 线程计时器1、最底层、轻量级的计时器。基于线程池实现的,工作在辅助线程。2、它并不是内在线
- 本文实例为大家分享了TextView绘制背景的方法,供大家参考,具体内容如下效果:实现流程:1.初始化:对画笔进行设置mPaintIn =
- 本文实例讲述了C#实现的二维数组排序算法。分享给大家供大家参考,具体如下:class Order{ /// <summar
- 先给大家展示下效果图,感觉不错请参考实例代码。实现思路在flutter中,如果想实现上面的页面切换效果,必然会想到pageView。page
- Redisson分布式锁之前的基于注解的锁有一种锁是基本redis的分布式锁,锁的实现我是基于redisson组件提供的RLock,这篇来看
- 我们在上网的过程中经常看到各种图片,那你知道它是如何实现的吗?接下来就让我们一块探讨一下。 网络图片的浏览可以分为俩部分,基本的页面布局与界
- VSCode配置C语言环境VSCode是一款强大编辑器,开源,免费,海量插件,支持很多编程语言。其中的很多功能可以大大地提高我们的学习与工作