SpringBoot如何缓存方法返回值?
Why?
为什么要对方法的返回值进行缓存呢?
简单来说是为了提升后端程序的性能和提高前端程序的访问速度。减小对db和后端应用程序的压力。
一般而言,缓存的内容都是不经常变化的,或者轻微变化对于前端应用程序是可以容忍的。
否则,不建议加入缓存,因为增加缓存会使程序复杂度增加,还会出现一些其他的问题,比如缓存同步,数据一致性,更甚者,可能出现经典的缓存穿透、缓存击穿、缓存雪崩问题。
HowDo
如何缓存方法的返回值?应该会有很多的办法,本文简单描述两个比较常见并且比较容易实现的办法:
- 自定义注解
- SpringCache
annotation
整体思路:
第一步:定义一个自定义注解,在需要缓存的方法上面添加此注解,当调用该方法的时候,方法返回值将被缓存起来,下次再调用的时候将不会进入该方法。其中需要指定一个缓存键用来区分不同的调用,建议为:类名+方法名+参数名
第二步:编写该注解的切面,根据缓存键查询缓存池,若池中已经存在则直接返回不执行方法;若不存在,将执行方法,并在方法执行完毕写入缓冲池中。方法如果抛异常了,将不会创建缓存
第三步:缓存池,首先需要尽量保证缓存池是线程安全的,当然了没有绝对的线程安全。其次为了不发生缓存臃肿的问题,可以提供缓存释放的能力。另外,缓存池应该设计为可替代,比如可以丝滑得在使用程序内存和使用redis直接调整。
MethodCache
创建一个名为MethodCache 的自定义注解
package com.ramble.methodcache.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodCache {
}
MethodCacheAspect
编写MethodCache注解的切面实现
package com.ramble.methodcache.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Aspect
@Component
public class MethodCacheAspect {
private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<>();
@Around(value = "@annotation(methodCache)")
public Object around(ProceedingJoinPoint jp, MethodCache methodCache) throws Throwable {
String className = jp.getSignature().getDeclaringType().getSimpleName();
String methodName = jp.getSignature().getName();
String args = String.join(",", Arrays.toString(jp.getArgs()));
String key = className + ":" + methodName + ":" + args;
// key 示例:DemoController:findUser:[FindUserParam(id=1, name=c7)]
log.debug("缓存的key={}", key);
Object cache = getCache(key);
if (null != cache) {
log.debug("走缓存");
return cache;
} else {
log.debug("不走缓存");
Object value = jp.proceed();
setCache(key, value);
return value;
}
}
private Object getCache(String key) {
return CACHE_MAP.get(key);
}
private void setCache(String key, Object value) {
CACHE_MAP.put(key, value);
}
}
- Around:对被MethodCache注解修饰的方法启用环绕通知
- ProceedingJoinPoint:通过此对象获取方法所在类、方法名和参数,用来组装缓存key
- CACHE_MAP:缓存池,生产环境建议使用redis等可以分布式存储的容器,直接放程序内存不利于后期业务扩张后多实例部署
controller
package com.ramble.methodcache.controller;
import com.ramble.methodcache.annotation.MethodCache;
import com.ramble.methodcache.controller.param.CreateUserParam;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Tag(name = "demo - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo")
public class DemoController {
private final DemoService demoService;
@MethodCache
@GetMapping("/{id}")
public String getUser(@PathVariable("id") String id) {
return demoService.getUser(id);
}
@Operation(summary = "查询用户")
@MethodCache
@PostMapping("/list")
public String findUser(@RequestBody FindUserParam param) {
return demoService.findUser(param);
}
}
通过反复调用被@MethodCache注解修饰的方法,会发现若缓存池有数据,将不会进入方法体。
SpringCache
其实SpringCache的实现思路和上述方法基本一致,SpringCache提供了更优雅的编程方式,更丝滑的缓存池切换和管理,更强大的功能和统一规范。
EnableCaching
使用 @EnableCaching 开启SpringCache功能,无需引入额外的pom。
默认情况下,缓存池将由 ConcurrentMapCacheManager 这个对象管理,也就是默认是程序内存中缓存。其中用于存放缓存数据的是一个 ConcurrentHashMap,源码如下:
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
......
}
此外可选的缓存池管理对象还有:
EhCacheCacheManager
JCacheCacheManager
RedisCacheManager
......
Cacheable
package com.ramble.methodcache.controller;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {
private final DemoService demoService;
@Cacheable(value = "userCache")
@GetMapping("/{id}")
public String getUser(@PathVariable("id") String id) {
return demoService.getUser(id);
}
@Operation(summary = "查询用户")
@Cacheable(value = "userCache")
@PostMapping("/list")
public String findUser(@RequestBody FindUserParam param) {
return demoService.findUser(param);
}
}
- 使用@Cacheable注解修饰需要缓存返回值的方法
- value必填,不然运行时报异常。类似一个分组,将不同的数据或者方法(当然也可以其他维度,主要看业务需要)放到一堆,便于管理
- 可以修饰接口方法,但是不建议,IDEA会报一个提示Spring doesn't recommend to annotate interface methods with @Cache* annotation
常用属性:
- value:缓存名称
- cacheNames:缓存名称。value 和cacheNames都被AliasFor注解修饰,他们互为别名
- key:缓存数据时候的key,默认使用方法参数的值,可以使用SpEL生产key
- keyGenerator:key生产器。和key二选一
- cacheManager:缓存管理器
- cacheResolver:和caheManager二选一,互为别名
- condition:创建缓存的条件,可用SpEL表达式(如#id>0,表示当入参id大于0时候才缓存方法返回值)
- unless:不创建缓存的条件,如#result==null,表示方法返回值为null的时候不缓存
CachePut
用来更新缓存。被CachePut注解修饰的方法,在被调用的时候不会校验缓存池中是否已经存在缓存,会直接发起调用,然后将返回值放入缓存池中。
CacheEvict
用来删除缓存,会根据key来删除缓存中的数据。并且不会将本方法返回值缓存起来。
常用属性:
- value/cacheeName:缓存名称,或者说缓存分组
- key:缓存数据的键
- allEntries:是否根据缓存名称清空所有缓存,默认为false。当此值为true的时候,将根据cacheName清空缓存池中的数据,然后将新的返回值放入缓存
- beforeInvocation:是否在方法执行之前就清空缓存,默认为false
Caching
此注解用于在一个方法或者类上面,同时指定多个SpringCache相关注解。这个也是SpringCache的强大之处,可以自定义各种缓存创建、更新、删除的逻辑,应对复杂的业务场景。
属性:
- cacheable:指定@Cacheable注解
- put:指定@CachePut注解
- evict:指定@CacheEvict注解
源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
相当于就是注解里面套注解,用来完成复杂和多变的场景,这个设计相当的哇塞。
CacheConfig
放在类上面,那么类中所有方法都会被缓存
SpringCacheEnv
SpringCache内置了一些环境变量,可用于各个注解的属性中。
methodName:被修饰方法的方法名
method:被修饰方法的Method对象
target:被修饰方法所属的类对象的实例
targetClass:被修饰方法所属类对象
args:方法入参,是一个 object[] 数组
caches:这个对象其实就是ConcurrentMapCacheManager中的cacheMap,这个cacheMap呢就是一开头提到的ConcurrentHashMap,即缓存池。caches的使用场景尚不明了。
argumentName:方法的入参
result:方法执行的返回值
使用示例:
@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null")
public String showEnv() {
return "打印各个环境变量";
}
表示仅当方法返回值不为null的时候才缓存结果,这里通过result env 获取返回值。
另外,condition 和 unless 为互补关系,上述condition = "#result!=null"和unless = "#result==null"其实是一个意思。
@Cacheable(value = "userCache", key = "#name")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用方法入参作为该条缓存数据的key,若传入的name为gg,则实际缓存的数据为:gg->打印各个环境变量
另外,如果name为空会报异常,因为缓存key不允许为null
@Cacheable(value = "userCache",key = "#root.args")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用方法的入参作为缓存的key,若传递的参数为id=100,name=gg,则实际缓存的数据为:Object[]->打印各个环境变量,Object[]数组中包含两个值。
既然是数组,可以通过下标进行访问,root.args[1] 表示获取第二个参数,本例中即 取 name 的值 gg,则实际缓存的数据为:gg->打印各个环境变量。
@Cacheable(value = "userCache",key = "#root.targetClass")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用被修饰的方法所属的类作为缓存key,实际缓存的数据为:Class->打印各个环境变量,key为class对象,不是全限定名,全限定名是一个字符串,这里是class对象。
可是,不是很懂这样设计的应用场景是什么......
@Cacheable(value = "userCache",key = "#root.target")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用被修饰方法所属类的实例作为key,实际缓存的数据为:UserController->打印各个环境变量。
被修饰的方法就是在UserController中,调试的时候甚至可以获取到此实例注入的其它容器对象,如userService等。
可是,不是很懂这样设计的应用场景是什么......
@Cacheable(value = "userCache",key = "#root.method")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用Method对象作为缓存的key,是Method对象,不是字符串。
可是,不是很懂这样设计的应用场景是什么......
@Cacheable(value = "userCache",key = "#root.methodName")
public String showEnv(String id, String name) {
return "打印各个环境变量";
}
表示使用方法名作为缓存的key,就是一个字符串。
如何获取缓存的数据?
ConcurrentMapCacheManager的cacheMap是一个私有变量,所以没有办法可以打印缓存池中的数据,不过可以通过调试的方式进入对象内部查看。如下:
@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {
private final ConcurrentMapCacheManager cacheManager;
/**
* 只有调试才课可以查看缓存池中的数据
*/
@GetMapping("/cache")
public void showCacheData() {
//需要debug进入
Collection<String> cacheNames = cacheManager.getCacheNames();
}
}
总结:
虽然提供了很多的环境变量,但是大多都无法找到对应的使用场景,其实在实际开发中,最常见的就是key的生产,一般而言使用类名+方法名+参数值足矣。
SqEL
参考:https://juejin.cn/post/6987993458807930893
cite
SpringBoot如何缓存方法返回值?的更多相关文章
- JAVAEE——SpringMVC第二天:高级参数绑定、@RequestMapping、方法返回值、异常处理、图片上传、Json交互、实现RESTful、拦截器
1. 课前回顾 https://www.cnblogs.com/xieyupeng/p/9093661.html 2. 课程计划 1.高级参数绑定 a) 数组类型的参数绑定 b) List类型的绑定 ...
- handlerAdapter与方法返回值的处理
前提:处理器方法被调用并返回了结果 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer ma ...
- C# 方法返回值的个数
方法返回值类型总的来说分为值类型,引用类型,Void 有些方法显示的标出返回值 public int Add(int a,int b) { return a+b; } 有些方法隐式的返回返回值,我们可 ...
- 使用Result代替ResultSet作为方法返回值
在开发过程中,我们不能将ResultSet对象作为方法的返回值,因为Connection连接一旦关闭,在此连接上的会话和在会话上的结果集也将会自动关闭,而Result对象则不会发生这种现象,所以在查询 ...
- AJAX JQuery 调用后台方法返回值(不刷新页面)
AJAX JQuery 调用后台方法返回值(不刷新页面) (1)无参数返回值(本人亲试返回结果不是预期结果) javascript方法: $(function () { //无 ...
- SpringMVC第五篇【方法返回值、数据回显、idea下配置虚拟目录、文件上传】
Controller方法返回值 Controller方法的返回值其实就几种类型,我们来总结一下-. void String ModelAndView redirect重定向 forward转发 数据回 ...
- 关于java字节流的read()方法返回值为int的思考
我们都知道java中io操作分为字节流和字符流,对于字节流,顾名思义是按字节的方式读取数据,所以我们常用字节流来读取二进制流(如图片,音乐 等文件).问题是为什么字节流中定义的read()方法返回值为 ...
- java 反射获取方法返回值类型
//ProceedingJoinPoint pjp //获取方法返回值类型 Object[] args = pjp.getArgs(); Class<?>[] paramsCls = ne ...
- js 获取getElementsTagName()方法返回值的内容
<div id="news-top" class="section"> <h3>Some title</h3> <di ...
- eclipse自动生成变量名声明(按方法返回值为本地变量赋值)
eclipse自动生成变量名声明(按方法返回值为本地变量赋值) ctrl+2+L 这个快捷键可自动补全代码,极大提升编码效率! 注:ctrl和2同时按完以后释放,再快速按L.不能同时按! 比如写这句代 ...
随机推荐
- java.lang.Error: Unresolved compilation problems
一般有两种常见的情况: 1.当一个 jar 文件的 MANIFEST.MF 中已经标记了 Sealed: true 时,这个 jar 内所有的 java package 中的类必须来自这个 jar 包 ...
- 每日一题 力扣 1090 https://leetcode.cn/problems/largest-values-from-labels/
每日一题 力扣 1090 https://leetcode.cn/problems/largest-values-from-labels/ 先对这道题目进行排序,贪心一下,要求分数最高的放在前面,而标 ...
- 正确处理 CSV 文件的引号和逗号
CSV(Comma-Separated Values,逗号分割值),就是用纯文本的形式存储表格数据,最大的特点就是方便. 作为开发,我们经常面临导数据的问题,特别是后台系统,产品或者运营的同事常常会提 ...
- Spring的Bean标签配置(一)
Bean标签基本配置 由于配置对象交由Spring来创建 默认情况下它调用的的是类中的无参构造函数,如果没有无参构造函数则不会创建成功 id:唯一标识符号,反射是通过无参构造创建对象的. class: ...
- Windows商店开发者注册失败
前言 最近写了个小工具想上架Windows应用商店,但是在填写信息那一页总是失败,提示Error code 2201. Correlation ID 9d436e3a-94df-498a-b224-8 ...
- 【go语言】1.2.1 Go 环境安装
Go 语言的安装过程非常简单,无论你使用的是哪种操作系统,都可以按照下面的步骤来进行. Windows 系统 前往 Go 语言的官方下载页面:https://golang.org/dl/ 根据你的操作 ...
- WPF自定义标题栏
往往原有的标题栏无法满足需求,此时就需要进行自定义标题栏. 重新定义Window的Template 首先,需修改WindowChrome的几个属性 CaptionHeight属性值就是自定义标题栏的高 ...
- [FlareOn4]login-buu ctf
打开压缩包 是个html,我直接???? 这不是web弄的吗 离谱了,不过f12还是会的 不过其中的逻辑还是比较清楚的 先用伪代码确定加密逻辑,再直接写直接进行爆破解码 wo cao,wrong!fl ...
- 学好Elasticsearch系列-索引的批量操作
本文已收录至 Github,推荐阅读 Java 随想录 微信公众号:Java 随想录 先看后赞,养成习惯. 点赞收藏,人生辉煌. 目录 基于 mget 的批量查询 基于 bulk 的批量增删改 增加 ...
- 一个可将执行文件打包成Windows服务的.Net开源工具
Windows服务一种在后台持续运行的程序,它可以在系统启动时自动启动,并在后台执行特定的任务,例如监视文件系统.管理硬件设备.执行定时任务等. 今天推荐一个可将执行文件打包成Windows 服务的工 ...