如何進行高效的原始碼閱讀:以Spring Cache擴充套件為例帶你搞清楚
摘要
日常開發中,需要用到各種各樣的框架來實現API、系統的構建。作為程式設計師,除了會使用框架還必須要了解框架工作的原理。這樣可以便於我們排查問題,和自定義的擴充套件。那麼如何去學習框架呢。通常我們通過閱讀文件、檢視原始碼,然後又很快忘記。始終不能融匯貫通。本文主要基於Spring Cache擴充套件為例,介紹如何進行高效的原始碼閱讀。
SpringCache的介紹
為什麼以Spring Cache為例呢,原因有兩個
- Spring框架是web開發最常用的框架,值得開發者去閱讀程式碼,吸收思想
- 快取是企業級應用開發必不可少的,而隨著系統的迭代,我們可能會需要用到記憶體快取、分散式快取。那麼Spring Cache作為膠水層,能夠遮蔽掉我們底層的快取實現。
一句話解釋Spring Cache: 通過註解的方式,利用AOP的思想來解放快取的管理。
step1 檢視文件
首先通過檢視官方文件,概括瞭解Spring Cache
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html
重點兩點
- 兩個介面抽象
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.
-
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)
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
總結
本文主要介紹一種原始碼學習方法,純屬拋磚引玉,如果你有好的方法,歡迎分