Aop实现注解限流和Redis缓存
2019-12-05 本文已影响0人
JLLang
gratisography-plam-trees-summer
限流注解实现
业务系统中某些接口需要进行限流的时候在spring家族中可以采用RateLimiter进行接口限流,减轻服务器的压力。实现思路如下:
RateLimit 注解
/**
* @description: 限流注解
* @author: lilang
* @version:
* @modified By:1170370113@qq.com
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
double limitNum() default 20; //默认每秒放入桶中的token
//获取令牌的等待时间
int timeOut() default 0;
//等待时间单位
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
注解RateLimit AOP实现类
@Component
@Aspect
public class RateLimitAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
//用来存放不同接口的RateLimiter(key为接口名称,value为RateLimiter)
private ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();
private static ObjectMapper objectMapper = new ObjectMapper();
private RateLimiter rateLimiter;
@Pointcut("@annotation(com.itstyle.mail.common.aop.RateLimit)")
public void serviceLimit() {
}
@ResponseBody
@Around("serviceLimit()")
public Object around(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
Object obj = null;
//获取拦截的方法名
Signature sig = joinPoint.getSignature();
//获取拦截的方法名
MethodSignature msig = (MethodSignature) sig;
//返回被织入增加处理目标对象
Object target = joinPoint.getTarget();
//为了获取注解信息
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
//获取注解信息
RateLimit annotation = currentMethod.getAnnotation(RateLimit.class);
double limitNum = annotation.limitNum(); //获取注解每秒加入桶中的token
TimeUnit timeUnit = annotation.timeUnit();//获取时间单位
String functionName = msig.getName(); // 注解所在方法名区分不同的限流策略
int timeOut = annotation.timeOut();
//获取rateLimiter
if(map.containsKey(functionName)){
rateLimiter = map.get(functionName);
}else {
map.put(functionName, RateLimiter.create(limitNum));
rateLimiter = map.get(functionName);
}
try {
if (rateLimiter.tryAcquire(timeOut,timeUnit)) {
//执行方法
obj = joinPoint.proceed();
} else {
return Result.error("服务器繁忙,请稍后再试....");
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
}
使用方式 比如在controller层进行控制
@RateLimit(limitNum = 10,timeOut = 1,timeUnit = TimeUnit.SECONDS)
@GetMapping("limit/go")
public void queryFromMysql(){
//数据库操作逻辑
}
Redis缓存实现
缓存注解定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {
String prefix() default "";
int expire() default 1;
TimeUnit TIME_UNIT() default TimeUnit.DAYS;
//缓存反序列化获取的对象
Class clazz() default Object.class;
//序列化后的对象是否是jsonarry 比如 List<Object>
boolean isArray() default false;
}
缓存切面类实现:
@Component
@Aspect
public class RedisCacheAspect {
private static Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);
@Autowired
private RedisTemplate redisTemplate ;
/**
* 分隔符 生成key 格式为 类全类名|方法名|参数所属类全类名
**/
private static final String DELIMITER = "-";
/**
* Service层切点 使用到了我们定义的 RedisCacheAspect 作为切点表达式。
* 而且我们可以看出此表达式基于 annotation。
* 并且用于内建属性为查询的方法之上
*/
@Pointcut("@annotation(com.itstyle.mail.common.aop.RedisCache)")
public void redisCacheAspect() {
}
/**
* Around 手动控制调用核心业务逻辑,以及调用前和调用后的处理,
* <p>
* 注意:当核心业务抛异常后,立即退出,转向AfterAdvice 执行完AfterAdvice,再转到ThrowingAdvice
*
*/
@Around(value = "redisCacheAspect()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 得到类名、方法名和参数
String clazzName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 根据类名、方法名和参数生成Key
logger.info("key参数: " + clazzName + "." + methodName);
String key = getKey(clazzName, methodName, args);
if (logger.isInfoEnabled()) {
logger.info("生成key: " + key);
}
// 得到被代理的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//redis 前缀
String prefix = method.getAnnotation(RedisCache.class).prefix();
int expire = method.getAnnotation(RedisCache.class).expire();
TimeUnit timeUnit = method.getAnnotation(RedisCache.class).TIME_UNIT();
Class objectType = method.getAnnotation(RedisCache.class).clazz();
boolean isArray=method.getAnnotation(RedisCache.class).isArray();
// 检查Redis中是否有缓存
String value = (String) redisTemplate.opsForValue().get(prefix);
// 得到被代理方法的返回值类型
// Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
// result是方法的最终返回结果
Object result = null;
try {
if (null == value) {
logger.info("缓存未命中");
// 调用数据库查询方法
result = joinPoint.proceed(args);
// 结果放入缓存
redisTemplate.opsForValue().set(prefix, JSON.toJSONString(result),expire,timeUnit);
} else {
/**
* 可以直接针对mapper进行缓存,如果mapper查询返回的List<DemoObjec> 需要isArray 为true 否则转换异常
*/
if (isArray){
return JSON.parseArray(value,objectType);
}else {
return JSON.parseObject(value,objectType);
}
}
} catch (Throwable e) {
logger.error("程序异常",e.getMessage());
throw e;
}
return result;
}
/**
* * 根据类名、方法名和参数生成Key
* * @param clazzName
* * @param methodName
* * @param args
* * @return key格式:全类名|方法名|参数类型
*
*/
private String getKey(String clazzName, String methodName, Object[] args) {
StringBuilder key = new StringBuilder(clazzName);
key.append(DELIMITER);
key.append(methodName);
key.append(DELIMITER);
key.append(Arrays.stream(args).map(x->x.toString()).collect(Collectors.joining(DELIMITER)));
return key.toString();
}
}
使用方式 :
以下是放在mapper层加入的注解,实际项目中可以根据自己的需求注解加在任意位置。
@Mapper
public interface DemoMapper {
@RedisCache(expire = 1,clazz = DemoObject.class,isArray = true)
public List<DemoObject> queryFromMysql();
@RedisCache(expire = 1,clazz = DemoObject.class)
public DemoObject queryFromMysql();
}
使用如上方式,我们便可以在实际项目中无侵入的实现业务限流和业务缓存。