SpringBoot uses interceptors to avoid repeated requests

interceptor

What is an interceptor

The Interceptor in Spring MVC is similar to the Filter in Servlet. It is mainly used to intercept user requests and process them accordingly. For example, the interceptor can perform authority verification, record the log of request information, and judge whether the user is logged in or not.

How to customize the interceptor

It is very simple to customize an interceptor. You only need to implement the HandlerInterceptor interface. This interface has three implementable methods

  1. preHandle() method: This method will be executed before the controller method, and its return value indicates whether it knows how to write an interface. Interrupt subsequent operations. When its return value is true, it means continue to execute downward; when its return value is false, it will interrupt all subsequent operations (including calling the next interceptor and method execution in the controller class, etc.).

  2. postHandle() method: This method will be executed after the controller method is called and before the view is parsed. Further modifications to models and views in the request domain can be made through this method.

  3. afterCompletion() method: This method will be executed after the entire request is completed, that is, after the view rendering ends. You can use this method to achieve some resource cleaning, log information and other tasks.

How to make the interceptor take effect in Spring Boot

It is actually very simple to take effect in Spring Boot. You only need to define a configuration class, implement the WebMvcConfigurer interface, and implement the addInterceptors() method. The code is as follows:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private XXX xxx;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // uri not intercepted
        final String[] commonExclude = {}};
        registry.addInterceptor(xxx).excludePathPatterns(commonExclude);
    }
}

Avoid duplicate requests with interceptors

need

In development, you may often encounter repeated requests within a few seconds due to repeated clicks by users in a short period of time. It may be that within a few seconds, due to various problems, such as network, transaction isolation, etc., the data Therefore, in daily development, such repeated request operations must be avoided. Today, we will simply use interceptors to deal with this problem.

train of thought

Before the interface is executed, the specified interface (such as the interface marked with a certain annotation) is judged. If it has been requested once within the specified time (such as 5 seconds), the repeated submission information will be returned to the caller.

What is the basis for judging that this interface has been requested?

The conditions that may be judged according to the structure of the project are also different, such as IP address, unique user ID, request parameters, request URI, etc., one or more of them.

Where is this specific information stored?

Since it is short-term or even instantaneous and must be guaranteed to fail at regular intervals, it must not exist in a transactional database. Therefore, among the commonly used databases, only Redis is more suitable.

accomplish

Docker starts a Redis

docker pull redis:7.0.4

docker run -itd \
	--name redis \
	-p 6379:6379 \
	redis:7.0.4

Create a Spring Boot project

Use idea's Spring Initializr to create a Spring Boot project, as shown below:

add dependencies

The pom.xml file is as follows

<?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 configuration-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!-- 1.5 The connection pool technology used by default in the version is jedis  2.0 The default connection pool of the above version is lettuce, adopt here jedis´╝îSo need to exclude lettuce of 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>

Configure Redis

application.properties

spring.redis.host=127.0.0.1
spring.redis.database=1
spring.redis.port=6379

define an annotation

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 {
    /**
     * The default expiration time is 5 seconds
     *
     * @return
     */
    long seconds() default 5;
}

Create an interceptor

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;

/**
 * Interceptor for repeated requests
 *
 * @Component: This annotation injects it into the IOC container
 */
@Slf4j
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

    /**
     * Redis API
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * preHandler method, executed before the controller method
     * <p>
     * The judgment condition only uses uri, and a unique identification condition is combined according to the actual situation in actual development.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            // Only intercept the annotation marked @RepeatSubmit
            HandlerMethod method = (HandlerMethod) handler;
            // @RepeatSubmit marked on the method
            RepeatSubmit repeatSubmitByMethod = AnnotationUtils.findAnnotation(method.getMethod(), RepeatSubmit.class);
            // @RepeatSubmit marked on the controller class
            RepeatSubmit repeatSubmitByCls = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), RepeatSubmit.class);
            // There is no limit to repeated submissions, skip directly
            if (Objects.isNull(repeatSubmitByMethod) && Objects.isNull(repeatSubmitByCls)) {
                log.info("isNull");
                return true;
            }

            // todo: Combination judgment conditions, here is just a demonstration, in actual projects, the conditions are combined according to the architecture
            //Request URI
            String uri = request.getRequestURI();

            //Returns false if it exists, returns true if it does not exist
            Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "",
                    Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod.seconds() : repeatSubmitByCls.seconds(), TimeUnit.SECONDS);

            //If it exists, it means that the request has been made, an exception is thrown directly, and the specified information is returned for processing by the global exception
            if (ifAbsent != null && !ifAbsent) {
                String msg = String.format("url:[%s]Repeat request", uri);
                log.warn(msg);
                // throw new RepeatSubmitException(msg);
                throw new Exception(msg);
            }
        }
        return true;
    }
}

Configure interceptors

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 not intercepted
        final String[] commonExclude = {"/error", "/files/**"};
        registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns(commonExclude);
    }
}

Write a test 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;

/**
 * Annotated with @RepeatSubmit, all interfaces need to be intercepted
 *
 */
@Slf4j
@RestController
@RequestMapping("/user")
@RepeatSubmit
public class UserController {

    @RequestMapping("/save")
    public ResponseEntity save() {
        log.info("/user/save");
        return ResponseEntity.ok("save success");
    }
}

test

Tags: Spring Boot

Posted by EternalSorrow on Mon, 21 Nov 2022 22:02:03 +0530