以Spring Cache擴展爲例介紹如何進行高效的源碼的閱讀

摘要

日常開發中,需要用到各種各樣的框架來實現API、系統的構建。作爲程序員,除了會使用框架還必須要了解框架工作的原理。這樣可以便於我們排查問題,和自定義的擴展。那麼如何去學習框架呢。通常我們通過閱讀文檔、查看源碼,然後又很快忘記。始終不能融匯貫通。本文主要基於Spring Cache擴展爲例,介紹如何進行高效的源碼閱讀。

SpringCache的介紹

爲什麼以Spring Cache爲例呢,原因有兩個

  1. Spring框架是web開發最常用的框架,值得開發者去閱讀代碼,吸收思想
  2. 緩存是企業級應用開發必不可少的,而隨着系統的迭代,我們可能會需要用到內存緩存、分佈式緩存。那麼Spring Cache作爲膠水層,能夠屏蔽掉我們底層的緩存實現。

一句話解釋Spring Cache: 通過註解的方式,利用AOP的思想來解放緩存的管理。

step1 查看文檔

首先通過查看官方文檔,概括瞭解Spring Cache
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html

重點兩點

  1. 兩個接口抽象 Cache,CacheManager,具體的實現都是基於這兩個抽象實現。
    典型的SPI機制,和eat your dog food。當需要提供接口給外部調用,首先自己內部的實現也必須基於同樣一套抽象機制

The cache abstraction does not provide an actual store and relies on abstraction materialized by the org.springframework.cache.Cache and org.springframework.cache.CacheManager interfaces.

  1. Spring Cache提供了這些緩存的實現,如果沒有一種CacheManage,或者CacheResolver,會按照指定的順序去實現

If you have not defined a bean of type CacheManager or a CacheResolver named cacheResolver (see CachingConfigurer), Spring Boot tries to detect the following providers (in the indicated order):
1.Generic
2.JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
3.EhCache 2.x
4.Hazelcast
5.Infinispan
6.Couchbase
7.Redis
8.Caffeine
9.Simple

step2 run demo

對Spring Cache有了一個大概的瞭解後,我們首先使用起來,跑個demo。

定義一個用戶查詢方法

@Component
public class CacheSample {
    @Cacheable(cacheNames = "users")
    public Map<Long, User> getUser(final Collection<Long> userIds) {
        System.out.println("not cache");
        final Map<Long, User> mapUser = new HashMap<>();
        userIds.forEach(userId -> {
            mapUser.put(userId, User.builder().userId(userId).name("name").build());
        });
        return mapUser;
    }

配置一個CacheManager

@Configuration
public class CacheConfig {
    @Primary
    @Bean(name = { "cacheManager" })
    public CacheManager getCache() {
      return new ConcurrentMapCacheManager("users");
    }

API調用

@RestController
@RequestMapping("/api/cache")
public class CacheController {
    @Autowired
    private CacheSample cacheSample;
    @GetMapping("/user/v1/1")
    public List<User> getUser() {
        return cacheSample.getUser(Arrays.asList(1L,2L)).values().stream().collect(Collectors.toList());
    }
    }

step3 debug 查看實現

demo跑起來後,就是debug看看代碼如何實現的了。
因爲直接看源代碼的,沒有調用關係,看起來會一頭霧水。通過debug能夠使你更快了解一個實現。

在這裏插入圖片描述
通過debug我們會發現主要控制邏輯是在切面CacheAspectSupport
會先根據cache key找緩存數據,沒有的話put進去。

// 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);
}

step4 實現擴展

知道如何使用Spring Cache後,我們需要進一步思考,就是如何擴展。那麼帶着問題出發。
比如Spring Cache不支持批量key的緩存,像上文我們舉的例子,我們希望緩存的key是userId,而不是Collection userIds。以userId爲key,這樣的緩存命中率更高,存儲的成本更小。

  @Cacheable(cacheNames = "users")
    public Map<Long, User> getUser(final Collection<Long> userIds) {

所以我們要實現對Spring Cache進行擴展。step3中我們已經大致瞭解了Spring Cache的實現。那麼實現這個擴展的功能就是拆分Collection userIds,緩存命中的從緩存中獲取,沒有命中的,調用源方法。

@Aspect
@Component
public class CacheExtenionAspect {

    @Autowired
    private CacheExtensionManage cacheExtensionManage;

    /**
     * 返回的結果中緩存命中的從緩存中獲取,沒有命中的調用原來的方法獲取
     * @param joinPoint
     * @return
     */
    @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
    @SuppressWarnings("unchecked")
    public Object aroundCache(final ProceedingJoinPoint joinPoint) {
    
        // 修改掉Collection值,cacheResult需要重新構造一個
        args[0] = cacheResult.getMiss();
        try {
            final Map<Object, Object> notHit = CollectionUtils.isEmpty(cacheResult.getMiss()) ? null
                    : (Map<Object, Object>) (method.invoke(target, args));
            final Map<Object, Object> hits = cacheResult.getHit();
            if (Objects.isNull(notHit)) {
                return hits;
            }
            // 設置緩存
            cacheResult.getCache().putAll(notHit);
            hits.putAll(notHit);
            return hits;
    }
}

然後擴展Cache,CacheManage
重寫Cache的查找緩存方法,返回新的CacheResult

  public static Object lookup(final CacheExtension cache, final Object key) {
        if (key instanceof Collection) {
            final Collection<Object> originalKeys = ((Collection) key);
            if (originalKeys == null || originalKeys.isEmpty()) {
                return CacheResult.builder().cache(cache).miss(
                        Collections.emptySet())
                        .build();
            }
            final List<Object> keys = originalKeys.stream()
                    .filter(Objects::nonNull).collect(Collectors.toList());
            final Map<Object, Object> hits = cache.getAll(keys);
            final Set<Object> miss = new HashSet(keys);
            miss.removeAll(hits.keySet());
            return CacheResult.builder().cache(cache).hit(hits).miss(miss).build();
        }
        return null;
    }

CacheResult就是新的緩存結果格式

 @Builder
    @Setter
    @Getter
    static class CacheResult {
        final CacheExtension cache;
        // 命中的緩存結果
        final Map<Object, Object> hit;
        // 需要重新調用源方法的keys
        private Set<Object> miss;
    }

然後擴展CacheManager,沒什麼重寫,就是自定義一種manager類型
爲緩存指定新的CacheManager

  @Primary
   @Bean
   public CacheManager getExtensionCache() {
       return new CacheExtensionManage("users2");
   }

完整代碼
https://github.com/FS1360472174/javaweb/tree/master/web/src/main/java/com/fs/web/cache

總結

本文主要介紹一種源碼學習方法,純屬拋磚引玉,如果你有好的方法,歡迎分享。

關注公衆號【方丈的寺院】,第一時間收到文章的更新,與方丈一起開始技術修行之路
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章