使用AOP來實現接口請求頻率限制

老規矩,定義一個註解:

/*
* 被該註解修飾的方法都會被切面攔截進行請求次數限制
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Limit {
    int maxRequestPerMinute();
}

我們在想要進行頻率限制的方法上添加註解:

@RequestMapping("/")
    @Limit(maxRequestPerMinute = 10)
    public String index(){
        return "index.html";
    }

定義一個切面,代碼如下(代碼很長,可以跳到最後看分析):

/**
 * @author my
 */
@Aspect
@Component
@Order(1)
public class RequestLimitAspect {

    public static final String REQUEST_LIMIT = "requestLimit";
    public static final int MINTUE = 60000;

    @Pointcut("@annotation(wang.ismy.zbq.annotations.Limit)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        var session = getCurrentUserSession();

        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        var a = method.getAnnotation(Limit.class);

        if (session == null) {
            ErrorUtils.error(R.UNKNOWN_ERROR);
        }

        if (session.getAttribute(REQUEST_LIMIT) == null) {
            Map<String, RequestLimitDTO> map = new HashMap<>();
            session.setAttribute(REQUEST_LIMIT, map);
        }
        Map<String, RequestLimitDTO> map = (Map<String, RequestLimitDTO>) session.getAttribute(REQUEST_LIMIT);

        String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();

        if (map.get(methodName) == null) {
            RequestLimitDTO dto = new RequestLimitDTO();
            dto.setLastRequestTime(System.currentTimeMillis());
            dto.setRequestCount(0);
            map.put(methodName, dto);
        } else {
            RequestLimitDTO dto = map.get(methodName);
            // 如果當前請求距離上一次請求時間間隔大於60s,則清空計數器
            if (System.currentTimeMillis()-dto.getLastRequestTime() >= MINTUE) {
                dto.setRequestCount(0);
            }

            dto.increaseCount();
            // 如果當前請求計數器的大於設定的閾值,則拒絕此次請求
            if (dto.getRequestCount() > a.maxRequestPerMinute()) {
                ErrorUtils.error(R.REQUEST_FREQUENTLY);
            }

            dto.setLastRequestTime(System.currentTimeMillis());


        }


    }

    private HttpSession getCurrentUserSession() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return request.getSession();
    }
}

那麼,在這裏談談這個頻率限制的大概實現:

首先,被註解修飾的方法都會經過這個前置通知:

    1)獲取該切面的方法全名。

    2)從session當中通過這個方法全名獲取對應的請求dto(dto使用哈希表與方法全名進行映射):

 * @author my
 */
@Data
public class RequestLimitDTO {

    private Integer requestCount;

    private Long lastRequestTime;

    public synchronized void increaseCount(){
        requestCount++;
    }

    public synchronized void setRequestCount(Integer requestCount) {
        this.requestCount = requestCount;
    }
}

    那麼重點來了:

    算法的核心是這段:

if (map.get(methodName) == null) {
    RequestLimitDTO dto = new RequestLimitDTO();
    dto.setLastRequestTime(System.currentTimeMillis());
    dto.setRequestCount(0);
    map.put(methodName, dto);
} else {
    RequestLimitDTO dto = map.get(methodName);
    // 如果當前請求距離上一次請求時間間隔大於60s,則清空計數器
    if (System.currentTimeMillis()-dto.getLastRequestTime() >= MINTUE) {
        dto.setRequestCount(0);
    }

    dto.increaseCount();
    // 如果當前請求計數器的大於設定的閾值,則拒絕此次請求
    if (dto.getRequestCount() > a.maxRequestPerMinute()) {
        ErrorUtils.error(R.REQUEST_FREQUENTLY);
    }

    dto.setLastRequestTime(System.currentTimeMillis());
}

        這段代碼沒有什麼特別的地方,只是要注意的是increase和setRequestCount方法都要被synchronized關鍵字修飾,避免在併發的情況下出現數據不一致。

 最後,不可否認,爲了快速實現這個功能,這段代碼寫的很爛,很多地方實現得都不優美。

比如

if (System.currentTimeMillis()-dto.getLastRequestTime() >= MINTUE) {

這裏的MINUTE常量可以讓客戶端程序員自行指定 ,這樣就能擁有更多的靈活性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章