跳至正文

从泛型擦除到优雅设计–仿大麦网的微服务项目(3)



书接上文,本篇文章主要聚焦于Redis-tools这一模块分析。

 

框架蓝图:清晰的模块化设计

 

在深入细节之前,我们先来看一下这个框架的整体结构。它被清晰地拆分成了三个独立的 Maven 模块。

BASH
.
├───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 的优雅封装

 

JAVA
@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 类给出了完美的解决方案。

它的好处是什么?

  1. 类型安全与意图明确:在方法签名中使用 RedisKeyBuild 而不是 String,在编译期就杜绝了传入非法字符串的可能。doSomething(RedisKeyBuild key) 的方法签名远比 doSomething(String key) 清晰。
  2. 强制规范与统一前缀:通过将构造函数私有化,强制使用者通过静态工厂方法 createRedisKey(...) 来创建实例。这个工厂方法内部可以统一添加业务前缀或环境前缀(如 dev:prod:),保证了 Key 的规范性。
  3. 正确的集合行为:该类重写了 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 匿名内部类可以保留父类泛型签名的技巧,在编译期“写死”完整的泛型类型。

  • 优势:简洁直观、官方标准、编译期安全。
  • 适用场景:在业务代码中,当你明确知道需要反序列化的目标类型时。
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的代码:

JAVA
/**
     * 构建类型
     *
     * @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 是调用时才决定的。

JAVA
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)模式。

JAVA
<T> T get(RedisKeyBuild key, Class<T> clazz, Supplier<T> supplier, long ttl, ...);

当缓存未命中时,不再需要我们手写 if/else 从数据库查询,而是直接将查询逻辑作为一个 Supplier Lambda 表达式传递进去。

JAVA
// 业务代码极其简洁
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。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注