摘要
日常開發中,需要用到各種各樣的框架來實現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)
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
總結
本文主要介紹一種源碼學習方法,純屬拋磚引玉,如果你有好的方法,歡迎分享。
關注公衆號【方丈的寺院】,第一時間收到文章的更新,與方丈一起開始技術修行之路