封装 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>
94 lines
4.2 KiB
Java
94 lines
4.2 KiB
Java
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 后缀");
|
||
}
|
||
}
|