Files
redis-stream-springboot-sta…/src/test/java/com/njcn/middle/stream/template/RedisStreamEnhanceTemplateTest.java
root 74f68c9680 feat: Redis Stream Spring Boot starter 首次提交
封装 Redis Streams 收发,零业务依赖。含自动装配、发送模板、监听容器、
autoclaim 崩溃恢复+死信淘汰、stream 定时裁剪、gzip 编解码、环境隔离。

本次随提交的改进:
- per-handler 重试次数:getMaxRetryTimes() 覆写生效,全局 redis-stream.max-retry 兜底
- 死信回调传入反序列化后的业务消息(saveExceptionFromFields)
- pom 升级 maven-surefire-plugin 至 2.22.2,使 JUnit 5 测试真正执行
- 集成测试连接信息改为 -D 注入(默认 localhost:6379),凭据不入代码
- lombok 改为 optional,不污染下游依赖
- 新增 README 使用文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:55:12 +08:00

94 lines
4.2 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.njcn.middle.stream.template;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.njcn.middle.stream.domain.StreamBaseMessage;
import com.njcn.middle.stream.support.StreamKeyBuilder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map;
import static com.njcn.middle.stream.constant.StreamMessageConstant.ENC_GZIP;
import static com.njcn.middle.stream.constant.StreamMessageConstant.ENC_PLAIN;
import static com.njcn.middle.stream.constant.StreamMessageConstant.F_BODY;
import static com.njcn.middle.stream.constant.StreamMessageConstant.F_ENC;
import static com.njcn.middle.stream.constant.StreamMessageConstant.F_KEY;
import static com.njcn.middle.stream.constant.StreamMessageConstant.F_TAG;
import static com.njcn.middle.stream.constant.StreamMessageConstant.F_TS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class RedisStreamEnhanceTemplateTest {
@Data
@EqualsAndHashCode(callSuper = true)
static class Foo extends StreamBaseMessage {
private String name;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private MapRecord<String, String, String> captureSend(boolean compress, Foo foo, String topic, StreamKeyBuilder kb) {
StringRedisTemplate redis = mock(StringRedisTemplate.class);
StreamOperations ops = mock(StreamOperations.class);
doReturn(ops).when(redis).opsForStream();
// send() 内 ofMap 返回 MapRecord编译期绑定 add(MapRecord) 重载(非 add(Record)
doReturn(RecordId.of("0-1")).when(ops).add(any(MapRecord.class));
RedisStreamEnhanceTemplate tpl = new RedisStreamEnhanceTemplate(redis, kb);
RecordId id = tpl.send(topic, foo, compress);
assertEquals("0-1", id.getValue(), "返回 XADD 的 RecordId");
ArgumentCaptor<MapRecord> cap = ArgumentCaptor.forClass(MapRecord.class);
verify(ops).add(cap.capture());
return (MapRecord<String, String, String>) cap.getValue();
}
@Test
void sendPlainFillsFiveFields() {
Foo foo = new Foo();
foo.setTag("t1");
foo.setName("hello");
MapRecord<String, String, String> rec = captureSend(false, foo, "MyTopic", new StreamKeyBuilder(false, ""));
assertEquals("MyTopic", rec.getStream(), "关隔离 → 原 topic 作 stream 名");
Map<String, String> v = rec.getValue();
assertEquals(5, v.size(), "固定五字段 key/tag/enc/body/ts");
assertEquals(foo.getKey(), v.get(F_KEY), "key 取自 message");
assertEquals("t1", v.get(F_TAG), "tag 取自 message");
assertEquals(ENC_PLAIN, v.get(F_ENC), "不压缩 enc=plain");
assertTrue(v.get(F_BODY).contains("hello"), "明文 body 含原文");
assertNotNull(v.get(F_TS), "ts 非空");
}
@Test
void sendCompressUsesGzipAndShorterBody() {
Foo foo = new Foo();
foo.setName(StrUtil.repeat("abcdefgh", 200));
String json = JSONObject.toJSONString(foo);
MapRecord<String, String, String> rec = captureSend(true, foo, "MyTopic", new StreamKeyBuilder(false, ""));
Map<String, String> v = rec.getValue();
assertEquals(ENC_GZIP, v.get(F_ENC), "压缩 enc=gzip");
assertTrue(v.get(F_BODY).length() < json.length(), "压缩 body 短于原 json");
}
@Test
void sendUsesEnvIsolatedStreamName() {
MapRecord<String, String, String> rec =
captureSend(false, new Foo(), "MyTopic", new StreamKeyBuilder(true, "prod"));
assertEquals("MyTopic_prod", rec.getStream(), "开隔离 → stream 名带 env 后缀");
}
}