一、背景
在springcloud微服务架构中,可能会经常通过 feign 组件调用其它的微服务,feign的底层其实是模拟一个http请求,通过访问接口的方式调用远程服务,要经历三次握手建立TCP连接,在项目中是一个比较“耗时”的操作。
如果经常请求一些很少变动的数据,或者在一定时间段内可容忍已过期的数据,那么则需要在调用feign之前能不能从缓存中获取,可以自定义注解,将feign返回的结果缓存到redis中一段时间。
二、自定义注解
1 - 定义一个注解标记需要缓存的方法
package cn.wework.backend.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 此注解用于缓存feign rpc调用的结果,由于远程调用比较耗时,并且大多数情况下返回的结果都不会改变
* 在可容忍的时间内缓存结果提升前端响应速度
* @see FeignClientAspect
* @author Lianghao Teng
* @date 2021/9/23 10:00 AM
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignCache {
/**
* 指定缓存失效时间
* 注意:所有的缓存都应该指定一个合适的缓存时间,项目启动时会自动删除feign-client的缓存
* 如果不指定,则缓存将一直存在,直到下次项目重启
* @return 缓存失效时间,单位:秒
*/
long expiresIn() default -1;
}
2 - 定义切面
package cn.wework.backend.aspect;
import cn.hutool.extra.spring.SpringUtil;
import cn.wework.backend.common.constant.GlobalConstant;
import cn.wework.backend.util.RedisUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Set;
/**
* @author lianghaoteng
*/
@Aspect
@Component
@Slf4j
public class FeignClientAspect implements InitializingBean {
@Autowired
private RedisUtil redisUtil;
private static final String FEIGN_CLIENT_CACHE_PREFIX = "FEIGN-CLIENT-CACHE:";
/**
* 缓存rpc调用的环绕通知
* @param joinPoint
* @return 目标方法调用的结果
* @throws Throwable
*/
@Around("@annotation(cn.wework.backend.aspect.FeignCache)")
public Object cacheAround(ProceedingJoinPoint joinPoint) throws Throwable {
String key = FEIGN_CLIENT_CACHE_PREFIX + joinPoint.getSignature().getDeclaringTypeName() + "#" + joinPoint.getSignature().getName() + "#" + SimpleKeyGenerator.generateKey(joinPoint.getArgs());
Object cache = redisUtil.get(key);
if (cache != null) {
return cache;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
FeignCache annotation = signature.getMethod().getAnnotation(FeignCache.class);
// 这里可以对结果进行过滤,缓存正确的结果,视具体的业务逻辑来定
cache = joinPoint.proceed(joinPoint.getArgs());
redisUtil.set(key, cache, annotation.expiresIn());
return cache;
}
/**
这个方法的作用是:启动项目之前先把feign的缓存都清除掉,视具体情况而定
*/
@Override
public void afterPropertiesSet() {
RedisTemplate<String, Object> redisTemplate = redisUtil.getRedisTemplate();
Set<String> keys = redisTemplate.keys(FEIGN_CLIENT_CACHE_PREFIX + "*");
if (keys != null) {
Long count = redisTemplate.delete(keys);
log.info("feign cache has bean deleted, count = {}", count);
}
}
}
3 - redis操作工具类
package cn.wework.backend.util;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author lianghaoteng
*/
public class RedisUtil {
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
}
三、使用
以上工作做完之后,使用起来就非常方便了,直接在需要缓存的方法上使用注解即可
/**
* 查询city
* @param identity
* @return
*/
@GetMapping("/api/v1/city/{identity}")
@FeignCache(expiresIn = 2 * 60 * 60)
Response<MulanCityDTO> getCity(@PathVariable(value = "identity") String identity);