如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚

日常开发中,需要用到各种各样的框架来实现API、系统的构建。作为程序员,除了会使用框架还必须要了解框架工作的原理。

日常开发中,需要用到各种各样的框架来实现API、系统的构建。作为程序员,除了会使用框架还必须要了解框架工作的原理。

摘要

日常开发中,需要用到各种各样的框架来实现API、系统的构建。作为程序员,除了会使用框架还必须要了解框架工作的原理。这样可以便于我们排查问题,和自定义的扩展。那么如何去学习框架呢。通常我们通过阅读文档、查看源码,然后又很快忘记。始终不能融汇贯通。本文主要基于Spring Cache扩展为例,介绍如何进行高效的源码阅读。

Spring Cache的介绍

为什么以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。当需要提供接口给外部调用,首先自己内部的实现也必须基于同样一套抽象机制。

  1. Thecacheabstractiondoesnotprovideanactualstoreandreliesonabstractionmaterializedbytheorg.springframework.cache.Cacheandorg.springframework.cache.CacheManagerinterfaces.

2. Spring Cache提供了这些缓存的实现,如果没有一种CacheManage,或者CacheResolver,会按照指定的顺序去实现。

  1. IfyouhavenotdefinedabeanoftypeCacheManageroraCacheResolvernamedcacheResolver(seeCachingConfigurer),SpringBoottriestodetectthefollowingproviders(intheindicatedorder):
  2. 1.Generic
  3. 2.JCache(JSR-107)(EhCache3,Hazelcast,Infinispan,andothers)
  4. 3.EhCache2.x
  5. 4.Hazelcast
  6. 5.Infinispan
  7. 6.Couchbase
  8. 7.Redis
  9. 8.Caffeine
  10. 9.Simple

step2 run demo

对Spring Cache有了一个大概的了解后,我们首先使用起来,跑个demo。

定义一个用户查询方法:

  1. @Component
  2. publicclassCacheSample{
  3. @Cacheable(cacheNames="users")
  4. publicMap<Long,User>getUser(finalCollection<Long>userIds){
  5. System.out.println("notcache");
  6. finalMap<Long,User>mapUser=newHashMap<>();
  7. userIds.forEach(userId->{
  8. mapUser.put(userId,User.builder().userId(userId).name("name").build());
  9. });
  10. returnmapUser;
  11. }

配置一个CacheManager:

  1. @Configuration
  2. publicclassCacheConfig{
  3. @Primary
  4. @Bean(name={"cacheManager"})
  5. publicCacheManagergetCache(){
  6. returnnewConcurrentMapCacheManager("users");
  7. }

API调用:

  1. @RestController
  2. @RequestMapping("/api/cache")
  3. publicclassCacheController{
  4. @Autowired
  5. privateCacheSamplecacheSample;
  6. @GetMapping("/user/v1/1")
  7. publicList<User>getUser(){
  8. returncacheSample.getUser(Arrays.asList(1L,2L)).values().stream().collect(Collectors.toList());
  9. }
  10. }

step3 debug 查看实现

demo跑起来后,就是debug看看代码如何实现的了。
因为直接看源代码的,没有调用关系,看起来会一头雾水。通过debug能够使你更快了解一个实现。

在这里插入图片描述
通过debug我们会发现主要控制逻辑是在切面CacheAspectSupport
会先根据cache key找缓存数据,没有的话put进去。

step4 实现扩展

知道如何使用Spring Cache后,我们需要进一步思考,就是如何扩展。那么带着问题出发。比如Spring Cache不支持批量key的缓存,像上文我们举的例子,我们希望缓存的key是userId,而不是CollectionuserIds。以userId为key,这样的缓存命中率更高,存储的成本更小。

  1. @Cacheable(cacheNames="users")
  2. publicMap<Long,User>getUser(finalCollection<Long>userIds){}

所以我们要实现对Spring Cache进行扩展。step3中我们已经大致了解了Spring Cache的实现。那么实现这个扩展的功能就是拆分CollectionuserIds,缓存命中的从缓存中获取,没有命中的,调用源方法。

  1. @Aspect
  2. @Component
  3. publicclassCacheExtenionAspect{
  4. @Autowired
  5. privateCacheExtensionManagecacheExtensionManage;
  6. /**
  7. *返回的结果中缓存命中的从缓存中获取,没有命中的调用原来的方法获取
  8. *@paramjoinPoint
  9. *@return
  10. */
  11. @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
  12. @SuppressWarnings("unchecked")
  13. publicObjectaroundCache(finalProceedingJoinPointjoinPoint){
  14. //修改掉Collection值,cacheResult需要重新构造一个
  15. args[0]=cacheResult.getMiss();
  16. try{
  17. finalMap<Object,Object>notHit=CollectionUtils.isEmpty(cacheResult.getMiss())?null
  18. :(Map<Object,Object>)(method.invoke(target,args));
  19. finalMap<Object,Object>hits=cacheResult.getHit();
  20. if(Objects.isNull(notHit)){
  21. returnhits;
  22. }
  23. //设置缓存
  24. cacheResult.getCache().putAll(notHit);
  25. hits.putAll(notHit);
  26. returnhits;
  27. }
  28. }
  29. 然后扩展Cache,CacheManage
  30. 重写Cache的查找缓存方法,返回新的CacheResult
  31. publicstaticObjectlookup(finalCacheExtensioncache,finalObjectkey){
  32. if(keyinstanceofCollection){
  33. finalCollection<Object>originalKeys=((Collection)key);
  34. if(originalKeys==null||originalKeys.isEmpty()){
  35. returnCacheResult.builder().cache(cache).miss(
  36. Collections.emptySet())
  37. .build();
  38. }
  39. finalList<Object>keys=originalKeys.stream()
  40. .filter(Objects::nonNull).collect(Collectors.toList());
  41. finalMap<Object,Object>hits=cache.getAll(keys);
  42. finalSet<Object>miss=newHashSet(keys);
  43. miss.removeAll(hits.keySet());
  44. returnCacheResult.builder().cache(cache).hit(hits).miss(miss).build();
  45. }
  46. returnnull;
  47. }
  48. CacheResult就是新的缓存结果格式
  49. @Builder
  50. @Setter
  51. @Getter
  52. staticclassCacheResult{
  53. finalCacheExtensioncache;
  54. //命中的缓存结果
  55. finalMap<Object,Object>hit;
  56. //需要重新调用源方法的keys
  57. privateSet<Object>miss;
  58. }

然后扩展CacheManager,没什么重写,就是自定义一种manager类型。

为缓存指定新的CacheManager:

  1. @Primary@BeanpublicCacheManagergetExtensionCache(){returnnewCacheExtensionManage("users2");}

完整代码:

https://github.com/FS1360472174/javaweb/tree/master/web/src/main/java/com/fs/web/cache

总结

本文主要介绍一种源码学习方法,纯属抛砖引玉,如果你有好的方法,欢迎分享。

©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
上一篇 2023年5月5日 07:25
下一篇 2023年5月5日 07:25

相关推荐

发表评论

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信