引言

后面咱们有学习Caffeine 本地缓存性能之王Caffeine,并且也提到SpringBoot默认应用的本地缓存也是Caffeine啦,明天咱们来看看Caffeine如何与SpringBoot集成的。

集成caffeine

caffeineSpringBoot集成有两种形式:

  • 一种是咱们间接引入 Caffeine 依赖,而后应用 Caffeine 办法实现缓存。相当于应用原生api
  • 引入 CaffeineSpring Cache 依赖,应用 SpringCache 注解办法实现缓存。SpringCache帮咱们封装了Caffeine
    pom文件引入

    <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-cache</artifactId></dependency><dependency>  <groupId>com.github.ben-manes.caffeine</groupId>  <artifactId>caffeine</artifactId>  <version>2.6.0</version></dependency>

    第一种形式

    首先配置一个Cache,通过结构者模式构建一个Cache对象,而后后续对于缓存的增删查都是基于这个cache对象。

    @Configurationpublic class CacheConfig {  @Bean  public Cache<String, Object> caffeineCache() {      return Caffeine.newBuilder()              // 设置最初一次写入或拜访后通过固定工夫过期              .expireAfterWrite(60, TimeUnit.SECONDS)              // 初始的缓存空间大小              .initialCapacity(100)              // 缓存的最大条数              .maximumSize(1000)              .build();  }

    第一种形式咱们就一一不介绍了,基本上就是应用caffeineCache来依据你本人的业务来操作以下办法

    这种形式应用的话是对代码有侵入性的。

    第二种形式

  • 须要在SpingBoot启动类标上EnableCaching注解,这个玩意跟很多框架都一样,比方咱们肴集成dubbo也须要标上@EnableDubbole注解等。

      @SpringBootApplication  @EnableCaching  public class DemoApplication {      public static void main(String[] args) {          SpringApplication.run(DemoApplication.class, args);      }
  • application.yml配置咱们的应用的缓存类型、过期工夫、缓存策略等。

    spring:profiles:  active: devcache:  type: CAFFEINE  caffeine:    spec: maximumSize=500,expireAfterAccess=600s

    如果咱们不习惯应用这种形式的配置,当然咱们也能够应用JavaConfig的配置形式来代替配置文件。

    @Configurationpublic class CacheConfig {      @Bean      public CacheManager cacheManager() {          CaffeineCacheManager cacheManager = new CaffeineCacheManager();          cacheManager.setCaffeine(Caffeine.newBuilder()                  // 设置最初一次写入或拜访后通过固定工夫过期                  .expireAfterAccess(600, TimeUnit.SECONDS)                  // 初始的缓存空间大小                  .initialCapacity(100)                  // 缓存的最大条数                  .maximumSize(500));          return cacheManager;      }

    接下来就是代码中如何来应用这个缓存了

      @Override  @CachePut(value = "user", key = "#userDTO.id")  public UserDTO save(UserDTO userDTO) {      userRepository.save(userDTO);      return userDTO;  }  @Override  @CacheEvict(value = "user", key = "#id")//2  public void remove(Long id) {      logger.info("删除了id、key为" + id + "的数据缓存");  }  @Override  @Cacheable(value = "user",key = "#id")  public UserDTO getUserById(Long id) {      return userRepository.findOne(id);  }

    上述代码中咱们能够看到有几个注解@CachePut、@CacheEvict、@Cacheable
    咱们只须要在办法上标上这几个注解,咱们就可能应用缓存了,咱们别离来介绍下这几个注解。

    @Cacheable

    @Cacheable它是既能够标注在类上也能够标注在办法上,当它标记在类上的时候它表述这个类下面的所有办法都会反对缓存,同样的
    当它作用在法下面时候它示意这个办法是反对缓存的。比方下面咱们代码中的getUserById这个办法第一次缓存外面没有数据,咱们会去查问DB,然而第二次来查问的时候就不会走DB查问了,而是间接从缓存外面拿到后果就返回了。

    value 属性
  • @Cacheablevalue属性是必须指定的,其示意以后办法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。

    key
  • @Cacheablekey 有两种形式一种是咱们本人显示的去指定咱们的key,还有一种默认的生成策略,默认的生成策略是SimpleKeyGenerator这个类,这个生成key的形式也比较简单咱们能够看下它的源码:

    public static Object generateKey(Object... params) {      // 如果办法没有参数 key就是一个 new SimpleKey()      if (params.length == 0) {          return SimpleKey.EMPTY;      }      // 如果办法只有一个参数 key就是以后参数      if (params.length == 1) {          Object param = params[0];          if (param != null && !param.getClass().isArray()) {              return param;          }      }      // 如果key是多个参数,key就是new SimpleKey ,不过这个SimpleKey对象的hashCode 和Equals办法是依据办法传入的参数重写的。      return new SimpleKey(params);  }

    上述代码还是十分好了解的分为三种状况:

  • 办法没有参数,那就new应用一个全局空的SimpleKey对象来作为key
  • 办法就一个参数,就应用以后参数来作为key
  • 办法参数大于1个,就new一个SimpleKey对象来作为keynew 这个SimpleKey的时候用传入的参数重写了SimpleKeyhashCodeequals办法,
    至于为啥须要重写的起因话,就跟Map用自定义对象来作为key的时候必须要重写hashCodeequals办法原理是一样的,因为caffein也是借助了ConcurrentHashMap来实现,

    小结

    上述代码咱们能够发现默认生成key只跟咱们传入的参数有关系,如果咱们有一个类外面如果存在多个没有参数的办法,而后咱们应用了默认的缓存生成策略的话,就会造成缓存失落。
    或者缓存互相笼罩,或者还有可能会产生ClassCastException 因为都是应用同一个key。比方上面这代码就会产生异样(ClassCastException)

    @Cacheable(value = "user")  public UserDTO getUser() {      UserDTO userDTO = new UserDTO();      userDTO.setUserName("Java金融");      return userDTO;  }  @Cacheable(value = "user")  public UserDTO2 getUser1() {      UserDTO2 userDTO2 = new UserDTO2();      userDTO2.setUserName2("javajr.cn");      return userDTO2;  }

    所以个别不怎么举荐应用默认的缓存生成key的策略。如果非要用的话咱们最好本人重写一下,带上办法名字等。相似于如下代码:

    @Componentpublic class MyKeyGenerator extends SimpleKeyGenerator {  @Override  public Object generate(Object target, Method method, Object... params) {      Object generate = super.generate(target, method, params);      String format = MessageFormat.format("{0}{1}{2}", method.toGenericString(), generate);      return format;  }
    自定义key

    咱们能够通过Spring的EL表达式来指定咱们的key。这里的EL表达式能够应用办法参数及它们对应的属性。
    应用办法参数时咱们能够间接应用“#参数名”或者“#p参数index”这也是咱们比拟举荐的做法:

     @Cacheable(value="user", key="#id")  public UserDTO getUserById(Long id) {      UserDTO userDTO = new UserDTO();      userDTO.setUserName("java金融");      return userDTO;  }  @Cacheable(value="user", key="#p0")  public UserDTO getUserById1(Long id) {      return null;  }  @Cacheable(value="user", key="#userDTO.id")  public UserDTO getUserById2(UserDTO userDTO) {      return null;  }  @Cacheable(value="user", key="#p0.id")  public UserDTO getUserById3(UserDTO userDTO) {      return null;  }

    @CachePut

    @CachePut指定的属性是和@Cacheable一样的,然而它们两个是有区别的,@CachePut标注的办法不会先去查问缓存是否有值,而是每次都会先去执行该办法,而后把后果返回,并且后果也会缓存起来。

    ![在这里插入图片形容](https://img-blog.csdnimg.cn/ff516023113046adbf86caaea6e499f6.png)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                

为什么是这样的一个流程咱们能够去看看它的源码要害代码就是这一行,

        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

当咱们应用办法上有@Cacheable注解的时候再contexts外面会把CacheableOperation退出进去,只有contexts.get(CacheableOperation.class)取到的内容不为空的话,才会去从缓存外面取内容,否则的话cacheHit会间接返回null。至于contexts什么时候退出CacheableOperation的话咱们看下SpringCacheAnnotationParser#parseCacheAnnotations这个办法就会明确的。具体的源码就不展现了,感兴趣的能够本人去翻。

@CacheEvict

把缓存中数据删除,用法跟后面两个注解差不多有value和key属性,须要留神一点的是它多了一个属性beforeInvocation

  • beforeInvocation 这个属性须要留神下它的默认值是false,false代表的意思是再执调用办法之前不删除缓存,只有办法执行胜利之后才会去删除缓存。设置为true的话调用办法之前会去删除一下缓存,办法执行胜利之后还会去调用删除缓存这样就是双删了。如果办法执行异样的话就不会去删除缓存。
  • allEntrie 是否清空所有缓存内容,默认值为 false,如果指定为 true,则办法调用后将立刻清空所有缓存

@Caching

这是一个组合注解集成了下面三个注解,有三个属性:cacheable、put和evict,别离用于来指定@Cacheable@CachePut@CacheEvict

小结

第二种形式是侵入式的,它的实现原理也比较简单就是通过切面的办法拦截器来实现,拦挡所有的办法,它的外围代码如下:看起来就跟咱们的业务代码差不了多少,感兴趣的也能够去瞅一瞅。

if (contexts.isSynchronized()) {            CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();            if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {                Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);                Cache cache = context.getCaches().iterator().next();                try {                    return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));                }                catch (Cache.ValueRetrievalException ex) {                    // The invoker wraps any Throwable in a ThrowableWrapper instance so we                    // can just make sure that one bubbles up the stack.                    throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();                }            }            else {                // No caching required, only call the underlying method                return invokeOperation(invoker);            }        }        // Process any early evictions        // beforeInvocation 属性是否为true,如果是true就删除缓存        processCacheEvicts(contexts.get(CacheEvictOperation.class), true,                CacheOperationExpressionEvaluator.NO_RESULT);        // Check if we have a cached item matching the conditions        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));        // Collect puts from any @Cacheable miss, if no cached item is found        List<CachePutRequest> cachePutRequests = new LinkedList<>();        if (cacheHit == null) {            collectPutRequests(contexts.get(CacheableOperation.class),                    CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);        }        Object cacheValue;        Object returnValue;        if (cacheHit != null && !hasCachePut(contexts)) {            // If there are no put requests, just use the cache hit            cacheValue = cacheHit.get();            returnValue = wrapCacheValue(method, cacheValue);        }        else {            // Invoke the method if we don't have a cache hit            returnValue = invokeOperation(invoker);            cacheValue = unwrapReturnValue(returnValue);        }        // Collect any explicit @CachePuts        collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);        // Process any collected put requests, either from @CachePut or a @Cacheable miss        for (CachePutRequest cachePutRequest : cachePutRequests) {            cachePutRequest.apply(cacheValue);        }        // Process any late evictions        processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);        return returnValue;    }

完结

  • 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来,我会对其加以修改。
  • 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
  • 感谢您的浏览,非常欢送并感谢您的关注。


站在伟人的肩膀上摘苹果:
https://www.cnblogs.com/fashf...!comments