SpringBoot如何缓存方法返回值?

Why?

为什么要对方法的返回值进行缓存呢?

简单来说是为了提升后端程序的性能和提高前端程序的访问速度。减小对db和后端应用程序的压力。

一般而言,缓存的内容都是不经常变化的,或者轻微变化对于前端应用程序是可以容忍的。

否则,不建议加入缓存,因为增加缓存会使程序复杂度增加,还会出现一些其他的问题,比如缓存同步,数据一致性,更甚者,可能出现经典的缓存穿透、缓存击穿、缓存雪崩问题。

HowDo

如何缓存方法的返回值?应该会有很多的办法,本文简单描述两个比较常见并且比较容易实现的办法:

  • 自定义注解
  • SpringCache

annotation

整体思路:

第一步:定义一个自定义注解,在需要缓存的方法上面添加此注解,当调用该方法的时候,方法返回值将被缓存起来,下次再调用的时候将不会进入该方法。其中需要指定一个缓存键用来区分不同的调用,建议为:类名+方法名+参数名

第二步:编写该注解的切面,根据缓存键查询缓存池,若池中已经存在则直接返回不执行方法;若不存在,将执行方法,并在方法执行完毕写入缓冲池中。方法如果抛异常了,将不会创建缓存

第三步:缓存池,首先需要尽量保证缓存池是线程安全的,当然了没有绝对的线程安全。其次为了不发生缓存臃肿的问题,可以提供缓存释放的能力。另外,缓存池应该设计为可替代,比如可以丝滑得在使用程序内存和使用redis直接调整。

MethodCache

创建一个名为MethodCache 的自定义注解

java 复制代码
package com.ramble.methodcache.annotation;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodCache {

}

MethodCacheAspect

编写MethodCache注解的切面实现

java 复制代码
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

java 复制代码
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,源码如下:

java 复制代码
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
    
   
    ......
    
}

此外可选的缓存池管理对象还有:

  • EhCacheCacheManager

  • JCacheCacheManager

  • RedisCacheManager

  • ......

Cacheable

java 复制代码
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注解

源码:

java 复制代码
@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:方法执行的返回值

使用示例:

java 复制代码
@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null")
public String showEnv() { 
    return "打印各个环境变量";
 }

表示仅当方法返回值不为null的时候才缓存结果,这里通过result env 获取返回值。

另外,condition 和 unless 为互补关系,上述condition = "#result!=null"和unless = "#result==null"其实是一个意思。

java 复制代码
@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->打印各个环境变量。

java 复制代码
@Cacheable(value = "userCache",key = "#root.targetClass")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用被修饰的方法所属的类作为缓存key,实际缓存的数据为:Class->打印各个环境变量,key为class对象,不是全限定名,全限定名是一个字符串,这里是class对象。

可是,不是很懂这样设计的应用场景是什么......

java 复制代码
@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对象,不是字符串。

可是,不是很懂这样设计的应用场景是什么......

java 复制代码
@Cacheable(value = "userCache",key = "#root.methodName")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用方法名作为缓存的key,就是一个字符串。

如何获取缓存的数据?

ConcurrentMapCacheManager的cacheMap是一个私有变量,所以没有办法可以打印缓存池中的数据,不过可以通过调试的方式进入对象内部查看。如下:

java 复制代码
@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的生产,一般而言使用类名+方法名+参数值足矣。

相关推荐
向宇it3 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行5 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
Yvemil72 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。2 小时前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
计算机学长felix2 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计