1. 程式人生 > >如何進行高效的原始碼閱讀:以Spring Cache擴充套件為例帶你搞清楚

如何進行高效的原始碼閱讀:以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進去。

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

總結

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