前言

偶尔看到了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、测试

可看到第二次请求,直接走的缓存返回结果,未进入接口进行逻辑处理。

大家有疑问或更好的建议,可以提出来,楼主看到会第一时间反应,谢谢。

自定义注解,实现请求缓存【Spring Cache】的更多相关文章

  1. SpringBoot 拦截器和自定义注解判断请求是否合法

    应用场景举例: 当不同身份的用户请求一个接口时,用来校验用户某些身份,这样可以对单个字段数据进行精确权限控制,具体看代码注释 自定义注解 /** * 对比请求的用户身份是否符合 * @author l ...

  2. 自定义注解获取请求Header中的值

    前言 这几天开发一个项目,为了方便,前台将当前登陆人的ID和名称放在每个请求的Header中(这里不考虑安全性之类的),这样后台只要需要用到,就直接从Header中get出来就可以了. 后台实现方法 ...

  3. 基于Spring Cache实现二级缓存(Caffeine+Redis)

    一.聊聊什么是硬编码使用缓存? 在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存. 我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下: @A ...

  4. ssm+redis 如何更简洁的利用自定义注解+AOP实现redis缓存

    基于 ssm + maven + redis 使用自定义注解 利用aop基于AspectJ方式 实现redis缓存 如何能更简洁的利用aop实现redis缓存,话不多说,上demo 需求: 数据查询时 ...

  5. Spring-cloud (八) Hystrix 请求缓存的使用

    前言: 最近忙着微服务项目的开发,脱更了半个月多,今天项目的初版已经完成,所以打算继续我们的微服务学习,由于Hystrix这一块东西好多,只好多拆分几篇文章写,对于一般对性能要求不是很高的项目中,可以 ...

  6. Spring Boot 2.X(七):Spring Cache 使用

    Spring Cache 简介 在 Spring 3.1 中引入了多 Cache 的支持,在 spring-context 包中定义了org.springframework.cache.Cache 和 ...

  7. Spring Cache For Redis

    一.概述 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息都是立即可用的. 常用的缓存数据库: Redis   使用内存存储(in-memory)的非关系数据库,字符串.列 ...

  8. JAVA 框架 Spring Cache For Redis.

    一.概述 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息都是立即可用的. 常用的缓存数据库: Redis   使用内存存储(in-memory)的非关系数据库,字符串.列 ...

  9. 自定义缓存管理器 或者 Spring -- cache

    Spring Cache 缓存是实际工作中非常常用的一种提高性能的方法, 我们会在许多场景下来使用缓存. 本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 spring 的基于注释的 c ...

  10. Spring Cache扩展:注解失效时间+主动刷新缓存

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

随机推荐

  1. PHP编程与系统开发

    PHP开发环境配置 一.开发环境 1.XAMPP或LNMP.WNMP,先安装widows版本的XMAPP-5.6版本(PHP 5.6) 2.VSCode:微软开发的集成开发环境(IDE) 二.安装教程 ...

  2. Error in render: “TypeError: Cannot read property ‘0‘ of null“

    我们web的同学运行程序时经常会遇到如下错误,而查找起来却相当费劲 看错误提示第一反应会想到是不是我的js 方法中的某个对象取值错误了,如: 但完全错了,当你把方法里的js 翻来覆去找了一遍又一遍,任 ...

  3. 运行和编译时期资源加载的不同【vue】

    开发语言都有编译和运行两个阶段,很多时候这个也会带来许多bug 如:一个项目在开发阶段测试没有问题,然上线发布后就会有这样那样的问题,譬如说图片的加载,静态数据(js,css,json)读取错误 一 ...

  4. linux 的 vi 命令详解

    vi编辑器是所有Unix及Linux系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令.由于对Unix及Linux系统的任何版本,vi编辑器是完全相 ...

  5. vst实例(2) 创建VST

    前面我们知道,创建一个虚拟树,应该首先告知VST节点数据的大小(即nodedatasize),其实在创建树结构时,这一点并不是必须的,而是如果你需要让VST的每一个节点能指向一定的数据,从而在执行树的 ...

  6. 火爆,Github标星240K的编程学习路线图,适合所有程序员!

    推荐一个涵盖开发.运维.产品设计的学习路线图,在Github已经start超过240K,包括各门编程语言! 一.涵盖的路线图 该项目涵盖了非常全面的学习路线图: 前端路线图 后端路线图 ASP.NET ...

  7. Java中如何中断线程

    在Java中,可以使用以下方法中断线程: 1. 使用`interrupt()`方法:每个线程对象都有一个`interrupt()`方法,用于中断该线程.当调用线程的`interrupt()`方法时,它 ...

  8. Vue——动态组件、插槽、Vue-cli创建项目、Vue项目目录结构、编写规范、es6导入导出语法

    动态组件 // <component :is="who"></component> // component标签的is属性等于组件名字,这里就会显示这个组件 ...

  9. OpenAI发布ChatGPT函数调用和API更新

    2023年6月13日,OpenAI针对开发者调用的API做了重大更新,包括更易操控的 API模型.函数调用功能.更长的上下文和更低的价格. 在今年早些时候发布gpt-3.5-turbo,gpt-4在短 ...

  10. Go 并发模型—Goroutines

    前言 Goroutines 是 Go 语言主要的并发原语.它看起来非常像线程,但是相比于线程它的创建和管理成本很低.Go 在运行时将 goroutine 有效地调度到真实的线程上,以避免浪费资源,因此 ...