前言
偶尔看到了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、测试
可看到第二次请求,直接走的缓存返回结果,未进入接口进行逻辑处理。
大家有疑问或更好的建议,可以提出来,楼主看到会第一时间反应,谢谢。