SpringBoot利用攔截器實(shí)現(xiàn)避免重復(fù)請(qǐng)求
攔截器
什么是攔截器
Spring MVC中的攔截器(Interceptor)類似于Servlet中的過(guò)濾器(Filter),它主要用于攔截用戶請(qǐng)求并作相應(yīng)的處理。例如通過(guò)攔截器可以進(jìn)行權(quán)限驗(yàn)證、記錄請(qǐng)求信息的日志、判斷用戶是否登錄等。
如何自定義攔截器
自定義一個(gè)攔截器非常簡(jiǎn)單,只需要實(shí)現(xiàn)HandlerInterceptor這個(gè)接口即可,這個(gè)接口有三個(gè)可實(shí)現(xiàn)的方法
preHandle()
方法:該方法會(huì)在控制器方法前執(zhí)行,其返回值表示是否知道如何寫(xiě)一個(gè)接口。中斷后續(xù)操作。當(dāng)其返回值為true
時(shí),表示繼續(xù)向下執(zhí)行;當(dāng)其返回值為false
時(shí),會(huì)中斷后續(xù)的所有操作(包括調(diào)用下一個(gè)攔截器和控制器類中的方法執(zhí)行等)。postHandle()
方法:該方法會(huì)在控制器方法調(diào)用之后,且解析視圖之前執(zhí)行??梢酝ㄟ^(guò)此方法對(duì)請(qǐng)求域中的模型和視圖做出進(jìn)一步的修改。afterCompletion()
方法:該方法會(huì)在整個(gè)請(qǐng)求完成,即視圖渲染結(jié)束之后執(zhí)行??梢酝ㄟ^(guò)此方法實(shí)現(xiàn)一些資源清理、記錄日志信息等工作。
如何讓攔截器在Spring Boot中生效
想要在Spring Boot生效其實(shí)很簡(jiǎn)單,只需要定義一個(gè)配置類,實(shí)現(xiàn)WebMvcConfigurer
這個(gè)接口,并且實(shí)現(xiàn)其中的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); } }
用攔截器規(guī)避重復(fù)請(qǐng)求
需求
開(kāi)發(fā)中可能會(huì)經(jīng)常遇到短時(shí)間內(nèi)由于用戶的重復(fù)點(diǎn)擊導(dǎo)致幾秒之內(nèi)重復(fù)的請(qǐng)求,可能就是在這幾秒之內(nèi)由于各種問(wèn)題,比如網(wǎng)絡(luò),事務(wù)的隔離性等等問(wèn)題導(dǎo)致了數(shù)據(jù)的重復(fù)等問(wèn)題,因此在日常開(kāi)發(fā)中必須規(guī)避這類的重復(fù)請(qǐng)求操作,今天就用攔截器簡(jiǎn)單的處理一下這個(gè)問(wèn)題。
思路
在接口執(zhí)行之前先對(duì)指定接口(比如標(biāo)注某個(gè)注解的接口)進(jìn)行判斷,如果在指定的時(shí)間內(nèi)(比如5秒)已經(jīng)請(qǐng)求過(guò)一次了,則返回重復(fù)提交的信息給調(diào)用者。
根據(jù)什么判斷這個(gè)接口已經(jīng)請(qǐng)求了?
根據(jù)項(xiàng)目的架構(gòu)可能判斷的條件也是不同的,比如IP地址,用戶唯一標(biāo)識(shí)、請(qǐng)求參數(shù)、請(qǐng)求URI等等其中的某一個(gè)或者多個(gè)的組合。
這個(gè)具體的信息存放在哪?
由于是短時(shí)間內(nèi)甚至是瞬間并且要保證定時(shí)失效,肯定不能存在事務(wù)性數(shù)據(jù)庫(kù)中了,因此常用的幾種數(shù)據(jù)庫(kù)中只有Redis比較合適了。
實(shí)現(xiàn)
Docker啟動(dòng)一個(gè)Redis
docker pull redis:7.0.4 docker run -itd \ --name redis \ -p 6379:6379 \ redis:7.0.4
創(chuàng)建一個(gè)Spring Boot項(xiàng)目
使用idea的Spring Initializr來(lái)創(chuàng)建一個(gè)Spring Boot項(xiàng)目,如下圖:
添加依賴
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的版本默認(rèn)采用的連接池技術(shù)是jedis 2.0以上版本默認(rèn)連接池是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
定義一個(gè)注解
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 { /** * 默認(rèn)失效時(shí)間5秒 * * @return */ long seconds() default 5; }
創(chuàng)建一個(gè)攔截器
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; /** * 重復(fù)請(qǐng)求的攔截器 * * @Component:該注解將其注入到IOC容器中 */ @Slf4j @Component public class RepeatSubmitInterceptor implements HandlerInterceptor { /** * Redis的API */ @Autowired private StringRedisTemplate stringRedisTemplate; /** * preHandler方法,在controller方法之前執(zhí)行 * <p> * 判斷條件僅僅是用了uri,實(shí)際開(kāi)發(fā)中根據(jù)實(shí)際情況組合一個(gè)唯一識(shí)別的條件。 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { // 只攔截標(biāo)注了@RepeatSubmit該注解 HandlerMethod method = (HandlerMethod) handler; // 標(biāo)注在方法上的@RepeatSubmit RepeatSubmit repeatSubmitByMethod = AnnotationUtils.findAnnotation(method.getMethod(), RepeatSubmit.class); // 標(biāo)注在controler類上的@RepeatSubmit RepeatSubmit repeatSubmitByCls = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), RepeatSubmit.class); // 沒(méi)有限制重復(fù)提交,直接跳過(guò) if (Objects.isNull(repeatSubmitByMethod) && Objects.isNull(repeatSubmitByCls)) { log.info("isNull"); return true; } // todo: 組合判斷條件,這里僅僅是演示,實(shí)際項(xiàng)目中根據(jù)架構(gòu)組合條件 //請(qǐng)求的URI String uri = request.getRequestURI(); //存在即返回false,不存在即返回true Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "", Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod.seconds() : repeatSubmitByCls.seconds(), TimeUnit.SECONDS); //如果存在,表示已經(jīng)請(qǐng)求過(guò)了,直接拋出異常,由全局異常進(jìn)行處理返回指定信息 if (ifAbsent != null && !ifAbsent) { String msg = String.format("url:[%s]重復(fù)請(qǐng)求", 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); } }
寫(xiě)個(gè)測(cè)試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; /** * 標(biāo)注了@RepeatSubmit注解,全部的接口都需要攔截 * */ @Slf4j @RestController @RequestMapping("/user") @RepeatSubmit public class UserController { @RequestMapping("/save") public ResponseEntity save() { log.info("/user/save"); return ResponseEntity.ok("save success"); } }
測(cè)試
到此這篇關(guān)于SpringBoot利用攔截器實(shí)現(xiàn)避免重復(fù)請(qǐng)求的文章就介紹到這了,更多相關(guān)SpringBoot攔截器防重復(fù)請(qǐng)求內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Java開(kāi)發(fā)中的安全編碼問(wèn)題
這篇文章主要介紹了淺談Java開(kāi)發(fā)中的安全編碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10Mybatis-Plus中update()和updateById()將字段更新為null
本文主要介紹了Mybatis-Plus中update()和updateById()將字段更新為null,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Mybatis查詢返回Map<String,Object>類型的實(shí)現(xiàn)
本文主要介紹了Mybatis查詢返回Map<String,Object>類型的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07springboot結(jié)合mysql主從來(lái)實(shí)現(xiàn)讀寫(xiě)分離的方法示例
這篇文章主要介紹了springboot結(jié)合mysql主從來(lái)實(shí)現(xiàn)讀寫(xiě)分離的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Mybatis-plus多租戶項(xiàng)目實(shí)戰(zhàn)進(jìn)階指南
多租戶是一種軟件架構(gòu)技術(shù),在多用戶的環(huán)境下共有同一套系統(tǒng),并且要注意數(shù)據(jù)之間的隔離性,下面這篇文章主要給大家介紹了關(guān)于Mybatis-plus多租戶項(xiàng)目實(shí)戰(zhàn)進(jìn)階的相關(guān)資料,需要的朋友可以參考下2022-02-02Java Quartz觸發(fā)器CronTriggerBean配置用法詳解
這篇文章主要介紹了Java Quartz觸發(fā)器CronTriggerBean配置用法詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Spring Boot通過(guò)Junit實(shí)現(xiàn)單元測(cè)試過(guò)程解析
這篇文章主要介紹了Spring Boot通過(guò)Junit實(shí)現(xiàn)單元測(cè)試過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01解析MapStruct轉(zhuǎn)換javaBean時(shí)出現(xiàn)的詭異事件
在項(xiàng)目中用到了MapStruct,對(duì)其可以轉(zhuǎn)換JavaBean特別好奇,今天小編給大家分享一個(gè)demo給大家講解MapStruct轉(zhuǎn)換javaBean時(shí)出現(xiàn)的詭異事件,感興趣的朋友一起看看吧2021-09-09Spring三級(jí)緩存思想解決循環(huán)依賴總結(jié)分析
這篇文章主要為大家介紹了Spring三級(jí)緩存思想解決循環(huán)依賴總結(jié)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08