• SpringBoot之用拦截器避免重复请求


    拦截器

    什么是拦截器

    Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

    如何自定义拦截器

    自定义一个拦截器非常简单,只需要实现HandlerInterceptor这个接口即可,这个接口有三个可实现的方法

    1. preHandle()方法:该方法会在控制器方法前执行,其返回值表示是否知道如何写一个接口。中断后续操作。当其返回值为true时,表示继续向下执行;当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。

    2. postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。

    3. 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    用拦截器规避重复请求

    需求

    开发中可能会经常遇到短时间内由于用户的重复点击导致几秒之内重复的请求,可能就是在这几秒之内由于各种问题,比如网络事务的隔离性等等问题导致了数据的重复等问题,因此在日常开发中必须规避这类的重复请求操作,今天就用拦截器简单的处理一下这个问题。

    思路

    在接口执行之前先对指定接口(比如标注某个注解的接口)进行判断,如果在指定的时间内(比如5秒)已经请求过一次了,则返回重复提交的信息给调用者。

    根据什么判断这个接口已经请求了?

    根据项目的架构可能判断的条件也是不同的,比如IP地址用户唯一标识请求参数请求URI等等其中的某一个或者多个的组合。

    这个具体的信息存放在哪?

    由于是短时间内甚至是瞬间并且要保证定时失效,肯定不能存在事务性数据库中了,因此常用的几种数据库中只有Redis比较合适了。

    实现

    Docker启动一个Redis

    docker pull redis:7.0.4
    
    docker run -itd \
    	--name redis \
    	-p 6379:6379 \
    	redis:7.0.4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建一个Spring Boot项目

    使用idea的Spring Initializr来创建一个Spring Boot项目,如下图:

    在这里插入图片描述

    添加依赖

    pom.xml文件如下

    
    <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.0modelVersion>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.7.5version>
            <relativePath/> 
        parent>
    
        <groupId>com.examplegroupId>
        <artifactId>springboot_06artifactId>
        <version>0.0.1-SNAPSHOTversion>
        <name>springboot_06name>
        <description>Demo project for Spring Bootdescription>
    
        <properties>
            <java.version>1.8java.version>
        properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <optional>trueoptional>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
                
                <exclusions>
                    <exclusion>
                        <groupId>redis.clientsgroupId>
                        <artifactId>jedisartifactId>
                    exclusion>
                    <exclusion>
                        <groupId>io.lettucegroupId>
                        <artifactId>lettuce-coreartifactId>
                    exclusion>
                exclusions>
            dependency>
    
            <dependency>
                <groupId>redis.clientsgroupId>
                <artifactId>jedisartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintagegroupId>
                        <artifactId>junit-vintage-engineartifactId>
                    exclusion>
                exclusions>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    
    project>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    配置Redis

    application.properties

    spring.redis.host=127.0.0.1
    spring.redis.database=1
    spring.redis.port=6379
    
    • 1
    • 2
    • 3

    定义一个注解

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    创建一个拦截器

    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方法之前执行
         * 

    * 判断条件仅仅是用了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; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    配置拦截器

    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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    写个测试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");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    测试

    在这里插入图片描述

  • 相关阅读:
    LeetCode的第 363 场周赛——记录+补题
    一文吃透KMP
    hadoop3.x入门到精通-阶段五(图解剖析MapReduce源码分析)
    git 基础之分支操作(三)
    vue3.0 组件传参
    使用CyberController来将旧手机改造成电脑外挂------手机交互翻译、人脸解锁、语音识别....各个功能等你来探索
    【软件工程之美 - 专栏笔记】34 | 账号密码泄露成灾,应该怎样预防?
    华为云开源低代码引擎 TinyEngine 正式发布
    spring的自动装配
    初始Tomcat(Tomcat的基础介绍)
  • 原文地址:https://blog.csdn.net/ln_ydc/article/details/127974163