接口的节流是开发过程中为了防止单一微服务模块突然遭受太多并发导致用户服务不流畅而产生的业务需求,就是实现在固定时间内访问同一个接口的次数也固定。开发过程中通常采用redis去作为缓存去快存快取,对于需求次数较多的数据可以存储在redis内部,那么我们能不能采用redis实现接口的节流呢?
一、先说说自定义注解annotation
我们知道注解的一般结构是:
@Target({ElementType.METHOD})@Retention(value = RetentionPolicy.RUNTIME)@Repeatablepublic @interface 注解名{ 类型 属性名() default 默认值, ...}
Target是指定此注解能打在哪些地方,METHOD就是打在方法上,PARAMTERS就是打在参数上等等。
Retention是指定注解的生命周期,RUNTIME>CLASS>SOURCE。
Repeatable是指该注解能否在同一个类或者方法或者属性下被重复使用。
当我们将一个注解规定之后就可以直接通过@注解名去使用它了。
二、再聊一聊AOP
之前学习Spring的时候有说过aop的两种使用方法,这里主要描述的是aop的注解使用方法,首先要想使用aop我们需要拥有aop的依赖
org.aspectj aspectjweaver
之后我们来观察这个依赖jar包里的相关常用注解:
Aspect是切面类注解,打上的类都会变成一个切面类,里面可以定义一些切点,并设置该切点的前置后置环绕等增强。
Aspect的源码:
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface Aspect { String value() default "";}
补充一下切点表达式的格式:execution(切入点方法修饰符 返回值类型 类的全限定名.方法名(..)),其中 * 代表全部。
三、应用案例:实现模拟的redis节流
首先导入相关的依赖之后,我们编写自定义注解@InCache:
// ElementType.FIELD用于属性,ElementType.PARAMETER用于方法参数,ElementType.METHOD 用于方法@Target({ElementType.METHOD})@Retention(value = RetentionPolicy.RUNTIME)public @interface InCache { String key() default ""; int value() default 0;}
将来这个key与value就可以放入模拟的redis中,当然真正的redis设置setnx时会多一个过期时间的参数,这里由于是用concurrentMap模拟的redis就不添加这个时间的参数了。
然后我们书写基本的接口与其service类:
//利用自定义注解和Aop对接口进行节流@RestController@CrossOriginpublic class UserController { @Resource UserServiceImpl userService; @GetMapping("login") @ResponseBody @InCache(key = "login",value = 3) public String login(){ Integer value = AopUtil.redis.get("login"); if (value == 0) return "访问次数刷完了,访问被refuse"; return "第"+(4-value)+"次访问:"+userService.login("account", "password"); }}@Componentpublic class UserServiceImpl { public String login(String account, String password) { if (account.equals("account") & password.equals("password")) { System.out.println("----------user login success -------"); return "success"; }else return "fail"; }}
接下来我们给controller类中的login方法配置我们的aop增强方法:
@Aspect@Component@Slf4jpublic class AopUtil { //代替redis public static ConcurrentMap redis = new ConcurrentHashMap(); //定义切入点并使用切面类方法代替 @Pointcut("execution(public String com.example.aop.UserController.login())") public void interPoint(){} //利用前置增强检查注解 @Before("interPoint()") public void checkAnnotation(JoinPoint joinPoint){ //获取参数列表 Object[] params = joinPoint.getArgs(); if (params.length != 0) System.out.println("切入方法的参数列表为:"+ Arrays.toString(Arrays.stream(params).toArray())); //获取方法,强行将签名对象类型转换成方法签名对象,仅用于在已知连接点类型为方法时使用 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //根据注解类型获取注解数组,这个直接就是索引加注解对象,可获取注解的属性值 InCache annotation = method.getAnnotation(InCache.class); if (annotation == null){ System.out.println("该方法没有使用InCache注解!"); return ; } String key = annotation.key(); if(redis.containsKey(key)){ log.info("缓存已存在此key,将value进行减一操作"); Integer oldValue = redis.get(key); if (oldValue > 0){ redis.remove(key); redis.put(key,oldValue-1); }else { log.info("缓存中的此key已经为0,拒绝接口访问数据库!"); } }else { log.info("将key 与 value 存入缓存"); redis.put(key,annotation.value()); } }}
这样就可以实现基本的次数限制了,来看效果图: