摘要

日常开发中,需要用到各种各样的框架来实现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进去。

// 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

总结

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

关注公众号【方丈的寺院】,第一时间收到文章的更新,与方丈一起开始技术修行之路

以Spring Cache扩展为例介绍如何进行高效的源码的阅读的更多相关文章

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

    摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...

  2. Spring Cache扩展:注解失效时间+主动刷新缓存(二)

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  3. 日志组件Log2Net的介绍和使用(附源码开源地址)

    Log2Net是一个用于收集日志到数据库或文件的组件,支持.NET和.NetCore平台. 此组件自动收集系统的运行日志(服务器运行情况.在线人数等).异常日志.程序员还可以添加自定义日志. 该组件支 ...

  4. Spring3 + Spring MVC+ Mybatis 3+Mysql 项目整合(注解及源码)

    Spring3 + Spring MVC+ Mybatis 3+Mysql 项目整合(注解及源码) 备注: 之前在Spring3 + Spring MVC+ Mybatis 3+Mysql 项目整合中 ...

  5. Spring Cache缓存技术的介绍

    缓存用于提升系统的性能,特别适用于一些对资源需求比较高的操作.本文介绍如何基于spring boot cache技术,使用caffeine作为具体的缓存实现,对操作的结果进行缓存. demo场景 本d ...

  6. Spring Cache扩展:注解失效时间+主动刷新缓存

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  7. Spring IOC 特性有哪些,不会读不懂源码!

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 多线程.锁.JVM调优,都背出花啦,怎么一写代码还是乱糟糟? 为什么这些无论从书本. ...

  8. 【浅墨著作】《OpenCV3编程入门》内容简单介绍&amp;勘误&amp;配套源码下载

    经过近一年的沉淀和总结,<OpenCV3编程入门>一书最终和大家见面了. 近期有为数不少的小伙伴们发邮件给浅墨建议最好在博客里面贴出这本书的文件夹,方便大家更好的了解这本书的内容.事实上近 ...

  9. spring boot整合spring5-webflux从0开始的实战及源码解析

    上篇文章<你的响应阻塞了没有?--Spring-WebFlux源码分析>介绍了spring5.0 新出来的异步非阻塞服务,很多读者说太理论了,太单调了,这次我们就通过一个从0开始的实例实战 ...

随机推荐

  1. QString转换为LPTSTR(使用了reinterpret_cast,真是叹为观止,但是也开阔了思路),三篇文章合起来的各种转换方法

    醉了,windows下宏定义了很多char类型 LPTSTR .今天,直接使用,qt报错,真TM费事. 将“CPU”转化为wcha_t * QString str = "CPU"; ...

  2. .NET Core整合log4net以及全局异常捕获实现2

    Startup代码 public static ILoggerRepository repository { get; set; } public Startup(IConfiguration con ...

  3. UWP ListView嵌套ListView

    要求:加载全部的订单,每个订单里面有一个或者多个产品,在列表中要展现出来, 1. xaml界面 步骤:1.这里使用的是x:bind绑定所以要引入实体类命名空间(OrderList集合中类的命名空间): ...

  4. 在Azure中搭建Ghost博客并绑定自定义域名和HTTPS

    绪论 之前一直使用cnblog写博客,现在将博客迁移至Microsoft Azure上的Ghost博客上,Ghost博客使用Markdown书写博客,页面简洁,是我喜欢的风格.具体参见官网:https ...

  5. 利用Delphi实现网络监控系统

    实现的原理WINSOCK是一组API,用于在INTE.Net上传输数据和交换信息.用它编程本来是很麻烦的,但在DELPHHI中并不需要直接与WINSOCK的API打交道,因为TclientSocket ...

  6. 浅议Delphi中的Windows API调用(举的两个例子分别是String和API,都不错,挺具有代表性)

    浅议Delphi中的Windows API调用http://tech.163.com/school • 2005-08-15 10:57:41 • 来源: 天极网为了能在Windows下快速开发应用程 ...

  7. 深入浅出RPC——深入篇(转载)

    本文转载自这里是原文 <深入篇>我们主要围绕 RPC 的功能目标和实现考量去展开,一个基本的 RPC 框架应该提供什么功能,满足什么要求以及如何去实现它? RPC 功能目标 RPC的主要功 ...

  8. 浅谈网络爬虫爬js动态加载网页(一)

    由于别的项目组在做舆情的预言项目,我手头正好没有什么项目,突然心血来潮想研究一下爬虫.分析的简单原型.网上查查这方面的资料还真是多,眼睛都看花了.搜了搜对于我这种新手来说,想做一个简单的爬虫程序,所以 ...

  9. 基于Node.js的web聊天系统 - 真正意义上的web实时聊天系统

    简单介绍一下这个实时web聊天系统的功能,首先进入系统的人填入名字和邮件地址后会获取到一个由系统创建的URL地址,你可以把这个地址发给另外一个人,另外一个人进入系统后就可以和你进行实时的聊天对话咯.主 ...

  10. 利用GitLab自动同步软件仓库

    利用GitLab自动同步GitHub.Gitee.Bitbucket软件仓库 我在码云的账号:userName密码:password项目地址:https://gitee.com/Bytom/bytom ...