Java使用自定义注解限制接口调用频率

青旬

        记录一次在公司遇到的错误:当时对接了高德的周边搜索接口开放给前端调用,但是只是过了3个小时前端就过来找我说接口报错了,我看日志发现问题是高德接口调用次数在当日已经超过了5000次所以高德做限制了,于是决定做接口我调用频率限制的代码。

        限制接口调用频率的方法一般都是通过时间来判断,所以最好的办法就是使用redis来记录接口调用的时间。

        这里我先放上使用 HashMap<String, Long> 代替redis的一种非常简单的写法。

  1. import java.time.LocalDateTime;
  2. import java.util.HashMap;
  3. /**
  4. * @author QingXun123
  5. * @version 1.0.0
  6. * @since 2023-08-29
  7. */
  8. public class SubmitBufferSingleton {
  9. private static HashMap<String, Long> hashMap = new HashMap<>();
  10. private SubmitBufferSingleton() {
  11. }
  12. public static HashMap<String, Long> getInstance() {
  13. return hashMap;
  14. }
  15. }

        SubmitBufferSingleton 用来获取唯一的 HashMap<String, Long>,其中的Value是从 1970-01-01T00:00:00Z(协调世界时,UTC)到当前时间点之间的毫秒数。

  1. import org.springframework.core.Ordered;
  2. import org.springframework.core.annotation.Order;
  3. import java.lang.annotation.*;
  4. /**
  5. * @author QingXun123
  6. * @version 1.0.0
  7. * @since 2023-08-29
  8. */
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Target({ElementType.TYPE, ElementType.METHOD})
  11. @Documented
  12. @Order(Ordered.HIGHEST_PRECEDENCE)
  13. public @interface RequestLimit {
  14. long time() default 5000;
  15. }

         time是调用接口的间隔时间,默认是 5000 毫秒。

  1. import com.qingxun.aspect.annotation.RequestLimit;
  2. import com.qingxun.singleton.SubmitBufferSingleton;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.aspectj.lang.ProceedingJoinPoint;
  5. import org.aspectj.lang.annotation.Around;
  6. import org.aspectj.lang.annotation.Aspect;
  7. import org.springframework.stereotype.Component;
  8. import org.springframework.web.context.request.RequestContextHolder;
  9. import org.springframework.web.context.request.ServletRequestAttributes;
  10. import javax.servlet.http.HttpServletRequest;
  11. import java.time.Instant;
  12. import java.util.HashMap;
  13. /**
  14. * @author QingXun123
  15. * @version 1.0.0
  16. * @since 2023-08-29
  17. */
  18. @Aspect
  19. @Component
  20. @Slf4j
  21. public class NoRepeatSubmitAop {
  22. @Synchronized // 作用是创建一个互斥锁,保证只有一个线程对 SubmitBufferSingleton.getInstance() 这个变量进行修改。
  23. @Around("execution(* com.example..client.amap.controller..*.*(..)) && @annotation(nrs)") // 这里要根据自己项目中的controller包路径来配置好
  24. public Object arround(ProceedingJoinPoint pjp, RequestLimit nrs) throws Throwable {
  25. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  26. HttpServletRequest request = attributes.getRequest();
  27. Object[] args = pjp.getArgs();
  28. String key = getIp(request) + " :" + request.getServletPath();
  29. Long time = nrs.time();
  30. Object o = null;
  31. HashMap<String, Long> hashMap = SubmitBufferSingleton.getInstance();
  32. long nowTime = Instant.now().toEpochMilli();
  33. if (!hashMap.containsKey(key)) {
  34. hashMap.put(key, nowTime + time);
  35. o = pjp.proceed();
  36. return o;
  37. } else {
  38. if (nowTime > hashMap.get(key)) {
  39. hashMap.put(key, nowTime + time);
  40. o = pjp.proceed();
  41. return o;
  42. } else {
  43. log.error("操作过于频繁 {}", key);
  44. return "操作过于频繁";
  45. }
  46. }
  47. }
  48. // 获取调用者ip
  49. private static String getIp(HttpServletRequest request){
  50. String ip = request.getHeader("x-forwarded-for");
  51. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  52. ip = request.getHeader("Proxy-Client-IP");
  53. }
  54. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  55. ip = request.getHeader("WL-Proxy-Client-IP");
  56. }
  57. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  58. ip = request.getHeader("HTTP_CLIENT_IP");
  59. }
  60. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  61. ip = request.getHeader("HTTP_X_FORWARDED_FOR");
  62. }
  63. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  64. ip = request.getRemoteAddr();
  65. }
  66. return ip;
  67. }
  68. }

        这里其实可以直接将 HashMap<String, Long> 放到 NoRepeatSubmitAop 中的。将这个分离出来是为了解耦。

  1. import com.qingxun.singleton.SubmitBufferSingleton;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.scheduling.annotation.Scheduled;
  4. import org.springframework.stereotype.Component;
  5. import java.time.Instant;
  6. import java.time.LocalDateTime;
  7. import java.util.HashMap;
  8. import java.util.Iterator;
  9. import java.util.Map;
  10. /**
  11. * @author QingXun123
  12. * @version 1.0.0
  13. * @since 2023-08-29
  14. */
  15. @Component
  16. @Slf4j
  17. public class NoRepeatSubmitTask {
  18. @Scheduled(cron = "0 0 1 * * ?")
  19. public void start() {
  20. HashMap<String, Long> hashMap = SubmitBufferSingleton.getInstance();
  21. Iterator<Map.Entry<String, Long>> iterator = hashMap.entrySet().iterator();
  22. while (iterator.hasNext()) {
  23. Map.Entry<String, Long> next = iterator.next();
  24. String key = next.getKey();
  25. Long value = next.getValue();
  26. if (value > Instant.now().toEpochMilli()) {
  27. hashMap.remove(key);
  28. }
  29. }
  30. // 如果对时间没有特别严格的要求就直接clear
  31. // hashMap.clear();
  32. }
  33. }

        这里设置一个定时器在每天的凌晨1点清理一下HashMap数据,防止 HashMap<String, Long> 越来越大。将 HashMap<String, Long> 从 NoRepeatSubmitAop 中抽出来的优点在这里也体现出来了。

        如果使用了redis就不需要用到定时器,直接设置好过期时间让redis自己删除就可以了。

  1. import com.qingxun.aspect.annotation.RequestLimit;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.springframework.data.redis.core.RedisTemplate;
  7. import org.springframework.stereotype.Component;
  8. import org.springframework.web.context.request.RequestContextHolder;
  9. import org.springframework.web.context.request.ServletRequestAttributes;
  10. import javax.annotation.Resource;
  11. import javax.servlet.http.HttpServletRequest;
  12. import java.util.concurrent.TimeUnit;
  13. /**
  14. * @author QingXun123
  15. * @version 1.0.0
  16. * @since 2023-08-29
  17. */
  18. @Aspect
  19. @Component
  20. @Slf4j
  21. public class NoRepeatSubmitAop {
  22. @Resource
  23. public RedisTemplate redisTemplate;
  24. //@Synchronized // 根据情况使用
  25. @Around("execution(* com.example.controller..*.*(..)) && @annotation(nrs)") // 这里要根据自己项目中的controller包路径来配置好
  26. public Object arround(ProceedingJoinPoint pjp, RequestLimit nrs) throws Throwable {
  27. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  28. HttpServletRequest request = attributes.getRequest();
  29. Object[] args = pjp.getArgs();
  30. String key = getIp(request) + " :" + request.getServletPath();
  31. Object o = null;
  32. Long time = nrs.time();
  33. if (!redisTemplate.hasKey(key)) {
  34. redisTemplate.opsForValue().set(key, 0, time.intValue(), TimeUnit.MILLISECONDS);
  35. o = pjp.proceed();
  36. return o;
  37. } else {
  38. log.error("操作过于频繁 {}", key);
  39. return "操作过于频繁";
  40. }
  41. }
  42. // 获取调用者ip
  43. private static String getIp(HttpServletRequest request){
  44. String ip = request.getHeader("x-forwarded-for");
  45. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  46. ip = request.getHeader("Proxy-Client-IP");
  47. }
  48. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  49. ip = request.getHeader("WL-Proxy-Client-IP");
  50. }
  51. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  52. ip = request.getHeader("HTTP_CLIENT_IP");
  53. }
  54. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  55. ip = request.getHeader("HTTP_X_FORWARDED_FOR");
  56. }
  57. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  58. ip = request.getRemoteAddr();
  59. }
  60. return ip;
  61. }
  62. }

        这里直接判断redis中是否有这个key,没有就说明没有调用过这个接口或者已经超过了限制时间,所以后端就可以返回数据给前端。


        测试一下是否有用。

         这里我们简单写了一个测试接口,然后在 swagger 中调用成功。

         再点一下就失败了。

        测试通过。

        事实证明后端还是不能相信前端,前端调试3个小时调了我5000次的接口😂。