SpringBoot利用拦截器实现避免重复请求

 更新时间:2022年11月22日 09:52:39   作者:iicode  
Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。本文就将利用拦截器实现避免重复请求,感兴趣的小伙伴可以了解一下

拦截器

什么是拦截器

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

测试

到此这篇关于SpringBoot利用拦截器实现避免重复请求的文章就介绍到这了,更多相关SpringBoot拦截器防重复请求内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java代码规范与质量检测插件SonarLint的使用

    Java代码规范与质量检测插件SonarLint的使用

    本文主要介绍了Java代码规范与质量检测插件SonarLint的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • 浅析java修饰符访问权限(动力节点Java学院整理)

    浅析java修饰符访问权限(动力节点Java学院整理)

    Java有四种访问权限,其中三种有访问权限修饰符,分别为private,public和protected,还有一种不带任何修饰符,下面通过本文给大家简单介绍下java修饰符访问权限相关知识,感兴趣的朋友一起学习吧
    2017-04-04
  • SpringBoot整合tkMapper的方法

    SpringBoot整合tkMapper的方法

    项目使用SpringBoot2.0,H2数据库,使用了 Lombok 简化代码,下面是本人使用SpringBoot整合tkMapper的一个小demo,记录下来本人在此处踩得坑
    2022-11-11
  • Ubuntu搭建Java开发环境笔记

    Ubuntu搭建Java开发环境笔记

    这篇文章主要介绍了Ubuntu搭建Java开发环境笔记,本文讲解了JDK安装、MyEclipse安装及第一个Hello world程序等内容,需要的朋友可以参考下
    2015-03-03
  • 使用Maven进行依赖排除的详细步骤

    使用Maven进行依赖排除的详细步骤

    在Maven中,依赖排除是一种常见的技术,用于从项目的依赖中排除特定的传递性依赖,这通常用于解决依赖冲突或避免引入不需要的库,以下是如何在Maven中使用依赖排除的详细步骤,包括代码示例,需要的朋友可以参考下
    2024-11-11
  • MybatisPlus实现数据权限隔离的示例详解

    MybatisPlus实现数据权限隔离的示例详解

    Mybatis Plus对Mybatis做了无侵入的增强,非常的好用,今天就给大家介绍它的其中一个实用功能:数据权限插件,感兴趣的可以跟随小编一起了解下
    2024-04-04
  • 举例讲解Java的Spring框架中AOP程序设计方式的使用

    举例讲解Java的Spring框架中AOP程序设计方式的使用

    这篇文章主要介绍了Java的Spring框架中AOP程序设计方式的使用讲解,文中举的AOP下抛出异常的例子非常实用,需要的朋友可以参考下
    2016-04-04
  • JAVA定义变量与输出详解

    JAVA定义变量与输出详解

    这篇文章主要介绍了JAVA定义变量与输出详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07
  • Kotlin 基础教程之数组容器

    Kotlin 基础教程之数组容器

    这篇文章主要介绍了Kotlin 基础教程之数组容器的相关资料,需要的朋友可以参考下
    2017-06-06
  • java基于JSON实现前后端交互(附代码)

    java基于JSON实现前后端交互(附代码)

    本文主要介绍了java基于JSON实现前后端交互,通过实际代码示例展示了如何前后端JSON交互,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01

最新评论