前言
偶尔看到了spring cache的文章,我去,实现原理基本相同,哈哈,大家可以结合着看看。
简介
实际项目中,会遇到很多查询数据的场景,这些数据更新频率也不是很高,一般我们在业务处理时,会对这些数据进行缓存,防止多次与数据库交互。
这次我们讲的是,所有这些场景,通过一个注解即可实现。
实现过程
1、首先我们添加一个自定义注解
package com.bangdao.parking.applets.api.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* 仅针对查询场景使用,其它需要更新数据的请勿使用,不然重复请求不会进行处理
* 请求参数必须是json格式
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheRequest {
/\*\*
\* 缓存时间,默认60秒,单位:秒
\*
\* @return
\*/
@AliasFor("value")
int expire() default 60;
@AliasFor("expire")
int value() default 60;
/\*\*
\* 是否按用户维度区分
\* 比如用户A和用户B先后访问同一个接口,如果该值设置未true,则根据用户区分返回,否则返回用户A的数据
\* 场景A,获取用户个人信息,则此值设为true
\* 场景B,获取车场数据(与个人无关),则此值可设为false
\*
\* @return
\*/
boolean byUser() default true;
/\*\*
\* 自定义key,后续便于其它接口清理缓存。若无更新操作,可忽略此配置
\* @return
\*/
String key() default "";
}
定义两个属性,
①expire,设置缓存内容的过期时间,过期后再次访问,则从数据库查询再次进行缓存,
②byUser,是否根据用户维度区分缓存,有些场景不同用户访问的是相同数据,所以这个是否设置为false,则只缓存一份,更节省缓存空间
③key,不根据参数生成缓存,自定义配置,便于后续有更新操作无法处理,具体可以看下面aop的clearCache方法
2、添加切面,进行数据缓存处理
@Aspect
@Configuration
public class CacheRequestAop {
private static final Logger log = LoggerFactory.getLogger(CacheRequest.class);
@Autowired
private RedisService redisService;
// 这里项目会有个拦截器校验用户登录态,然后会缓存用户信息,根据实际场景获取,如需要,可看我其它博客
@Autowired
private CacheService cacheService;
// 此处注解路径,按实际项目开发进行配置
@Pointcut("@annotation(xxx.CacheRequest)")
public void pointCut() {
}
@Around("pointCut()")
public Object handler(ProceedingJoinPoint pjp) throws Throwable {
log.info("# \[BEGIN\]请求缓存处理");
// 获取注解对象
CacheRequest annotation = getDeclaredAnnotation(pjp, CacheRequest.class);
long expire = annotation.expire();
boolean byUser = annotation.byUser();
// 请求参数排序
TreeMap<String, String> args = new TreeMap<String, String>();
Object\[\] objs = pjp.getArgs();
if (objs.length > 0) {
// json序列化工具,大家可自行选择,建议使用springboot的jackson或者google的gson
// 这里默认取第一个参数对象,因为我们默认为请求格式为Json
args = JacksonUtil.jsonToObject(JacksonUtil.marshallToString(objs\[0\]), new TypeReference<TreeMap<String, String>>() {
});
}
if (byUser) {
args.put("userId", cacheService.getUserId());
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
args.put("requestUrl", request.getRequestURI());
String sign = DigestUtils.md5Hex(JacksonUtil.marshallToString(args));
log.info("# sign:{}", sign);
// 一般项目的返回都会有基类,这里的BaseResult就是
Object result = redisService.get("request:cache:" + sign, BaseResult.class);
// 如果有缓存,则不会进行处理,直接返回缓存结果
if (result != null) {
log.info("# \[END\]请求返回缓存数据");
return result;
}
// 不存在缓存,就进行处理,处理完成在进行缓存
result = pjp.proceed();
redisService.set("request:cache:" + sign, result, expire);
log.info("# \[END\]请求缓存处理");
return result;
}
/\*\*
\* 获取当前注解对象
\*
\* @param <T>
\* @param joinPoint
\* @param clazz
\* @return
\* @throws NoSuchMethodException
\*/
public static <T extends Annotation> T getDeclaredAnnotation(ProceedingJoinPoint joinPoint, Class<T> clazz) throws NoSuchMethodException {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 反射获取目标类
Class<?> targetClass = joinPoint.getTarget().getClass();
// 拿到方法对应的参数类型
Class<?>\[\] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
// 根据类、方法、参数类型(重载)获取到方法的具体信息
Method objMethod = targetClass.getMethod(methodName, parameterTypes);
// 拿到方法定义的注解信息
T annotation = objMethod.getDeclaredAnnotation(clazz);
// 返回
return annotation;
}
public boolean clearCache(String key) {
return clearCache(key, true);
}
public boolean clearCache(String key, boolean byUser) {
TreeMap<String, Object> args = new TreeMap<String, Object>();
args.put("key", key);
if (byUser) {
args.put("openId", cacheService.getUserId());
}
String sign = DigestUtils.md5Hex(JacksonUtil.marshallToString(args));
return redisService.delete(RedisKeyPrefixConts.CACHE\_REQUEST + sign);
}
}
添加切面处理,一般根据三个维度进行缓存(请求地址、用户、请求参数),第一次请求进行返回数据的缓存,后续请求则直接获取缓存数据,不进入接口进行逻辑处理。
有些需要更新信息的场景,需要更新数据后返回最新数据,则可以自定义key,在更新操作时调用clearCache方法即可。
3、项目使用
// 使用默认配置,过期60S,根据用户维度区分
@CacheRequest
@RequestMapping("/test1")
public void test1() {}
// 过期60S,根据用户维度区分
@CacheRequest(60)
@RequestMapping("/test2")
public void test2() {}
// 过期60S,不根据用户维度区分
@CacheRequest(expire = 60,byUser = false)
@RequestMapping("/test3")
public void test3() {}
// 自定义key,便于后续更新操作可清空缓存,定义key时,说明有更新操作,则只需在业务处理时,注入切面,调用clearCache方法即可
@CacheRequest(expire = 60,key="test4")
@RequestMapping("/test4")
public void test4() {}
实际开发,只需要在请求接口添加注解,根据实际场景配置属性即可
4、测试
可看到第二次请求,直接走的缓存返回结果,未进入接口进行逻辑处理。
大家有疑问或更好的建议,可以提出来,楼主看到会第一时间反应,谢谢。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章