Spring Boot第七彈,別再問我攔截器如何配置了!!!

持續原創輸出,點擊上方藍字關注我吧

前言

上篇文章講了Spring Boot的WEB開發基礎內容,相信讀者朋友們已經有了初步的了解,知道如何寫一個介面。

今天這篇文章來介紹一下攔截器在Spring Boot中如何自定義以及配置。

Spring Boot 版本

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

什麼是攔截器?

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比較合適了。

如何實現?

第一步,先自定義一個註解,可以標註在類或者方法上,如下:

@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://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配置的。

總結

至此,攔截器的內容就介紹完了,其實配置起來很簡單,沒什麼重要的內容。