基于 Spring AOP 和自定义注解的接口调用次数统计功能
完善上篇博文中的代码,使其支持 @Countable
注解中的 time
属性,并且只统计指定时间之前的接口调用次数,同时将统计信息存入 Redis,我们需要进行以下改进:
- 自定义注解:添加
time
属性。 - 引入 Redis 依赖:用于存储和检索调用次数统计。
- 调整切面逻辑:在切面中处理时间条件,并使用 Redis 存储统计数据。
- 创建 Redis 配置和服务:用于与 Redis 进行交互。
步骤 1:修改自定义注解
我们将在 @Countable
注解中添加一个 time
属性,表示方法调用的截止时间(以毫秒为单位)。
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Countable {
String value() default "";
long time() default Long.MAX_VALUE; // 默认值为 Long.MAX_VALUE 表示没有时间限制
}
步骤 2:引入 Redis 依赖
确保你的 pom.xml
或 build.gradle
文件中有 Spring Data Redis 的依赖:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
步骤 3:配置 Redis
创建 Redis 配置类来设置连接参数。
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
步骤 4:调整切面逻辑
更新切面逻辑以考虑时间条件,并将调用次数保存到 Redis 中。
package com.example.demo.aspect;
import com.example.demo.annotation.Countable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.RedisTemplate;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class MethodInvocationCounterAspect {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public MethodInvocationCounterAspect(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 定义切入点,匹配所有带有 @Countable 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.Countable)")
public void countableMethods() {}
// 环绕通知,以便可以在方法执行前后检查时间条件并更新计数
@Around("countableMethods()")
public Object countMethodInvocations(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Countable countable = method.getAnnotation(Countable.class);
// 获取当前时间和注解中的截止时间
long currentTime = System.currentTimeMillis();
long endTime = countable.time();
if (currentTime <= endTime || endTime == Long.MAX_VALUE) {
// 执行目标方法
Object result = joinPoint.proceed();
// 更新 Redis 中的计数
String methodName = joinPoint.getTarget().getClass().getName() + "." + method.getName();
Long currentCount = redisTemplate.opsForValue().increment(methodName, 1);
// 设置过期时间(可选)
if (endTime != Long.MAX_VALUE) {
redisTemplate.expireAt(methodName, Instant.ofEpochMilli(endTime));
}
return result;
} else {
// 如果超过了截止时间,则不执行方法并返回 null 或抛出异常
throw new IllegalStateException("Method call is beyond the allowed time.");
}
}
}
步骤 5:应用到 Controller
继续在 Controller
中使用 @Countable
注解来标记需要统计的方法,并指定 time
属性。
package com.example.demo.controller;
import com.example.demo.annotation.Countable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class MyController {
@GetMapping("/hello")
@Countable(time = 1708000000000L) // 指定时间戳,例如:2024-02-26T00:00:00Z
public String sayHello() {
return "Hello, World!";
}
@GetMapping("/bye")
@Countable(value = "Goodbye", time = 1708000000000L)
public String sayGoodbye() {
return "Goodbye!";
}
}
步骤 6:添加控制器获取统计数据
创建一个新的控制器方法来从 Redis 中获取并返回当前的调用次数统计信息。
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/stats")
public class StatsController {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public StatsController(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@GetMapping("/method-counts")
public Map<String, Object> getMethodCounts() {
Set<String> keys = redisTemplate.keys("*");
return redisTemplate.opsForValue().multiGet(keys).stream()
.collect(Collectors.toMap(key -> key, value -> value));
}
}
总结
通过以上步骤,我们实现了对带有 @Countable
注解的方法进行调用次数统计的功能,并且:
- 添加了
time
属性来限制统计的时间范围。 - 使用 Redis 来存储和管理调用次数统计信息。
- 提供了一个接口来查看这些统计数据。