模拟Spring缓存机制
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
阶段4、深入jdk其余源码解析
阶段5、深入jvm源码解析
在日常开发中,缓存是提高系统吞吐量的常见手段。不论是使用Guava、Redis还是别的缓存,工程中都不可避免地要编写以下模板代码:
public User method(Long id, boolean useCache) {
if (useCache) {
String cacheKey = this.generateKey(id);
String cachedValue = cache.get(cacheKey);
if (StringUtils.isNotEmpty(cachedValue)) {
// 已缓存,直接返回
return JSON.parseObject(cachedValue, User.class);
} else {
// 执行并缓存
User user = userService.getById(id);
cache.put(cacheKey, JSON.toJSONString(user));
return result;
}
}
return userService.getById(id);
}
为此SpringBoot根据JSR107抽象出了一套缓存机制,其中大家最熟悉的可能是@Cacheable/@CachePut等注解。
今天我们再造一个轮子,模拟SpringBoot的这套缓存机制。
不使用缓存:
使用缓存:
设计思路
- ApplicationContext:简单模拟Spring容器
- HandlerFactory:增强工厂,这里主要进行缓存功能的增强
- DefaultKeyGenerator:解析CacheKeyTemplate,比如把obm:member:{uid}:{platform}替换成obm:member:10086:1
模拟简单的Spring容器
public class ApplicationContext {
@SneakyThrows
public Object getBean(String name) {
// 根据全类名,得到目标类的Class对象
Class<?> clazz = Class.forName(name);
// 根据Class反射创建目标对象
Object target = clazz.newInstance();
// 获取代理对象
return Proxy.newProxyInstance(
clazz.getClassLoader(), // 1.类加载器
clazz.getInterfaces(), // 2.需要代理的接口
HandlerFactory.getCacheInvocationHandler(target, new LocalCache()) // 3.增强逻辑
);
}
}
缓存代理增强
public class HandlerFactory {
/**
* 获取缓存增强
*
* @param target 目标对象
* @param cache 缓存执行器
* @return
*/
public static InvocationHandler getCacheInvocationHandler(final Object target, final Cache cache) {
return new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
// 目标方法是否有@Cacheable
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
boolean useCache = targetMethod.isAnnotationPresent(Cacheable.class);
if (useCache) {
String cacheKey = DefaultKeyGenerator.generate(targetMethod, args);
String cachedValue = cache.get(cacheKey);
if (StringUtils.isNotEmpty(cachedValue)) {
// 已缓存,直接返回
return JSON.parseObject(cachedValue, targetMethod.getReturnType());
} else {
// 执行并缓存
Object result = Optional.ofNullable(method.invoke(target, args))
.orElse(createEmptyObject(method.getReturnType()));
cache.put(cacheKey, JSON.toJSONString(result));
return result;
}
}
// 否则直接invoke目标方法
return method.invoke(target, args);
}
};
}
private static Object createEmptyObject(Class<?> returnType) {
try {
return returnType.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
System.out.println("createEmptyObject error");
return null;
}
}
}
缓存机制
注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String key();
}
缓存
/**
* 缓存接口
*/
public interface Cache {
/**
* 获取缓存
*
* @param cacheKey
* @return
*/
String get(String cacheKey);
/**
* 设置缓存
*
* @param cacheKey
* @param value
*/
void put(String cacheKey, String value);
}
为了简化代码,这里就不引入Redis了,直接用Map代替。如果后期有需要,可以自行编写RedisCache implements Cache。
/**
* 本地缓存,充当Redis
*/
public class LocalCache implements Cache {
private final Map<String, String> LOCAL_CACHE = new ConcurrentHashMap<>();
@Override
public String get(String cacheKey) {
return LOCAL_CACHE.get(cacheKey);
}
@Override
public void put(String cacheKey, String value) {
System.out.println("使用了缓存, cacheKey=" + cacheKey);
LOCAL_CACHE.put(cacheKey, value);
}
}
缓存key生成器
/**
* 默认的CacheKey生成器
*/
public final class DefaultKeyGenerator {
private static final Pattern CACHE_KEY_PATTERN = Pattern.compile("(?<=\\{)(.+?)(?=\\})");
/**
* 生成缓存key
*
* @param method 目标方法
* @param args 目标方法参数
* @return
*/
public static String generate(Method method, Object[] args) {
String cacheKey = method.getAnnotation(Cacheable.class).key();
// 解析 obm:member:{uid}:{platform} 得到 [uid, platform]
List<String> placeholders = parsePlaceholder(cacheKey);
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (placeholders.contains(parameters[i].getName())) {
// 将占位符替换为变量值
cacheKey = cacheKey.replace(parameters[i].getName(), String.valueOf(args[i]));
}
}
return cacheKey;
}
private static List<String> parsePlaceholder(String cacheKeyTemplate) {
Matcher matcher = CACHE_KEY_PATTERN.matcher(cacheKeyTemplate);
List<String> fields = new ArrayList<>();
while (matcher.find()) {
fields.add(matcher.group());
}
return fields;
}
/**
* 测试
*
* @param args
*/
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(?<=\\{)(.+?)(?=\\})");
Matcher matcher = pattern.matcher("{uid}_{name}");
while (matcher.find()) {
System.out.println(matcher.group());
}
String format = MessageFormat.format("obm:{0}:{1}", "2", "3");
System.out.println(format);
}
}
测试案例
public interface UserService {
UserTO getUser(Long uid, Long platform);
}
public class UserServiceImpl implements UserService {
@Cacheable(key = "obm:member:{platform}:{uid}")
@Override
public UserTO getUser(Long uid, Long platform) {
System.out.println("调用UserService获取User, uid:" + uid);
return new UserTO("peter");
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserTO implements Serializable {
private String username;
}
public class CacheTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ApplicationContext();
UserService userService = (UserService) applicationContext.getBean("com.bravo.tech_share.september.service.UserServiceImpl");
// 运行五次,如果开启缓存,那么只有第一次会发出实际请求,后续请求均从缓存读取
for (int i = 0; i < 5; i++) {
UserTO user = userService.getUser(10086L, 1L);
System.out.println(JSON.toJSONString(user));
}
}
}
需要说明的是:
- SpringBoot的缓存机制有好几层抽象,比如CacheProvider、CacheManager、Cache等,而上面为了简化,只抽象了Cache
- 缓存key的生成规则是最麻烦的,特别是如何应对各种不同的业务需求。SpringBoot抽取了KeyGenerator接口,允许使用者通过@Bean自定义KeyGenerator
最后请大家思考一个问题:
public class UserServiceImpl implements UserService {
@Cacheable(key = "obm:member:{uid}:{platform}")
@Override
public UserTO getUser(User user, Long platform) {
// ...
}
}
getUser()的参数从uid+platform变成了user+platform,但我还是希望以obm:member:uid:platform的方式构建cacheKey,该怎么做呢?
不妨思考一下,uid其实可以通过user.getUid()得到。如果@Cacheable的key表达式能支持"obm:member:{user.uid}:{platform}",通过user.uid的形式解析出uid,是不是就能解决问题了?此时就要求我们的KeyGenerator支持属性导航了。所谓属性导航,就是通过“点语法”的形式,通过一层层导航,最终获取目标字段,比如:province.city.area最终获取到某省某市的某个地区。
当然,我的观点是尽量不去要满足这种需求。实现起来比较繁琐,性能也不高。我在文章末尾尝试实现了一版,大家随便看看知道大概原理就行了。
拓展:支持属性导航
/**
* 默认的CacheKey生成器,支持obm:member:{user.username}:{platform})
*/
public final class DefaultKeyGenerator {
private static final Pattern CACHE_KEY_PATTERN = Pattern.compile("(?<=\\{)(.+?)(?=\\})");
/**
* 生成缓存key
*
* @param method 目标方法
* @param args 目标方法参数
* @return
*/
public static String generate(Method method, Object[] args) {
String cacheKey = method.getAnnotation(Cacheable.class).key();
// 解析 obm:member:{user.uid}:{platform} 得到 [user.uid, platform]
List<String> placeholders = parsePlaceholder(cacheKey);
Parameter[] parameters = method.getParameters();
for (String placeholder : placeholders) {
if (placeholder.contains(".")) {
// 可以尝试递归,但没必要,约定好最多两级即可
cacheKey = parseCascadePlaceholder(cacheKey, placeholder, parameters, args);
} else {
cacheKey = parseSimplePlaceholder(cacheKey, placeholder, parameters, args);
}
}
return cacheKey;
}
private static String parseSimplePlaceholder(String cacheKey, String placeholder, Parameter[] parameters, Object[] args) {
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].getName().equals(placeholder)) {
cacheKey = cacheKey.replace("{" + placeholder + "}", String.valueOf(args[i]));
break;
}
}
return cacheKey;
}
private static String parseCascadePlaceholder(String cacheKey, String placeholder, Parameter[] parameters, Object[] args) {
for (int i = 0; i < parameters.length; i++) {
Class<?> parameterType = parameters[i].getType();
if (isSimpleType(parameterType)) {
continue;
}
Object arg = args[i];
String[] split = StringUtils.split(placeholder, ".");
for (Field declaredField : parameterType.getDeclaredFields()) {
if (declaredField.getName().equals(split[1])) {
declaredField.setAccessible(true);
try {
Object fieldValue = declaredField.get(arg);
return cacheKey.replace("{" + placeholder + "}", String.valueOf(fieldValue));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
return cacheKey;
}
private static boolean isSimpleType(Class<?> parameterType) {
// 基础类型
if (parameterType.isPrimitive()) {
return true;
}
// 包装类型
if (Long.class.equals(parameterType)) {
return true;
} else if (Integer.class.equals(parameterType)) {
return true;
} else if (String.class.equals(parameterType)) {
return true;
} else if (Boolean.class.equals(parameterType)) {
return true;
} else if (Double.class.equals(parameterType)) {
return true;
} else if (BigDecimal.class.equals(parameterType)) {
return true;
}
return false;
}
private static List<String> parsePlaceholder(String cacheKeyTemplate) {
Matcher matcher = CACHE_KEY_PATTERN.matcher(cacheKeyTemplate);
List<String> fields = new ArrayList<>();
while (matcher.find()) {
fields.add(matcher.group());
}
return fields;
}
}
如果想提高性能,或许可以考虑提前缓存元数据字段等,但从效率角度考虑,不是很推荐太复杂的cacheKey生成策略。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!