关于springboot: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);
    }
}

用拦截器躲避反复申请

需要

开发中可能会常常遇到短时间内因为用户的反复点击导致几秒之内反复的申请,可能就是在这几秒之内因为各种问题,比方网络事务的隔离性等等问题导致了数据的反复等问题,因而在日常开发中必须躲避这类的反复申请操作,明天就用拦截器简略的解决一下这个问题。

思路

在接口执行之前先对指定接口(比方标注某个注解的接口)进行判断,如果在指定的工夫内(比方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");
    }
}

测试

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理