本文将介绍在spring项目中自定义注解,借助redis实现接口的限流

自定义注解类

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. /**
  6. * 基于注解的请求限制
  7. */
  8. @Target({ElementType.TYPE, ElementType.METHOD})
  9. @Retention(RetentionPolicy.RUNTIME)
  10. public @interface AccessLimit {
  11. /**
  12. * 请求限制数
  13. * @return
  14. */
  15. int limit();
  16. /**
  17. * 时间范围
  18. * @return
  19. */
  20. int timeScope();
  21. }

使用注解

我们在需要进行接口防刷的类或者方法上加上该注解即可,

  1. /**
  2. * 得到秒杀地址
  3. * 由于秒杀地址较为重要和敏感,为了防止恶意用户刷接口,
  4. * 我们将秒杀接口作为动态的
  5. * @param user
  6. * @param goodsId
  7. * @param tryCode
  8. * @return
  9. */
  10. @GetMapping("/path")
  11. @ResponseBody
  12. @AccessLimit(limit = 5, timeScope = 5) // 限制5秒内只能请求5次
  13. public Result<String> getMiaoshaPath(HttpServletRequest request, User user, long goodsId, String tryCode) {
  14. // 验证码校验
  15. Boolean verifyPass = kaptchaService.imgVerifyCode(user, goodsId, tryCode);
  16. if (!verifyPass) {
  17. log.info("【执行秒杀】-- 验证码错误");
  18. throw new FlashSaleException(KAPTCHA_VERIFY_FAIL);
  19. }
  20. String path = miaoshaService.createPath(user, goodsId);
  21. return Result.success(path);
  22. };

使用拦截器,在拦截方法时拿到注解上的属性

  1. @Override
  2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  3. // 从redis中取到值
  4. Cookie cookie = CookieUtil.get(request, Constants.Cookie.TOKEN);
  5. if (cookie == null) {
  6. throw new FlashSaleException(AuthFailEnum.COOKIE_HAVE_NO_TOKEN);
  7. }
  8. StringRedisTemplate redisTemplate = ApplicationContextHolder.get().getBean("stringRedisTemplate", StringRedisTemplate.class);
  9. String userStr = redisTemplate.opsForValue().get(cookie.getValue());
  10. if (StringUtils.isEmpty(userStr)) {
  11. throw new FlashSaleException(AuthFailEnum.REDIS_HAVE_NOT_TOKEN);
  12. }
  13. User user = JSON.parseObject(userStr, User.class);
  14. UserContextHolder.set(user);
  15. if (handler instanceof HandlerMethod) {
  16. HandlerMethod hm = (HandlerMethod) handler;
  17. // 拿到注解的内容
  18. AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
  19. if (accessLimit == null) {
  20. // 不需要限流验证
  21. return true;
  22. } else {
  23. // 需要限流验证
  24. int limit = accessLimit.limit();
  25. int timeScope = accessLimit.timeScope();
  26. // 次数校验(借助redis实现基于用户的限流验证)
  27. String requestURI = request.getRequestURI();
  28. final String redisKey = Constants.Cache.PATH_COUNT_PREFIX + user.getId() + ":" + requestURI;
  29. String currentCount = redisTemplate.opsForValue().get(redisKey);
  30. if (!StringUtils.isEmpty(currentCount)) {
  31. int count = Integer.valueOf(currentCount);
  32. if (count < limit) {
  33. redisTemplate.opsForValue().increment(redisKey, 1);
  34. } else {
  35. // 访问过于频繁
  36. throw new FlashSaleException(PATH_LIMIT_REACHED);
  37. }
  38. } else {
  39. redisTemplate.opsForValue().set(redisKey, "1", timeScope, TimeUnit.SECONDS);
  40. }
  41. }
  42. }
  43. UserContextHolder.set(user);
  44. renewExpiretime(response, cookie, userStr);
  45. return true;
  46. }

总结

 在实现了上述代码后,当我们访问到带有AccessLimit注解的方法或类时,只要拦截器拦截了该请求,就能通过getMethodAnnotation()拿到注解上的limit和timeScope属性,然后借助redis实现限流;比如某些接口我们可能想要2秒只能访问1次,那么就把limit=1 timeScope=2,某些接口我们想要限制1分钟访问10次,就把limit=10, timeScope=60