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项目,如下图:

SpringBoot利用 * 实现避免重复请求

添加依赖

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");
   }
}

测试

SpringBoot利用 * 实现避免重复请求

来源:https://segmentfault.com/a/1190000042858692

标签:SpringBoot, , ,重复,请求
0
投稿

猜你喜欢

  • Android组件Glide实现图片平滑滚动效果

    2022-02-24 22:53:14
  • 详解Vue响应式的部分实现

    2022-12-21 23:25:53
  • C#使用doggleReport生成pdf报表的方法

    2022-12-29 14:55:28
  • java中JSONObject转换为HashMap(方法+main方法调用实例)

    2023-08-10 04:04:08
  • springMvc注解之@ResponseBody和@RequestBody详解

    2022-10-09 17:57:19
  • Java集合框架之Stack Queue Deque使用详解刨析

    2022-06-11 06:10:19
  • Spring的异常重试框架Spring Retry简单配置操作

    2023-11-25 18:27:35
  • 教你快速搭建sona服务及idea使用sona的方法

    2023-11-20 05:22:53
  • Java有哪些操作字符串的类?区别在哪?

    2021-06-02 14:50:54
  • Android实现Activity水平和垂直滚动条的方法

    2021-07-04 13:06:06
  • Spring中bean集合注入的方法详解

    2022-08-26 07:37:41
  • Android签名机制介绍:生成keystore、签名、查看签名信息等方法

    2021-05-25 17:52:24
  • Android自定义可循环的滚动选择器CycleWheelView

    2023-04-06 00:43:16
  • java打印指定年月的日历

    2023-11-11 19:21:19
  • Java中List遍历删除元素remove()的方法

    2022-07-12 12:27:10
  • 深度理解C语言中的关键字static

    2023-03-12 02:47:37
  • Java等待唤醒机制线程通信原理解析

    2022-03-31 00:37:21
  • Android AsyncTask详解及使用方法

    2022-09-24 04:53:29
  • Android小知识之OkHttp的2种请求方式详解

    2021-06-05 04:33:45
  • JAVA如何调用Shell脚本

    2022-11-25 01:13:13
  • asp之家 软件编程 m.aspxhome.com