[springboot series] customize an interceptor, with source code

Spring Boot version

The version of Spring Boot on which this article is based is 2.3.4.RELEASE.

What is an interceptor?

Interceptor in Spring MVC is similar to Filter in Servlet. It is mainly used to intercept user requests and handle them accordingly. For example, the interceptor can perform permission verification, log the request information, and determine whether the user logs in.

How to customize an interceptor?

Customizing an interceptor is very simple. You only need to implement the HandlerInterceptor interface, which has three methods that can be implemented, as follows:

  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 the return value is true, it means to continue to execute downward; When the return value is false, all subsequent operations (including calling the next interceptor and method execution in the controller class) will be interrupted.

  2. postHandle() method: this method will be executed after the controller method is called and before the view is parsed. You can further modify the models and views in the request domain through this method.

  3. afterCompletion() method: this method will be executed after the entire request is completed, that is, after the view rendering is completed. This method can be used to clean up resources and record log information.

How to make it work in Spring Boot?

In fact, it is very simple to make Spring Boot effective. You only need to define a configuration class, implement the WebMvcConfigurer interface, and implement the addInterceptors() method. The code is shown 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);
    }
}

Take a chestnut

During development, we may often encounter repeated requests within a few seconds due to repeated clicks of users. It may be that various problems, such as network and transaction isolation, lead to data duplication within a few seconds. Therefore, we must avoid such repeated requests in daily development. Today, we will simply deal with this problem with interceptors.

thinking

Before the interface is executed, judge the specified interface (such as the interface marked with an annotation). If the request has been made once within the specified time (such as 5 seconds), the repeatedly submitted information will be returned to the caller.

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

Different conditions may be judged according to the project architecture, such as IP address, user unique identifier, request parameter, request URI, etc.

Where is this specific information stored?

Because it is a short time or even an instant, and it is necessary to ensure the timing of failure, it must not exist in the transactional database. Therefore, among the commonly used databases, only Redis is suitable.

How to achieve it?

The first step is to customize an annotation, which can be marked on a class or method, as follows:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * Default expiration time: 5 seconds
     */
    long seconds() default 5;
}

Step 2: create an interceptor and inject it into the IOC container. The implementation idea is very simple. Judge whether the @ RepeatSubmit annotation is marked on the controller class or method. If so, intercept and judge. Otherwise, skip. The code is as follows:

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

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

    /**
     * preHandler Method, executed before the controller method
     * 
     * The judgment condition is only uri. In actual development, a unique identification condition is combined according to the actual situation.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            //Only @ RepeatSubmit annotation is intercepted
            HandlerMethod method=(HandlerMethod)handler;
            //@ RepeatSubmit annotated 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 on repeated submission, skip directly
            if (Objects.isNull(repeatSubmitByMethod)&&Objects.isNull(repeatSubmitByCls))
                return true;

            //todo: combination judgment conditions. This is just a demonstration. In actual projects, conditions are combined according to the architecture
            //Requested URI
            String uri = request.getRequestURI();

            //Returns false if it exists, and 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 indicates that the request has been made, and an exception is thrown directly. The global exception is processed to return the specified information
            if (ifAbsent!=null&&!ifAbsent)
                throw new RepeatSubmitException();
        }
        return true;
    }
}

Step 3: configure the interceptor in Spring Boot. The code is as follows:

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

OK, the interceptor has been configured. Just mark @ RepeatSubmit on the interface to be intercepted, as follows:

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

    @RequestMapping("/login")
    public String login(){
        return "login success";
    }
}

At this time, request the URI:http://localhost:8080/springboot-demo/user/login can only be requested once within 5 seconds.

Note: the timeout time marked on the method will cover the time on the class because of the following code:

Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "", Objects.nonNull(repeatSubmitByMethod)?repeatSubmitByMethod.seconds():repeatSubmitByCls.seconds(), TimeUnit.SECONDS);

The expiration time of this code first takes the value configured in repeatSubmitByMethod. If it is null, it takes the value configured in repeatSubmitByCls.

demo details Source code: GitHub - wufaqidong / springboot intercept: Custom interceptor

Tags: Java Spring Spring Boot

Posted by ghurtado on Tue, 23 Aug 2022 04:29:05 +0300