• 前言

    上篇文章讲了Spring Boot的WEB开发基础内容,相信读者朋友们已经有了初步的了解,知道如何写一个接口。

    今天这篇文章来介绍一下拦截器在Spring Boot中如何自定义以及配置。

    Spring Boot 版本

    本文基于的Spring Boot的版本是2.3.4.RELEASE

    什么是拦截器?

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

    如何自定义一个拦截器?

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

    如何使其在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比较合适了。

    如何实现?

    第一步,先自定义一个注解,可以标注在类或者方法上,如下:

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RepeatSubmit {
        /**
         * 默认失效时间5秒
         */
        long seconds() default 5;
    }
    

    第二步,创建一个拦截器,注入到IOC容器中,实现的思路很简单,判断controller的类或者方法上是否标注了@RepeatSubmit这个注解,如果标注了,则拦截判断,否则跳过,代码如下:

    /**
     * 重复请求的拦截器
     * @Component:该注解将其注入到IOC容器中
     */
    @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))
                    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)
                    throw new RepeatSubmitException();
            }
            return true;
        }
    }
    

    第三步,在Spring Boot中配置这个拦截器,代码如下:

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

    OK,拦截器已经配置完成,只需要在需要拦截的接口上标注@RepeatSubmit这个注解即可,如下:

    @RestController
    @RequestMapping("/user")
    //标注了@RepeatSubmit注解,全部的接口都需要拦截
    @RepeatSubmit
    public class LoginController {
    
        @RequestMapping("/login")
        public String login(){
            return "login success";
        }
    }
    

    此时,请求这个URI:http://localhost:8080/springboot-demo/user/login在5秒之内只能请求一次。

    注意:标注在方法上的超时时间会覆盖掉类上的时间,因为如下一段代码:

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

    这段代码的失效时间先取值repeatSubmitByMethod中配置的,如果为null,则取值repeatSubmitByCls配置的。

    总结

    至此,拦截器的内容就介绍完了,其实配置起来很简单,没什么重要的内容。

    上述例子中的源代码有需要的朋友公号回复关键词拦截器即可获取。

    10-08 11:18