书接上文,本篇文章主要聚焦于Redis-tools这一模块分析。
框架蓝图:清晰的模块化设计
在深入细节之前,我们先来看一下这个框架的整体结构。它被清晰地拆分成了三个独立的 Maven 模块。
.
├───damai-redis-common-framework # 通用基础模块
├───damai-redis-framework # 核心框架模块 (String/Hash/List等)
└───damai-redis-stream-framework # Redis Stream 功能模块
common
模块:提供最基础的公共组件,如配置类、常量、或通用的工具。framework
模块:封装了对 Redis 几大数据结构的核心操作,是框架的主体。stream-framework
模块:专注于 Redis 5.0 以后引入的 Stream 数据结构,提供消息队列相关的功能。
这种分层结构职责清晰,便于维护和按需引入。
核心框架剖析:亮点与疑难点深度挖掘
这部分是我们探索的重点,其中包含了几个颇有亮点的设计。
亮点一:告别魔法字符串 —— RedisKeyBuild
的优雅封装
@Getter
public final class RedisKeyBuild {
/**
* 实际使用的key
* */
private final String relKey;
private RedisKeyBuild(String relKey){
this.relKey = relKey;
}
/**
* 构建真实的key
* @param redisKeyManage key的枚举
* @param args 占位符的值
* */
public static RedisKeyBuild createRedisKey(RedisKeyManage redisKeyManage, Object... args){
String redisRelKey = String.format(redisKeyManage.getKey(),args);
return new RedisKeyBuild(SpringUtil.getPrefixDistinctionName() + "-" + redisRelKey);
}
public static String getRedisKey(RedisKeyManage redisKeyManage) {
return SpringUtil.getPrefixDistinctionName() + "-" + redisKeyManage.getKey();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RedisKeyBuild that = (RedisKeyBuild) o;
return relKey.equals(that.relKey);
}
@Override
public int hashCode() {
return Objects.hash(relKey);
}
}
在任何与 Redis 打交道的项目中,Key 的管理都是一个不大不小的痛点。随意散落在业务代码中的 "user:info:" + userId
被称为“魔法字符串”,极难维护。这个框架通过一个 RedisKeyBuild
类给出了完美的解决方案。
它的好处是什么?
- 类型安全与意图明确:在方法签名中使用
RedisKeyBuild
而不是String
,在编译期就杜绝了传入非法字符串的可能。doSomething(RedisKeyBuild key)
的方法签名远比doSomething(String key)
清晰。 - 强制规范与统一前缀:通过将构造函数私有化,强制使用者通过静态工厂方法
createRedisKey(...)
来创建实例。这个工厂方法内部可以统一添加业务前缀或环境前缀(如dev:
、prod:
),保证了 Key 的规范性。 - 正确的集合行为:该类重写了
equals()
和hashCode()
方法。这至关重要,它保证了RedisKeyBuild
对象在作为HashMap
的键或存入HashSet
时,能够基于其内部的 relKey 字符串进行正确的逻辑判断,而不是比较内存地址。
亮点二:攻克泛型擦除 —— buildType
之于TypeReference
的优势
这是我本次学习中收获最大的部分。我们都知道,FastJSON
这类库在反序列化泛型容器(如 List<User>
)时会遇到泛型擦除问题。
- 为什么序列化没问题? 因为序列化时,我们面对的是一个活生生的、类型信息完备的对象实例。通过反射,
FastJSON
可以轻易地知道car.getOwners()
这个字段的类型就是List<User>
。 - 为什么反序列化有问题? 因为当我们调用
JSON.parseObject(json, List.class)
时,由于泛型擦除,List.class
本身不携带任何关于User
的信息。FastJSON
只知道要创建一个List
,但不知道里面该装什么。
为了解决这个问题,我们需要为 FastJSON
提供一份包含完整泛型信息的“说明书”。
标准方案:TypeReference
FastJSON 官方推荐的标准方案是 TypeReference
。它利用 Java 匿名内部类可以保留父类泛型签名的技巧,在编译期“写死”完整的泛型类型。
- 优势:简洁直观、官方标准、编译期安全。
- 适用场景:在业务代码中,当你明确知道需要反序列化的目标类型时。
String jsonArray = "[{\"name\":\"张三\"}, {\"name\":\"李四\"}]";
// 我在写代码时,100%确定我需要的是 List<User>
TypeReference<List<User>> typeRef = new TypeReference<List<User>>() {};
List<User> userList = JSON.parseObject(jsonArray, typeRef);
框架利器:buildType
的动态哲学
本框架的作者没有止步于此,而是设计了一个更为强大的 buildType
方法。它的核心优势在于灵活性——它可以在运行时根据传入的变量,动态地构造出 Type
对象。
我们可以先看一看buildType的代码:
/**
* 构建类型
*
* @param types
* @return
*/
public static Type buildType(Type... types) {
ParameterizedTypeImpl beforeType = null;
if (types != null && types.length > 0) {
if (types.length == 1) {
return new ParameterizedTypeImpl(new Type[]{null}, null,
types[0]);
}
JSONObject.parseObject(JSONObject.toJSONString(types), new TypeReference<List>() {
});
for (int i = types.length - 1; i > 0; i--) {
beforeType = new ParameterizedTypeImpl(new Type[]{beforeType == null ? types[i] : beforeType}, null,
types[i - 1]);
}
}
return beforeType;
}
public ParameterizedTypeImpl(Type[] actualTypeArguments, Type ownerType, Type rawType) {
super(actualTypeArguments, ownerType, rawType);
}
该方法内部也是使用了ParameterizedTypeImpl对象,该对象接受三个参数。第一个参数是实际类型,第二个参数是外部类的类型,第三个参数是原类型。第二个参数只有涉及到内部类的时候才会使用,平时基本不会使用。举个例子,我想要传入参数为List.class,User.class的时候,实际类型就是User.class,raw类型就是List.class
- 优势:强大、灵活,能处理在编译期无法确定的动态泛型。
- 适用场景:编写通用的框架或库时,需要处理各种未知的、动态的泛型结构。
一个 TypeReference
无能为力但可以使用buildType
的例子:
假设你在编写一个通用的 API 客户端,Result<T>
中的 T
是调用时才决定的。
public <T> Result<T> get(String apiPath, Class<T> entityClass) {
String jsonResponse = httpClient.get(apiPath);
// 这里无法使用 new TypeReference<Result<T>>() {},因为 T 是变量
// buildType 大显身手!它在运行时,根据传入的 entityClass 动态构建出 Result<T> 的类型
Type finalType = CacheUtil.buildType(Result.class, entityClass);
return JSON.parseObject(jsonResponse, finalType);
}
对比维度 | TypeReference (官方标准) |
buildType (框架自研) |
---|---|---|
本质 | 静态的,在编译期确定类型 | 动态的,在运行时根据参数构造类型 |
优势 | 简单、直观、官方标准、编译期安全 | 强大、灵活、可处理动态和未知泛型 |
劣势 | 灵活性差,无法处理运行时才能确定的泛型 | 理解其原理稍复杂,属于“高级工具” |
适用场景 | 业务应用开发:反序列化已知类型时。 | 框架/库开发:处理未知或动态泛型时。 |
形象比喻 | 从商店买一个标准尺寸的相框 | 使用一台 3D 打印机来定制任意物品 |
因此,buildType
的存在并非重复造轮子,而是框架设计者为了极致的灵活性和通用性所提供的“重型武器”。
亮点三:优雅的缓存重建 —— Supplier
函数式接口的应用
在 RedisCache
接口中,我发现了一个非常优雅的缓存重建方法,它完美地封装了经典的“缓存旁路”(Cache-Aside)模式。
<T> T get(RedisKeyBuild key, Class<T> clazz, Supplier<T> supplier, long ttl, ...);
当缓存未命中时,不再需要我们手写 if/else
从数据库查询,而是直接将查询逻辑作为一个 Supplier
Lambda 表达式传递进去。
// 业务代码极其简洁
User user = redisCache.get(userKey, User.class,
() -> userMapper.selectById(id), // 缓存不存在时,才会执行此DB查询
300, TimeUnit.SECONDS);
这种设计利用了函数式编程,让代码高度复用,逻辑也异常清晰。
Redis Stream 模块简单介绍:于细微处见真章
虽然在重量级消息队列场景下,Kafka
等更为常见,本项目主要使用的消息队列也是kafka。实际开发中很少使用到redis的Stream作为消息队列,但该框架的 Stream 模块中的StreamHandler类中,一个名为 streamBindingGroup
的方法展现了其对健壮性的追求。
它解决了一个常见的启动时序问题:消费者服务先于生产者启动,导致因 Stream 不存在而创建消费组失败,最终使服务崩溃。此方法通过“先发一条虚拟消息创建Stream -> 再创建消费组 -> 最后删除虚拟消息”的流程,确保了消费者在任何启动顺序下都能稳定运行。这种对边界条件的细致考虑,正是生产级框架的价值所在。
结语
这次对内部 RedisTools 框架的探索,让我深刻体会到,优秀的框架远不止是功能的堆砌。它是在对底层技术、设计模式和业务场景的深刻理解之上,进行的一次次优雅的抽象和封装。从 RedisKeyBuild
的类型安全,到 buildType
的动态泛型构造,再到 Supplier
的函数式之美,每一处设计都值得我们学习和品味。希望这次的分享,也能给正在阅读的你带来一些启发。
最新文章会在个人博客发布:nihiler.cn。