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

摘要

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

SpringCache的介紹

爲何以Spring Cache爲例呢,緣由有兩個java

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

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

step1 查看文檔

首先經過查看官方文檔,歸納瞭解Spring Cache
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html程序員

重點兩點github

  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.web

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

    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.Simpleapi

step2 run demo

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

定義一個用戶查詢方法app

@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

總結

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

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

相關文章
相關標籤/搜索