如何進(jìn)行高效的源碼閱讀:以Spring Cache擴(kuò)展為例帶你搞清楚
摘要
日常開發(fā)中,需要用到各種各樣的框架來實(shí)現(xiàn)API、系統(tǒng)的構(gòu)建。作為程序員,除了會(huì)使用框架還必須要了解框架工作的原理。這樣可以便于我們排查問題,和自定義的擴(kuò)展。那么如何去學(xué)習(xí)框架呢。通常我們通過閱讀文檔、查看源碼,然后又很快忘記。始終不能融匯貫通。本文主要基于Spring Cache擴(kuò)展為例,介紹如何進(jìn)行高效的源碼閱讀。
Spring Cache的介紹
為什么以Spring Cache為例呢,原因有兩個(gè):
- Spring框架是Web開發(fā)常用的框架,值得開發(fā)者去閱讀代碼,吸收思想;
- 緩存是企業(yè)級(jí)應(yīng)用開發(fā)必不可少的,而隨著系統(tǒng)的迭代,我們可能會(huì)需要用到內(nèi)存緩存、分布式緩存。那么Spring Cache作為膠水層,能夠屏蔽掉我們底層的緩存實(shí)現(xiàn)。
一句話解釋Spring Cache:通過注解的方式,利用AOP的思想來解放緩存的管理。
step1 查看文檔
首先通過查看官方文檔,概括了解Spring Cache。https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html
重點(diǎn)兩點(diǎn):
1. 兩個(gè)接口抽象 Cache
,CacheManager,
具體的實(shí)現(xiàn)都是基于這兩個(gè)抽象實(shí)現(xiàn)。
典型的SPI機(jī)制,和eat your dog food。當(dāng)需要提供接口給外部調(diào)用,首先自己內(nèi)部的實(shí)現(xiàn)也必須基于同樣一套抽象機(jī)制。
- 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.
2. Spring Cache提供了這些緩存的實(shí)現(xiàn),如果沒有一種CacheManage,或者CacheResolver,會(huì)按照指定的順序去實(shí)現(xiàn)。
- 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
對(duì)Spring Cache有了一個(gè)大概的了解后,我們首先使用起來,跑個(gè)demo。
定義一個(gè)用戶查詢方法:
- @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;
- }
配置一個(gè)CacheManager:
- @Configuration
- public class CacheConfig {
- @Primary
- @Bean(name = { "cacheManager" })
- public CacheManager getCache() {
- return new ConcurrentMapCacheManager("users");
- }
API調(diào)用:
- @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 查看實(shí)現(xiàn)
demo跑起來后,就是debug看看代碼如何實(shí)現(xiàn)的了。
因?yàn)橹苯涌丛创a的,沒有調(diào)用關(guān)系,看起來會(huì)一頭霧水。通過debug能夠使你更快了解一個(gè)實(shí)現(xiàn)。
通過debug我們會(huì)發(fā)現(xiàn)主要控制邏輯是在切面CacheAspectSupport
會(huì)先根據(jù)cache key找緩存數(shù)據(jù),沒有的話put進(jìn)去。
step4 實(shí)現(xiàn)擴(kuò)展
知道如何使用Spring Cache后,我們需要進(jìn)一步思考,就是如何擴(kuò)展。那么帶著問題出發(fā)。比如Spring Cache不支持批量key的緩存,像上文我們舉的例子,我們希望緩存的key是userId,而不是Collection userIds。以u(píng)serId為key,這樣的緩存命中率更高,存儲(chǔ)的成本更小。
- @Cacheable(cacheNames = "users")
- public Map<Long, User> getUser(final Collection<Long> userIds) {}
所以我們要實(shí)現(xiàn)對(duì)Spring Cache進(jìn)行擴(kuò)展。step3中我們已經(jīng)大致了解了Spring Cache的實(shí)現(xiàn)。那么實(shí)現(xiàn)這個(gè)擴(kuò)展的功能就是拆分Collection userIds,緩存命中的從緩存中獲取,沒有命中的,調(diào)用源方法。
- @Aspect
- @Component
- public class CacheExtenionAspect {
- @Autowired
- private CacheExtensionManage cacheExtensionManage;
- /**
- * 返回的結(jié)果中緩存命中的從緩存中獲取,沒有命中的調(diào)用原來的方法獲取
- * @param joinPoint
- * @return
- */
- @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
- @SuppressWarnings("unchecked")
- public Object aroundCache(final ProceedingJoinPoint joinPoint) {
- // 修改掉Collection值,cacheResult需要重新構(gòu)造一個(gè)
- 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;
- }
- // 設(shè)置緩存
- cacheResult.getCache().putAll(notHit);
- hits.putAll(notHit);
- return hits;
- }
- }
- 然后擴(kuò)展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就是新的緩存結(jié)果格式
- @Builder
- @Setter
- @Getter
- static class CacheResult {
- final CacheExtension cache;
- // 命中的緩存結(jié)果
- final Map<Object, Object> hit;
- // 需要重新調(diào)用源方法的keys
- private Set<Object> miss;
- }
然后擴(kuò)展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
總結(jié)
本文主要介紹一種源碼學(xué)習(xí)方法,純屬拋磚引玉,如果你有好的方法,歡迎分享。