DATAREDIS-864 - Polishing.

Exract nested Stream types into connection.stream package. Extract common code from Template-API based toObjectRecord handling into StreamObjectMapper.

StreamReceiver and StreamMessageListenerContainer now can emit MapRecord or ObjectRecord for easier consumption of stream records.

Original Pull Request: #356
This commit is contained in:
Mark Paluch
2018-11-06 11:41:31 +01:00
committed by Christoph Strobl
parent 13648d7c85
commit eafcb331d8
56 changed files with 2699 additions and 1890 deletions

View File

@@ -34,6 +34,14 @@ import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.convert.ListConverter;
import org.springframework.data.redis.connection.convert.MapConverter;
import org.springframework.data.redis.connection.convert.SetConverter;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StringRecord;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;

View File

@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.connection;
import org.springframework.data.redis.connection.stream.StreamRecords;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -30,12 +31,12 @@ import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

View File

@@ -15,30 +15,14 @@
*/
package org.springframework.data.redis.connection;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
* Stream-specific Redis commands.
@@ -291,736 +275,4 @@ public interface RedisStreamCommands {
*/
@Nullable
Long xTrim(byte[] key, long count);
/**
* Value object representing read offset for a Stream.
*/
@EqualsAndHashCode
@ToString
@Getter
class ReadOffset {
private final String offset;
private ReadOffset(String offset) {
this.offset = offset;
}
/**
* Read from the latest offset.
*
* @return
*/
public static ReadOffset latest() {
return new ReadOffset("$");
}
/**
* Read all new arriving elements with ids greater than the last one consumed by the consumer group.
*
* @return the {@link ReadOffset} object without a specific offset.
*/
public static ReadOffset lastConsumed() {
return new ReadOffset(">");
}
/**
* Read all arriving elements from the stream starting at {@code offset}.
*
* @param offset the stream offset.
* @return the {@link StreamOffset} object without a specific offset.
*/
public static ReadOffset from(String offset) {
Assert.hasText(offset, "Offset must not be empty");
return new ReadOffset(offset);
}
public static ReadOffset from(RecordId offset) {
if (offset.shouldBeAutoGenerated()) {
return latest();
}
return from(offset.getValue());
}
}
/**
* Value object representing a Stream Id with its offset.
*/
@EqualsAndHashCode
@ToString
@Getter
class StreamOffset<K> {
private final K key;
private final ReadOffset offset;
private StreamOffset(K key, ReadOffset offset) {
this.key = key;
this.offset = offset;
}
/**
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
*
* @param key the stream key.
* @param readOffset the {@link ReadOffset} to use.
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> create(K key, ReadOffset readOffset) {
return new StreamOffset<>(key, readOffset);
}
/**
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
*
* @param key he stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset latest(K key) {
return new StreamOffset(key, ReadOffset.latest());
}
/**
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#from(String)
* ReadOffset#from("0-0")}.
*
* @param key he stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset fromStart(K key) {
return new StreamOffset(key, ReadOffset.from("0-0"));
}
/**
* Create a {@link StreamOffset} using the given {@link Record#getId() record id} as reference to create the
* {@link ReadOffset#from(String)}.
*
* @param reference the record to be used as refrence point.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> of(Record<K, ?> reference) {
return create(reference.getStream(), ReadOffset.from(reference.getId()));
}
}
/**
* Options for reading messages from a Redis Stream.
*/
@EqualsAndHashCode
@ToString
@Getter
class StreamReadOptions {
private static final StreamReadOptions EMPTY = new StreamReadOptions(null, null, false);
private final @Nullable Long block;
private final @Nullable Long count;
private final boolean noack;
private StreamReadOptions(@Nullable Long block, @Nullable Long count, boolean noack) {
this.block = block;
this.count = count;
this.noack = noack;
}
/**
* Creates an empty {@link StreamReadOptions} instance.
*
* @return an empty {@link StreamReadOptions} instance.
*/
public static StreamReadOptions empty() {
return EMPTY;
}
/**
* Disable auto-acknowledgement when reading in the context of a consumer group.
*
* @return {@link StreamReadOptions} with {@code noack} applied.
*/
public StreamReadOptions noack() {
return new StreamReadOptions(block, count, true);
}
/**
* Use a blocking read and supply the {@link Duration timeout} after which the call will terminate if no message was
* read.
*
* @param timeout the timeout for the blocking read, must not be {@literal null} or negative.
* @return {@link StreamReadOptions} with {@code block} applied.
*/
public StreamReadOptions block(Duration timeout) {
Assert.notNull(timeout, "Block timeout must not be null!");
Assert.isTrue(!timeout.isNegative(), "Block timeout must not be negative!");
return new StreamReadOptions(timeout.toMillis(), count, noack);
}
/**
* Limit the number of messages returned per stream.
*
* @param count the maximum number of messages to read.
* @return {@link StreamReadOptions} with {@code count} applied.
*/
public StreamReadOptions count(long count) {
Assert.isTrue(count > 0, "Count must be greater or equal to zero!");
return new StreamReadOptions(block, count, noack);
}
}
/**
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
* keys.
*/
@EqualsAndHashCode
@Getter
class Consumer {
private final String group;
private final String name;
private Consumer(String group, String name) {
this.group = group;
this.name = name;
}
/**
* Create a new consumer.
*
* @param group name of the consumer group, must not be {@literal null} or empty.
* @param name name of the consumer, must not be {@literal null} or empty.
* @return the consumer {@link io.lettuce.core.Consumer} object.
*/
public static Consumer from(String group, String name) {
Assert.hasText(group, "Group must not be null");
Assert.hasText(name, "Name must not be null");
return new Consumer(group, name);
}
@Override
public String toString() {
return String.format("%s:%s", group, name);
}
}
/**
* The id of a single {@link Record} within a stream. Composed of two parts:
* {@literal <millisecondsTime>-<sequenceNumber>}.
*
* @author Christoph Strobl
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
*/
@EqualsAndHashCode
class RecordId {
private static final String GENERATE_ID = "*";
private static final String DELIMINATOR = "-";
/**
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
* computation.
*/
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
@Override
public Long getSequence() {
return null;
}
@Override
public Long getTimestamp() {
return null;
}
@Override
public boolean shouldBeAutoGenerated() {
return true;
}
};
private final String raw;
/**
* Private constructor - validate input in static initializer blocks.
*
* @param raw
*/
private RecordId(String raw) {
this.raw = raw;
}
/**
* Obtain an instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better,
* just use {@link #autoGenerate()}.
*
* @param value can be {@literal null}.
* @return new instance of {@link RecordId} if no autogenerated one requested.
*/
public static RecordId of(@Nullable String value) {
if (value == null || GENERATE_ID.equals(value)) {
return autoGenerate();
}
Assert.isTrue(value.contains(DELIMINATOR),
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
return new RecordId(value);
}
/**
* Create a new instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
*
* @param millisecondsTime
* @param sequenceNumber
* @return new instance of {@link RecordId}.
*/
public static RecordId of(long millisecondsTime, long sequenceNumber) {
return of(millisecondsTime + DELIMINATOR + sequenceNumber);
}
/**
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert
* ({@code XADD}).
*
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
*/
public static RecordId autoGenerate() {
return AUTOGENERATED;
}
/**
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getTimestamp() {
return value(0);
}
/**
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getSequence() {
return value(1);
}
/**
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
*/
public boolean shouldBeAutoGenerated() {
return false;
}
/**
* @return get the string representation of the {@literal entry-id} in
* {@literal <millisecondsTime>-<sequenceNumber>} format or {@literal *} if it
* {@link #shouldBeAutoGenerated()}. Never {@literal null}.
*/
public String getValue() {
return raw;
}
@Override
public String toString() {
return raw;
}
private Long value(int index) {
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMINATOR)[index], Long.class);
}
}
/**
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
* collection of {@link MapRecord field/value pairs}).
*
* @param <V> the type backing the {@link Record}.
* @author Christoph Strobl
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
*/
interface Record<S, V> {
/**
* The id of the stream (aka the {@literal key} in Redis).
*
* @return can be {@literal null}.
*/
@Nullable
S getStream();
/**
* The id of the entry inside the stream.
*
* @return never {@literal null}.
*/
RecordId getId();
/**
* @return the actual content. Never {@literal null}.
*/
V getValue();
/**
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
* <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param map the raw map.
* @param <K> the key type of the given {@link Map}.
* @param <V> the value type of the given {@link Map}.
* @return new instance of {@link MapRecord}.
*/
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
Assert.notNull(map, "Map must not be null!");
return StreamRecords.<S, K, V> mapBacked(map);
}
/**
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
* like {@link String} or a complex one. <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param value the value to persist.
* @param <V> the type of the backing value.
* @return new instance of {@link MapRecord}.
*/
static <S, V> ObjectRecord<S, V> of(V value) {
Assert.notNull(value, "Value must not be null!");
return StreamRecords.objectBacked(value);
}
/**
* Create a new instance of {@link Record} with the given {@link RecordId}.
*
* @param id must not be {@literal null}.
* @return new instance of {@link Record}.
*/
Record<S, V> withId(RecordId id);
/**
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
*
* @param key the Redis key identifying the stream.
* @param <S1>
* @return new instance of {@link Record}.
*/
<S1> Record<S1, V> withStreamKey(S1 key);
}
/**
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or
* a complex one.
*
* @param <V> the type of the backing Object.
*/
interface ObjectRecord<S, V> extends Record<S, V> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ObjectRecord<S, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
<S1> ObjectRecord<S1, V> withStreamKey(S1 key);
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <HK> the key type of the resulting {@link MapRecord}.
* @param <HV> the value type of the resulting {@link MapRecord}.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
*
* @param <K> the field type of the backing collection.
* @param <V> the value type of the backing collection.
*/
interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
MapRecord<S, K, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
<S1> MapRecord<S1, K, V> withStreamKey(S1 key);
/**
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
* {@link MapRecord}.
*
* @param mapFunction must not be {@literal null}.
* @param <HK> the field type of the new backing collection.
* @param <HV> the value type of the new backing collection.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
Map<HK, HV> mapped = new LinkedHashMap<>();
iterator().forEachRemaining(it -> {
Entry<HK, HV> mappedPair = mapFunction.apply(it);
mapped.put(mappedPair.getKey(), mappedPair.getValue());
});
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
}
default <S1, HK, HV> MapRecord<S1, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<S1, HK, HV>> mapFunction) {
return mapFunction.apply(this);
}
/**
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link ByteRecord} holding the serialized values.
*/
default ByteRecord serialize(@Nullable RedisSerializer serializer) {
return serialize(serializer, serializer, serializer);
}
/**
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key is binary.
* @param fieldSerializer can be {@literal null} if the fields are binary.
* @param valueSerializer can be {@literal null} if the values are binary.
* @return new {@link ByteRecord} holding the serialized values.
*/
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
MapRecord<S, byte[], byte[]> x = mapEntries(it -> Collections
.singletonMap(fieldSerializer != null ? fieldSerializer.serialize(it.getKey()) : (byte[]) it.getKey(),
valueSerializer != null ? valueSerializer.serialize(it.getValue()) : (byte[]) it.getValue())
.entrySet().iterator().next());
return StreamRecords.newRecord() //
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
.withId(getId()) //
.ofBytes(x.getValue());
}
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <OV> type of the value backing the {@link ObjectRecord}.
* @return new instance of {@link ObjectRecord}.
*/
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
return Record.<S, OV> of((OV) (mapper).fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteBufferRecord withId(RecordId id);
ByteBufferRecord withStreamKey(ByteBuffer key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections.<HK, HV> singletonMap(
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
.entrySet().iterator().next())
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream()))
: (K) getStream());
}
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
// static ByteBufferRecord of(MapRecord<byte[], byte[], byte[]> source) {
// return
// StreamRecords.newRecord().in(ByteBuffer.wrap(source.getStream())).withId(ByteBuffer.wrap(source.getId())).ofBytes(ByteBuffer.wrap(source.getValue()));
// }
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
}
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
.withStreamKey(getStream());
}
}
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteRecord withId(RecordId id);
ByteRecord withStreamKey(byte[] key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
* id} is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
.entrySet().iterator().next())
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
}
/**
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
}
}
/**
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
*
* @author Christoph Strobl
*/
interface StringRecord extends MapRecord<String, String, String> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
StringRecord withId(RecordId id);
StringRecord withStreamKey(String key);
/**
* Turn a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link StringRecord}.
*/
static StringRecord of(MapRecord<String, String, String> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
}
}
}

View File

@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.connection.stream.StringRecord;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;

View File

@@ -32,12 +32,11 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyComm
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.ReactiveStreamCommands;
import org.springframework.data.redis.connection.ReactiveStreamCommands.GroupCommand.GroupCommandAction;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.Assert;
@@ -270,7 +269,8 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
}
@SuppressWarnings("unchecked")
private static <T> StreamOffset<T>[] toStreamOffsets(Collection<RedisStreamCommands.StreamOffset<T>> streams) {
private static <T> StreamOffset<T>[] toStreamOffsets(
Collection<org.springframework.data.redis.connection.stream.StreamOffset<T>> streams) {
return streams.stream().map(it -> StreamOffset.from(it.getKey(), it.getOffset().getOffset()))
.toArray(StreamOffset[]::new);

View File

@@ -28,8 +28,15 @@ import java.util.function.Function;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.util.Assert;
/**

View File

@@ -21,9 +21,9 @@ import io.lettuce.core.XReadArgs;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.connection.convert.ListConverter;
/**

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
* @see 2.2
*/
public interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteBufferRecord withId(RecordId id);
/**
* Create a new {@link ByteBufferRecord} with the associated stream {@literal key}.
*
* @param key the binary stream key.
* @return a new {@link ByteBufferRecord}.
*/
ByteBufferRecord withStreamKey(ByteBuffer key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
* is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections.<HK, HV> singletonMap(
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
.entrySet().iterator().next()).withStreamKey(
streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream())) : (K) getStream());
}
/**
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
}
/**
* Convert a binary {@link MapRecord} into an {@link ObjectRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
.withStreamKey(getStream());
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import java.util.Collections;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
/**
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
*
* @author Christoph Strobl
* @see 2.2
*/
public interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ByteRecord withId(RecordId id);
/**
* Create a new {@link ByteRecord} with the associated stream {@literal key}.
*
* @param key the binary stream key.
* @return a new {@link ByteRecord}.
*/
ByteRecord withStreamKey(byte[] key);
/**
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
return deserialize(serializer, serializer, serializer);
}
/**
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
* is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key suites already the target format.
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
* @param valueSerializer can be {@literal null} if the values suite already the target format.
* @return new {@link MapRecord} holding the deserialized values.
*/
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
@Nullable RedisSerializer<? extends HK> fieldSerializer,
@Nullable RedisSerializer<? extends HV> valueSerializer) {
return mapEntries(it -> Collections
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
.entrySet().iterator().next())
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
}
/**
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link ByteRecord}.
*/
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.util.Assert;
/**
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
* keys.
*
* @author Mark Paluch
* @see 2.2
*/
@EqualsAndHashCode
@Getter
public class Consumer {
private final String group;
private final String name;
private Consumer(String group, String name) {
this.group = group;
this.name = name;
}
/**
* Create a new consumer.
*
* @param group name of the consumer group, must not be {@literal null} or empty.
* @param name name of the consumer, must not be {@literal null} or empty.
* @return the consumer {@link io.lettuce.core.Consumer} object.
*/
public static Consumer from(String group, String name) {
Assert.hasText(group, "Group must not be null");
Assert.hasText(name, "Name must not be null");
return new Consumer(group, name);
}
@Override
public String toString() {
return String.format("%s:%s", group, name);
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import org.springframework.data.redis.connection.stream.StreamRecords.MapBackedRecord;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
*
* @param <K> the field type of the backing map.
* @param <V> the value type of the backing map.
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.2
*/
public interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
/**
* Creates a new {@link MapRecord} associated with the {@code stream} key and {@link Map value}.
*
* @param stream the stream key.
* @param map the value.
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
*/
static <S, K, V> MapRecord<S, K, V> create(S stream, Map<K, V> map) {
Assert.notNull(stream, "Stream must not be null");
Assert.notNull(map, "Map must not be null");
return new MapBackedRecord<>(stream, RecordId.autoGenerate(), map);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
MapRecord<S, K, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
@Override
<SK> MapRecord<SK, K, V> withStreamKey(SK key);
/**
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
* {@link MapRecord}.
*
* @param mapFunction must not be {@literal null}.
* @param <HK> the field type of the new backing collection.
* @param <HV> the value type of the new backing collection.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
Map<HK, HV> mapped = new LinkedHashMap<>();
iterator().forEachRemaining(it -> {
Entry<HK, HV> mappedPair = mapFunction.apply(it);
mapped.put(mappedPair.getKey(), mappedPair.getValue());
});
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
}
/**
* Map this {@link MapRecord} by applying the mapping {@link Function}.
*
* @param mapFunction function to apply to this {@link MapRecord} element.
* @return the mapped {@link MapRecord}.
*/
default <SK, HK, HV> MapRecord<SK, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<SK, HK, HV>> mapFunction) {
return mapFunction.apply(this);
}
/**
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
*
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
* @return new {@link ByteRecord} holding the serialized values.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
default ByteRecord serialize(@Nullable RedisSerializer<?> serializer) {
return serialize((RedisSerializer) serializer, (RedisSerializer) serializer, (RedisSerializer) serializer);
}
/**
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
* is carried over to the new instance.
*
* @param streamSerializer can be {@literal null} if the key is binary.
* @param fieldSerializer can be {@literal null} if the fields are binary.
* @param valueSerializer can be {@literal null} if the values are binary.
* @return new {@link ByteRecord} holding the serialized values.
*/
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
MapRecord<S, byte[], byte[]> binaryMap = mapEntries(
it -> Collections.singletonMap(StreamSerialization.serialize(fieldSerializer, it.getKey()),
StreamSerialization.serialize(fieldSerializer, it.getValue())).entrySet().iterator().next());
return StreamRecords.newRecord() //
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
.withId(getId()) //
.ofBytes(binaryMap.getValue());
}
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <OV> type of the value backing the {@link ObjectRecord}.
* @return new instance of {@link ObjectRecord}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
return Record.<S, OV> of((OV) mapper.fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import org.springframework.data.redis.connection.stream.StreamRecords.ObjectBackedRecord;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.util.Assert;
/**
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or a
* complex one.
*
* @param <V> the type of the backing Object.
* @author Christoph Strobl
* @author Mark Paluch
* @see 2.2
*/
public interface ObjectRecord<S, V> extends Record<S, V> {
/**
* Creates a new {@link ObjectRecord} associated with the {@code stream} key and {@code value}.
*
* @param stream the stream key.
* @param value the value.
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
*/
static <S, V> ObjectRecord<S, V> create(S stream, V value) {
Assert.notNull(stream, "Stream must not be null");
Assert.notNull(value, "Value must not be null");
return new ObjectBackedRecord<>(stream, RecordId.autoGenerate(), value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
ObjectRecord<S, V> withId(RecordId id);
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
*/
<SK> ObjectRecord<SK, V> withStreamKey(SK key);
/**
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
* {@link RecordId id} is carried over to the new instance.
*
* @param mapper must not be {@literal null}.
* @param <HK> the key type of the resulting {@link MapRecord}.
* @param <HV> the value type of the resulting {@link MapRecord}.
* @return new instance of {@link MapRecord}.
*/
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.util.Assert;
/**
* Value object representing read offset for a Stream.
*/
@EqualsAndHashCode
@ToString
@Getter
public final class ReadOffset {
private final String offset;
private ReadOffset(String offset) {
this.offset = offset;
}
/**
* Read from the latest offset.
*
* @return
*/
public static ReadOffset latest() {
return new ReadOffset("$");
}
/**
* Read all new arriving elements with ids greater than the last one consumed by the consumer group.
*
* @return the {@link ReadOffset} object without a specific offset.
*/
public static ReadOffset lastConsumed() {
return new ReadOffset(">");
}
/**
* Read all arriving elements from the stream starting at {@code offset}.
*
* @param offset the stream offset.
* @return the {@link ReadOffset} starting at {@code offset}.
*/
public static ReadOffset from(String offset) {
Assert.hasText(offset, "Offset must not be empty");
return new ReadOffset(offset);
}
/**
* Read all arriving elements from the stream starting at {@link RecordId}. Using a
* {@link RecordId#shouldBeAutoGenerated() auto-generated} {@link RecordId} returns the {@link #latest()} read offset.
*
* @param offset the stream offset.
* @return the {@link ReadOffset} starting at {@link RecordId}.
*/
public static ReadOffset from(RecordId offset) {
if (offset.shouldBeAutoGenerated()) {
return latest();
}
return from(offset.getValue());
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
* collection of {@link MapRecord field/value pairs}).
*
* @param <V> the type backing the {@link Record}.
* @author Christoph Strobl
* @since 2.2
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
*/
public interface Record<S, V> {
/**
* The id of the stream (aka the {@literal key} in Redis).
*
* @return can be {@literal null}.
*/
@Nullable
S getStream();
/**
* The id of the entry inside the stream.
*
* @return never {@literal null}.
*/
RecordId getId();
/**
* @return the actual content. Never {@literal null}.
*/
V getValue();
/**
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
* <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param map the raw map.
* @param <K> the key type of the given {@link Map}.
* @param <V> the value type of the given {@link Map}.
* @return new instance of {@link MapRecord}.
*/
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
Assert.notNull(map, "Map must not be null!");
return StreamRecords.mapBacked(map);
}
/**
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
* like {@link String} or a complex one. <br />
* You may want to use the builders available via {@link StreamRecords}.
*
* @param value the value to persist.
* @param <V> the type of the backing value.
* @return new instance of {@link MapRecord}.
*/
static <S, V> ObjectRecord<S, V> of(V value) {
Assert.notNull(value, "Value must not be null!");
return StreamRecords.objectBacked(value);
}
/**
* Create a new instance of {@link Record} with the given {@link RecordId}.
*
* @param id must not be {@literal null}.
* @return new instance of {@link Record}.
*/
Record<S, V> withId(RecordId id);
/**
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
*
* @param key the Redis key identifying the stream.
* @param <SK>
* @return new instance of {@link Record}.
*/
<SK> Record<SK, V> withStreamKey(SK key);
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
* The id of a single {@link Record} within a stream. Composed of two parts:
* {@literal <millisecondsTime>-<sequenceNumber>}.
*
* @author Christoph Strobl
* @since 2.2
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
*/
@EqualsAndHashCode
public class RecordId {
private static final String GENERATE_ID = "*";
private static final String DELIMITER = "-";
/**
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
* computation.
*/
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
@Override
public Long getSequence() {
return null;
}
@Override
public Long getTimestamp() {
return null;
}
@Override
public boolean shouldBeAutoGenerated() {
return true;
}
};
private final String raw;
/**
* Private constructor - validate input in static initializer blocks.
*
* @param raw
*/
private RecordId(String raw) {
this.raw = raw;
}
/**
* Obtain an instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better, just
* use {@link #autoGenerate()}.
*
* @param value can be {@literal null}.
* @return new instance of {@link RecordId} if no autogenerated one requested.
*/
public static RecordId of(@Nullable String value) {
if (value == null || GENERATE_ID.equals(value)) {
return autoGenerate();
}
Assert.isTrue(value.contains(DELIMITER),
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
return new RecordId(value);
}
/**
* Create a new instance of {@link RecordId} using the provided String formatted as
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
*
* @param millisecondsTime
* @param sequenceNumber
* @return new instance of {@link RecordId}.
*/
public static RecordId of(long millisecondsTime, long sequenceNumber) {
return of(millisecondsTime + DELIMITER + sequenceNumber);
}
/**
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert ({@code XADD}).
*
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
*/
public static RecordId autoGenerate() {
return AUTOGENERATED;
}
/**
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getTimestamp() {
return value(0);
}
/**
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
*
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
*/
@Nullable
public Long getSequence() {
return value(1);
}
/**
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
*/
public boolean shouldBeAutoGenerated() {
return false;
}
/**
* @return get the string representation of the {@literal entry-id} in {@literal <millisecondsTime>-<sequenceNumber>}
* format or {@literal *} if it {@link #shouldBeAutoGenerated()}. Never {@literal null}.
*/
public String getValue() {
return raw;
}
@Override
public String toString() {
return raw;
}
private Long value(int index) {
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMITER)[index], Long.class);
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.util.Assert;
/**
* Value object representing a Stream Id with its offset.
*
* @author Mark Paluch
* @see 2.2
*/
@EqualsAndHashCode
@ToString
@Getter
public final class StreamOffset<K> {
private final K key;
private final ReadOffset offset;
private StreamOffset(K key, ReadOffset offset) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(offset, "ReadOffset must not be null");
this.key = key;
this.offset = offset;
}
/**
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
*
* @param stream the stream key.
* @param readOffset the {@link ReadOffset} to use.
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> create(K stream, ReadOffset readOffset) {
return new StreamOffset<>(stream, readOffset);
}
/**
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
*
* @param stream the stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> latest(K stream) {
return new StreamOffset<>(stream, ReadOffset.latest());
}
/**
* Create a {@link StreamOffset} given {@code stream} starting at {@link ReadOffset#from(String)
* ReadOffset#from("0-0")}.
*
* @param stream the stream key.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> fromStart(K stream) {
return new StreamOffset<>(stream, ReadOffset.from("0-0"));
}
/**
* Create a {@link StreamOffset} from the given {@link Record#getId() record id} as reference to create the
* {@link ReadOffset#from(String)}.
*
* @param reference the record to be used as reference point.
* @param <K>
* @return new instance of {@link StreamOffset}.
*/
public static <K> StreamOffset<K> from(Record<K, ?> reference) {
Assert.notNull(reference, "Reference record must not be null");
return create(reference.getStream(), ReadOffset.from(reference.getId()));
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.time.Duration;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Options for reading messages from a Redis Stream.
*
* @author Mark Paluch
* @see 2.2
*/
@EqualsAndHashCode
@ToString
@Getter
public class StreamReadOptions {
private static final StreamReadOptions EMPTY = new StreamReadOptions(null, null, false);
private final @Nullable Long block;
private final @Nullable Long count;
private final boolean noack;
private StreamReadOptions(@Nullable Long block, @Nullable Long count, boolean noack) {
this.block = block;
this.count = count;
this.noack = noack;
}
/**
* Creates an empty {@link StreamReadOptions} instance.
*
* @return an empty {@link StreamReadOptions} instance.
*/
public static StreamReadOptions empty() {
return EMPTY;
}
/**
* Disable auto-acknowledgement when reading in the context of a consumer group.
*
* @return {@link StreamReadOptions} with {@code noack} applied.
*/
public StreamReadOptions noack() {
return new StreamReadOptions(block, count, true);
}
/**
* Use a blocking read and supply the {@link Duration timeout} after which the call will terminate if no message was
* read.
*
* @param timeout the timeout for the blocking read, must not be {@literal null} or negative.
* @return {@link StreamReadOptions} with {@code block} applied.
*/
public StreamReadOptions block(Duration timeout) {
Assert.notNull(timeout, "Block timeout must not be null!");
Assert.isTrue(!timeout.isNegative(), "Block timeout must not be negative!");
return new StreamReadOptions(timeout.toMillis(), count, noack);
}
/**
* Limit the number of messages returned per stream.
*
* @param count the maximum number of messages to read.
* @return {@link StreamReadOptions} with {@code count} applied.
*/
public StreamReadOptions count(long count) {
Assert.isTrue(count > 0, "Count must be greater or equal to zero!");
return new StreamReadOptions(block, count, noack);
}
}

View File

@@ -1,4 +1,19 @@
package org.springframework.data.redis.connection;
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import lombok.EqualsAndHashCode;
@@ -7,20 +22,14 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StringRecord;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link StreamRecords} provides utilities to create specific
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record} instances.
* {@link StreamRecords} provides utilities to create specific {@link Record} instances.
*
* @author Christoph Strobl
* @since 2.2
@@ -83,15 +92,22 @@ public class StreamRecords {
}
/**
* Obtain new instance of {@link RecordBuilder} to fluently create
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record records}.
* Obtain new instance of {@link RecordBuilder} to fluently create {@link Record records}.
*
* @return
*/
public static RecordBuilder newRecord() {
return new RecordBuilder(null, RecordId.autoGenerate());
public static RecordBuilder<?> newRecord() {
return new RecordBuilder<>(null, RecordId.autoGenerate());
}
// Utility constructor
private StreamRecords() {}
/**
* Builder for {@link Record}.
*
* @param <S> stream keyy type.
*/
public static class RecordBuilder<S> {
private RecordId id;
@@ -103,21 +119,49 @@ public class StreamRecords {
this.id = recordId;
}
/**
* Configure a stream key.
*
* @param stream the stream key, must not be null.
* @param <STREAM_KEY>
* @return {@literal this} {@link RecordBuilder}.
*/
public <STREAM_KEY> RecordBuilder<STREAM_KEY> in(STREAM_KEY stream) {
Assert.notNull(stream, "Stream key must not be null");
return new RecordBuilder<>(stream, id);
}
/**
* Configure a record Id given a {@link String}. Associates a user-supplied record id instead of using
* server-generated record Id's.
*
* @param id the record id.
* @return {@literal this} {@link RecordBuilder}.
* @see RecordId
*/
public RecordBuilder<S> withId(String id) {
return withId(RecordId.of(id));
}
/**
* Configure a {@link RecordId}. Associates a user-supplied record id instead of using server-generated record Id's.
*
* @param id the record id.
* @return {@literal this} {@link RecordBuilder}.
*/
public RecordBuilder<S> withId(RecordId id) {
Assert.notNull(id, "RecordId must not be null");
this.id = id;
return this;
}
/**
* Create a {@link MapRecord}.
*
* @param map
* @param <K>
* @param <V>
@@ -128,17 +172,22 @@ public class StreamRecords {
}
/**
* Create a {@link StringRecord}.
*
* @param map
* @return new instance of {@link StringRecord}.
* @see MapRecord
*/
public StringRecord ofStrings(Map<String, String> map) {
return new StringMapBackedRecord(ObjectUtils.nullSafeToString(stream), id, map);
}
/**
* Create an {@link ObjectRecord}.
*
* @param value
* @param <V>
* @return ni instance of {@link ObjectRecord}.
* @return new instance of {@link ObjectRecord}.
*/
public <V> ObjectRecord<S, V> ofObject(V value) {
return new ObjectBackedRecord<>(stream, id, value);
@@ -160,7 +209,7 @@ public class StreamRecords {
*/
public ByteBufferRecord ofBuffer(Map<ByteBuffer, ByteBuffer> value) {
ByteBuffer streamKey = null;
ByteBuffer streamKey;
if (stream instanceof ByteBuffer) {
streamKey = (ByteBuffer) stream;
@@ -176,13 +225,20 @@ public class StreamRecords {
}
}
/**
* Default implementation of {@link MapRecord}.
*
* @param <S>
* @param <K>
* @param <V>
*/
static class MapBackedRecord<S, K, V> implements MapRecord<S, K, V> {
private @Nullable S stream;
private RecordId recordId;
private final Map<K, V> kvMap;
MapBackedRecord(S stream, RecordId recordId, Map<K, V> kvMap) {
MapBackedRecord(@Nullable S stream, RecordId recordId, Map<K, V> kvMap) {
this.stream = stream;
this.recordId = recordId;
@@ -263,6 +319,9 @@ public class StreamRecords {
}
}
/**
* Default implementation of {@link ByteRecord}.
*/
static class ByteMapBackedRecord extends MapBackedRecord<byte[], byte[], byte[]> implements ByteRecord {
ByteMapBackedRecord(byte[] stream, RecordId recordId, Map<byte[], byte[]> map) {
@@ -274,11 +333,15 @@ public class StreamRecords {
return new ByteMapBackedRecord(key, getId(), getValue());
}
@Override
public ByteMapBackedRecord withId(RecordId id) {
return new ByteMapBackedRecord(getStream(), id, getValue());
}
}
/**
* Default implementation of {@link ByteBufferRecord}.
*/
static class ByteBufferMapBackedRecord extends MapBackedRecord<ByteBuffer, ByteBuffer, ByteBuffer>
implements ByteBufferRecord {
@@ -291,11 +354,15 @@ public class StreamRecords {
return new ByteBufferMapBackedRecord(key, getId(), getValue());
}
@Override
public ByteBufferMapBackedRecord withId(RecordId id) {
return new ByteBufferMapBackedRecord(getStream(), id, getValue());
}
}
/**
* Default implementation of StringRecord.
*/
static class StringMapBackedRecord extends MapBackedRecord<String, String, String> implements StringRecord {
StringMapBackedRecord(String stream, RecordId recordId, Map<String, String> stringStringMap) {
@@ -307,11 +374,18 @@ public class StreamRecords {
return new StringMapBackedRecord(key, getId(), getValue());
}
@Override
public StringMapBackedRecord withId(RecordId id) {
return new StringMapBackedRecord(getStream(), id, getValue());
}
}
/**
* Default implementation of {@link ObjectRecord}.
*
* @param <S>
* @param <V>
*/
@EqualsAndHashCode
static class ObjectBackedRecord<S, V> implements ObjectRecord<S, V> {
@@ -319,7 +393,7 @@ public class StreamRecords {
private RecordId recordId;
private final V value;
public ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
this.stream = stream;
this.recordId = recordId;
@@ -349,7 +423,7 @@ public class StreamRecords {
}
@Override
public <S1> ObjectRecord<S1, V> withStreamKey(S1 key) {
public <SK> ObjectRecord<SK, V> withStreamKey(SK key) {
return new ObjectBackedRecord<>(key, recordId, value);
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
/**
* Utility methods for stream serialization.
*
* @author Mark Paluch
* @since 2.2
*/
class StreamSerialization {
/**
* Serialize the {@code value} using the optional {@link RedisSerializer}. If no conversion is possible, {@code value}
* is assumed to be a byte array.
*
* @param serializer the serializer. Can be {@literal null}.
* @param value the value to serialize.
* @return the serialized (binary) representation of {@code value}.
*/
@SuppressWarnings("unchecked")
static byte[] serialize(@Nullable RedisSerializer<?> serializer, Object value) {
return canSerialize(serializer, value) ? ((RedisSerializer) serializer).serialize(value) : (byte[]) value;
}
/**
* Returns whether the given {@link RedisSerializer} is capable of serializing the {@code value} to {@literal byte[]}.
*
* @param serializer the serializer. Can be {@literal null}.
* @param value the value to serialize.
* @return {@literal true} if the given {@link RedisSerializer} is capable of serializing the {@code value} to
* {@literal byte[]}.
*/
private static boolean canSerialize(@Nullable RedisSerializer<?> serializer, Object value) {
return serializer != null && (value == null || serializer.canSerialize(value.getClass()));
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.stream;
/**
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
*
* @author Christoph Strobl
* @since 2.2
*/
public interface StringRecord extends MapRecord<String, String, String> {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
*/
@Override
StringRecord withId(RecordId id);
/**
* Create a new {@link StringRecord} with the associated stream {@literal key}.
*
* @param key the stream key.
* @return a new {@link StringRecord}.
*/
StringRecord withStreamKey(String key);
/**
* Convert a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
*
* @param source must not be {@literal null}.
* @return new instance of {@link StringRecord}.
*/
static StringRecord of(MapRecord<String, String, String> source) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
}
}

View File

@@ -0,0 +1,6 @@
/**
* Data structures and interfaces to interact with Redis Streams.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.redis.connection.stream;

View File

@@ -19,11 +19,11 @@ import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;

View File

@@ -20,12 +20,12 @@ import java.util.Map;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.lang.Nullable;

View File

@@ -25,23 +25,21 @@ import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.ReactiveStreamCommands;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -58,20 +56,60 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
private final ReactiveRedisTemplate<?, ?> template;
private final RedisSerializationContext<K, ?> serializationContext;
private final StreamObjectMapper objectMapper;
private final RedisCustomConversions rcc = new RedisCustomConversions();
private DefaultConversionService conversionService;
private HashMapper<? super K, ? super HK, ? super HV> mapper;
public DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
RedisSerializationContext<K, ?> serializationContext,
@Nullable HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
this.template = template;
this.serializationContext = serializationContext;
this.objectMapper = new StreamObjectMapper(hashMapper) {
this.conversionService = new DefaultConversionService();
this.mapper = mapper != null ? mapper : (HashMapper<? super K, ? super HK, ? super HV>) new ObjectHashMapper();
rcc.registerConvertersIn(conversionService);
@Override
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
if (objectMapper.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
return new HashMapper<Object, Object, Object>() {
@Override
public Map<Object, Object> toHash(Object object) {
Object key = "payload";
Object value = object;
if (serializationContext.getHashKeySerializationPair() == null) {
key = key.toString().getBytes(StandardCharsets.UTF_8);
}
if (serializationContext.getHashValueSerializationPair() == null) {
value = conversionService.convert(value, byte[].class);
}
return Collections.singletonMap(key, value);
}
@Override
public Object fromHash(Map<Object, Object> hash) {
Object value = hash.values().iterator().next();
if (ClassUtils.isAssignableValue(targetType, value)) {
return value;
}
HV deserialized = deserializeHashValue((ByteBuffer) value);
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
return value;
}
return conversionService.convert(deserialized, targetType);
}
};
}
return super.doGetHashMapper(conversionService, targetType);
}
};
}
/*
@@ -94,12 +132,14 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
* @see org.springframework.data.redis.core.ReactiveStreamOperations#add(java.lang.Object, java.util.Map)
*/
@Override
public Mono<RecordId> add(MapRecord<K, HK, HV> record) {
public Mono<RecordId> add(Record<K, ?> record) {
Assert.notNull(record.getStream(), "Key must not be null!");
Assert.notEmpty(record.getValue(), "Body must not be null!");
Assert.notNull(record.getValue(), "Body must not be null!");
return createMono(connection -> connection.xAdd(serializeRecord(record)));
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
return createMono(connection -> connection.xAdd(serializeRecord(input)));
}
/*
@@ -234,64 +274,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
@Override
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
if (rcc.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
HK key = (HK) "payload";
HV value = (HV) object;
if (serializationContext.getHashKeySerializationPair() == null) {
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
}
if (serializationContext.getHashValueSerializationPair() == null) {
value = (HV) conversionService.convert(value, byte[].class);
}
return Collections.singletonMap(key, value);
// return (Map<HK, HV>) Collections.singletonMap("payload".getBytes(StandardCharsets.UTF_8),
// serializeHashValueIfRequires((HV) object));
}
@Override
public V fromHash(Map<HK, HV> hash) {
Object value = hash.values().iterator().next();
if (ClassUtils.isAssignableValue(targetType, value)) {
return (V) value;
}
return (V) deserializeHashValue((ByteBuffer) value);
}
};
}
if (mapper instanceof ObjectHashMapper) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
}
@Override
public V fromHash(Map<HK, HV> hash) {
Map<byte[], byte[]> map = hash.entrySet().stream()
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
return (V) mapper.fromHash((Map) map);
}
};
}
return (HashMapper<V, HK, HV>) mapper;
return objectMapper.getHashMapper(targetType);
}
@SuppressWarnings("unchecked")
@@ -319,21 +302,24 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
return serializationContext.getKeySerializationPair().write(key);
}
@SuppressWarnings("unchecked")
private ByteBuffer rawHashKey(HK key) {
try {
return serializationContext.getHashKeySerializationPair().write(key);
} catch (IllegalStateException e) {}
return ByteBuffer.wrap(conversionService.convert(key, byte[].class));
return ByteBuffer.wrap(objectMapper.getConversionService().convert(key, byte[].class));
}
@SuppressWarnings("unchecked")
private ByteBuffer rawValue(HV value) {
try {
return serializationContext.getHashValueSerializationPair().write(value);
} catch (IllegalStateException e) {}
return ByteBuffer.wrap(conversionService.convert(value, byte[].class));
return ByteBuffer.wrap(objectMapper.getConversionService().convert(value, byte[].class));
}
@SuppressWarnings("unchecked")
private HK readHashKey(ByteBuffer buffer) {
return (HK) serializationContext.getHashKeySerializationPair().getReader().read(buffer);
}
@@ -342,6 +328,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
return serializationContext.getKeySerializationPair().read(buffer);
}
@SuppressWarnings("unchecked")
private HV deserializeHashValue(ByteBuffer buffer) {
return (HV) serializationContext.getHashValueSerializationPair().read(buffer);
}
@@ -355,12 +342,12 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
.next();
}
private ByteBufferRecord serializeRecord(MapRecord<K, HK, HV> record) {
private ByteBufferRecord serializeRecord(MapRecord<K, ? extends HK, ? extends HV> record) {
return ByteBufferRecord
.of(record.map(it -> it.mapEntries(this::serializeRecordFields).withStreamKey(rawKey(record.getStream()))));
}
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<HK, HV> it) {
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<? extends HK, ? extends HV> it) {
return Collections.singletonMap(rawHashKey(it.getKey()), rawValue(it.getValue())).entrySet().iterator().next();
}
}

View File

@@ -21,24 +21,23 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -50,17 +49,61 @@ import org.springframework.util.ClassUtils;
*/
class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> implements StreamOperations<K, HK, HV> {
private final RedisCustomConversions rcc = new RedisCustomConversions();
private DefaultConversionService conversionService;
private HashMapper<?, ? super HK, ? super HV> mapper;
private final StreamObjectMapper objectMapper;
@SuppressWarnings("unchecked")
DefaultStreamOperations(RedisTemplate<K, ?> template,
@Nullable HashMapper<? super K, ? super HK, ? super HV> mapper) {
super((RedisTemplate<K, Object>) template);
this.conversionService = new DefaultConversionService();
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
rcc.registerConvertersIn(conversionService);
this.objectMapper = new StreamObjectMapper(mapper) {
@Override
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
if (isSimpleType(targetType)) {
return new HashMapper<Object, Object, Object>() {
@Override
public Map<Object, Object> toHash(Object object) {
Object key = "payload";
Object value = object;
if (!template.isEnableDefaultSerializer()) {
if (template.getHashKeySerializer() == null) {
key = key.toString().getBytes(StandardCharsets.UTF_8);
}
if (template.getHashValueSerializer() == null) {
value = serializeHashValueIfRequires((HV) object);
}
}
return Collections.singletonMap(key, value);
}
@Override
public Object fromHash(Map<Object, Object> hash) {
Object value = hash.values().iterator().next();
if (ClassUtils.isAssignableValue(targetType, value)) {
return value;
}
HV deserialized = deserializeHashValue((byte[]) value);
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
return value;
}
return conversionService.convert(deserialized, targetType);
}
};
}
return super.doGetHashMapper(conversionService, targetType);
}
};
}
/*
@@ -76,12 +119,17 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.StreamOperations#add(java.lang.Object, java.util.Map)
* @see org.springframework.data.redis.core.StreamOperations#add(org.springframework.data.redis.connection.stream.Record)
*/
@Nullable
@Override
public RecordId add(MapRecord<K, HK, HV> record) {
public RecordId add(Record<K, ?> record) {
ByteRecord binaryRecord = record.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
Assert.notNull(record, "Record must not be null");
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
ByteRecord binaryRecord = input.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
return execute(connection -> connection.xAdd(binaryRecord), true);
}
@@ -148,7 +196,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
@Override
public List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new RecordDeserializingRedisCallback() {
@Nullable
@Override
@@ -165,7 +213,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
@Override
public List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new RecordDeserializingRedisCallback() {
@Nullable
@Override
@@ -182,7 +230,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
@Override
public List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new RecordDeserializingRedisCallback() {
@Nullable
@Override
@@ -199,7 +247,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
@Override
public List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit) {
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
return execute(new RecordDeserializingRedisCallback() {
@Nullable
@Override
@@ -218,68 +266,12 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
@Override
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
if (rcc.isSimpleType(targetType)) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
HK key = (HK) "payload";
HV value = (HV) object;
if (!template.isEnableDefaultSerializer()) {
if (template.getHashKeySerializer() == null) {
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
}
if (template.getHashValueSerializer() == null) {
value = (HV) serializeHashValueIfRequires((HV) object);
}
}
return Collections.singletonMap(key, value);
}
@Override
public V fromHash(Map<HK, HV> hash) {
Object value = hash.values().iterator().next();
if (ClassUtils.isAssignableValue(targetType, value)) {
return (V) value;
}
return (V) deserializeHashValue((byte[]) value, (Class<HV>) targetType);
}
};
}
if (mapper instanceof ObjectHashMapper) {
return new HashMapper<V, HK, HV>() {
@Override
public Map<HK, HV> toHash(V object) {
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
}
@Override
public V fromHash(Map<HK, HV> hash) {
Map<byte[], byte[]> map = hash.entrySet().stream()
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
return (V) mapper.fromHash((Map) map);
}
};
}
return (HashMapper<V, HK, HV>) mapper;
return objectMapper.getHashMapper(targetType);
}
protected byte[] serializeHashValueIfRequires(HV value) {
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer())
: conversionService.convert(value, byte[].class);
: objectMapper.getConversionService().convert(value, byte[].class);
}
protected boolean hashValueSerializerPresent() {
@@ -288,14 +280,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
protected HV deserializeHashValue(byte[] bytes, Class<HV> targetType) {
return hashValueSerializerPresent() ? (HV) hashValueSerializer().deserialize(bytes)
: conversionService.convert(bytes, targetType);
: objectMapper.getConversionService().convert(bytes, targetType);
}
byte[] serialize(Object value, RedisSerializer serializer) {
private byte[] serialize(Object value, RedisSerializer serializer) {
Object _value = value;
if (!serializer.canSerialize(value.getClass())) {
_value = conversionService.convert(value, serializer.getTargetType());
_value = objectMapper.getConversionService().convert(value, serializer.getTargetType());
}
return serializer.serialize(_value);
}
@@ -308,14 +300,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
.toArray(it -> new StreamOffset[it]);
}
abstract class RecordDeserializingRedisCallback<K, HK, HV> implements RedisCallback<List<MapRecord<K, HK, HV>>> {
abstract class RecordDeserializingRedisCallback implements RedisCallback<List<MapRecord<K, HK, HV>>> {
public final List<MapRecord<K, HK, HV>> doInRedis(RedisConnection connection) {
List<ByteRecord> x = inRedis(connection);
List<ByteRecord> raw = inRedis(connection);
List<MapRecord<K, HK, HV>> result = new ArrayList<>();
for (ByteRecord record : x) {
for (ByteRecord record : raw) {
result.add(record.deserialize(keySerializer(), hashKeySerializer(), hashValueSerializer()));
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import org.springframework.data.redis.hash.HashMapper;
/**
* Function that returns a {@link HashMapper} for a given {@link Class type}.
* <p/>
* Implementors of this interface can return a generic or a specific {@link HashMapper} implementation, depending on the
* serialization strategy for the requested {@link Class target type}.
*
* @param <HK>
* @param <HV>
* @author Mark Paluch
* @since 2.2
*/
@FunctionalInterface
public interface HashMapperProvider<HK, HV> {
/**
* Get the {@link HashMapper} for a specific type.
*
* @param targetType must not be {@literal null}.
* @param <V> the value target type.
* @return the {@link HashMapper} suitable for a given type;
*/
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.core;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -645,9 +646,10 @@ public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V
* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@SuppressWarnings("unchecked")
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
RedisSerializationContext<K, ?> serializationContext) {
return opsForStream(serializationContext, null);
return opsForStream(serializationContext, (HashMapper) new ObjectHashMapper());
}
protected <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(

View File

@@ -23,17 +23,18 @@ import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.util.Assert;
/**
* Redis stream specific operations.
@@ -42,7 +43,7 @@ import org.springframework.data.redis.hash.HashMapper;
* @author Christoph Strobl
* @since 2.2
*/
public interface ReactiveStreamOperations<K, HK, HV> {
public interface ReactiveStreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
/**
* Acknowledge one or more records as processed.
@@ -88,7 +89,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* @return the record Ids.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Flux<RecordId> add(K key, Publisher<? extends Map<HK, HV>> bodyPublisher) {
default Flux<RecordId> add(K key, Publisher<? extends Map<? extends HK, ? extends HV>> bodyPublisher) {
return Flux.from(bodyPublisher).flatMap(it -> add(key, it));
}
@@ -96,12 +97,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* Append a record to the stream {@code key}.
*
* @param key the stream key.
* @param body record body.
* @param content record content as Map.
* @return the {@link Mono} emitting the {@link RecordId}.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
default Mono<RecordId> add(K key, Map<HK, HV> body) {
return add(StreamRecords.newRecord().in(key).ofMap(body));
default Mono<RecordId> add(K key, Map<? extends HK, ? extends HV> content) {
return add(StreamRecords.newRecord().in(key).ofMap(content));
}
/**
@@ -111,18 +112,20 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* @return the {@link Mono} emitting the {@link RecordId}.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
Mono<RecordId> add(MapRecord<K, HK, HV> record);
@SuppressWarnings("unchecked")
default Mono<RecordId> add(MapRecord<K, ? extends HK, ? extends HV> record) {
return add((Record) record);
}
/**
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
*
* @param record must not be {@literal null}.
* @param <V>
* @return
* @return the {@link Mono} emitting the {@link RecordId}.
* @see MapRecord
* @see ObjectRecord
*/
default <V> Mono<RecordId> add(Record<K, V> record) {
return add(toMapRecord(record));
}
Mono<RecordId> add(Record<K, ?> record);
/**
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
@@ -163,7 +166,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param key
* @param group name of the consumer group.
* @return the {@link Mono} emitting {@literal ok} if successful.. {@literal null} when used in pipeline /
* @return the {@link Mono} emitting {@literal OK} if successful.. {@literal null} when used in pipeline /
* transaction.
*/
default Mono<String> createGroup(K key, String group) {
@@ -176,7 +179,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* @param key
* @param readOffset
* @param group name of the consumer group.
* @return the {@link Mono} emitting {@literal ok} if successful.
* @return the {@link Mono} emitting {@literal OK} if successful.
*/
Mono<String> createGroup(K key, ReadOffset readOffset, String group);
@@ -185,7 +188,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param key the stream key.
* @param consumer consumer identified by group name and consumer key.
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
*/
Mono<String> deleteConsumer(K key, Consumer consumer);
@@ -194,7 +197,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param key the stream key.
* @param group name of the consumer group.
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
*/
Mono<String> destroyGroup(K key, String group);
@@ -212,95 +215,131 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return the {@link Flux} emitting the records one by one.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range) {
return range(key, range, Limit.unlimited());
}
/**
* Read all records from a stream within a specific {@link Range}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return lthe {@link Flux} emitting the records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
return range(key, range, Limit.unlimited(), targetType);
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return lthe {@link Flux} emitting the records one by one.
* @return lthe {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
/**
* Read all records from a stream within a specific {@link Range}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
return range(targetType, key, range, Limit.unlimited());
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return lthe {@link Flux} emitting the records one by one.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Limit limit, Class<V> targetType) {
return range(key, range, limit).map(it -> toObjectRecord(it, targetType));
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
Assert.notNull(targetType, "Target type must not be null");
return range(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
}
/**
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
*
* @param stream the stream to read from.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
Assert.notNull(stream, "StreamOffset must not be null");
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param stream the stream to read from.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K> stream) {
Assert.notNull(stream, "StreamOffset must not be null");
return read(targetType, StreamReadOptions.empty(), new StreamOffset[] { stream });
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param targetType
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
return read(StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param streams the streams to read from.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
return read(targetType, StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
return read(StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
Flux<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read records from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
*
* @oaram targetType
* @param targetType the target type of the payload.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(readOptions, streams).map(it -> toObjectRecord(it, targetType));
Assert.notNull(targetType, "Target type must not be null");
return read(readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
}
/**
@@ -308,7 +347,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param consumer consumer/group.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams) {
@@ -316,11 +355,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param consumer consumer/group.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams) {
@@ -333,24 +373,27 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
*
* @param targetType
* @param targetType the target type of the payload.
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param streams the streams to read from.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(consumer, readOptions, streams).map(it -> toObjectRecord(it, targetType));
Assert.notNull(targetType, "Target type must not be null");
return read(consumer, readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
}
/**
@@ -358,40 +401,53 @@ public interface ReactiveStreamOperations<K, HK, HV> {
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range) {
return reverseRange(key, range, Limit.unlimited());
}
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
return reverseRange(targetType, key, range, Limit.unlimited());
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
*
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
*
* @param targetType
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
return reverseRange(targetType, key, range, Limit.unlimited());
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
* {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream.
* @return the {@link Flux} emitting records one by one.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
return reverseRange(key, range, limit).map(it -> toObjectRecord(it, targetType));
Assert.notNull(targetType, "Target type must not be null");
return reverseRange(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
}
/**
@@ -411,37 +467,6 @@ public interface ReactiveStreamOperations<K, HK, HV> {
* @param <V>
* @return the {@link HashMapper} suitable for a given type;
*/
@Override
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
/**
* App
*
* @param value
* @param <V>
* @return
*/
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
if (value instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) value);
// TODO: should we have this?
if (entry.getValue() instanceof Map) {
return StreamRecords.newRecord().in(value.getStream()).withId(value.getId()).ofMap((Map) entry.getValue());
}
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
}
if (value instanceof MapRecord) {
return (MapRecord<K, HK, HV>) value;
}
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
}
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
return entry.toObjectRecord(getHashMapper(targetType));
}
}

View File

@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.RedisClientInfo;
@@ -627,8 +628,7 @@ public interface RedisOperations<K, V> {
/**
* Returns the operations performed on Streams.
*
* @param hashMapper the {@link HashMapper} to use when converting
* {@link org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord}.
* @param hashMapper the {@link HashMapper} to use when converting {@link ObjectRecord}.
* @return stream operations.
* @since 2.2
*/

View File

@@ -46,6 +46,7 @@ import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.script.ScriptExecutor;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationUtils;
@@ -1310,7 +1311,7 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
public <HK, HV> StreamOperations<K, HK, HV> opsForStream() {
if (streamOps == null) {
streamOps = new DefaultStreamOperations<>(this, null);
streamOps = new DefaultStreamOperations<>(this, new ObjectHashMapper());
}
return (StreamOperations<K, HK, HV>) streamOps;
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Utility to provide a {@link HashMapper} for Stream object conversion.
* <p/>
* This utility can use generic a {@link HashMapper} or adapt specifically to {@link ObjectHashMapper}'s requirement to
* convert incoming data into byte arrays. This class can be subclassed to override template methods for specific object
* mapping strategies.
*
* @author Mark Paluch
* @since 2.2
* @see ObjectHashMapper
* @see #doGetHashMapper(ConversionService, Class)
*/
class StreamObjectMapper {
private final DefaultConversionService conversionService = new DefaultConversionService();
private final RedisCustomConversions customConversions = new RedisCustomConversions();
private final HashMapper<Object, Object, Object> mapper;
private final @Nullable HashMapper<Object, Object, Object> objectHashMapper;
/**
* Creates a new {@link StreamObjectMapper}.
*
* @param mapper the configured {@link HashMapper}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
StreamObjectMapper(HashMapper<?, ?, ?> mapper) {
Assert.notNull(mapper, "HashMapper must not be null");
this.customConversions.registerConvertersIn(conversionService);
this.mapper = (HashMapper) mapper;
if (mapper instanceof ObjectHashMapper) {
ObjectHashMapper ohm = (ObjectHashMapper) mapper;
this.objectHashMapper = new HashMapper<Object, Object, Object>() {
@Override
public Map<Object, Object> toHash(Object object) {
return (Map) ohm.toHash(object);
}
@Override
public Object fromHash(Map<Object, Object> hash) {
Map<byte[], byte[]> map = hash.entrySet().stream()
.collect(Collectors.toMap(e -> conversionService.convert(e.getKey(), byte[].class),
e -> conversionService.convert(e.getValue(), byte[].class)));
return ohm.fromHash(map);
}
};
} else {
this.objectHashMapper = null;
}
}
/**
* Convert the given {@link Record} into a {@link MapRecord}.
*
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
* @param source the source value.
* @return the converted {@link MapRecord}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
static <K, V, HK, HV> MapRecord<K, HK, HV> toMapRecord(HashMapperProvider<HK, HV> provider, Record<K, V> source) {
if (source instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) source);
if (entry.getValue() instanceof Map) {
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofMap((Map) entry.getValue());
}
return entry.toMapRecord(provider.getHashMapper(entry.getValue().getClass()));
}
if (source instanceof MapRecord) {
return (MapRecord<K, HK, HV>) source;
}
return Record.of(((HashMapper) provider.getHashMapper(source.getClass())).toHash(source))
.withStreamKey(source.getStream());
}
/**
* Convert the given {@link Record} into an {@link ObjectRecord}.
*
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
* @param source the source value.
* @param targetType the desired target type.
* @return the converted {@link ObjectRecord}.
*/
static <K, V, HK, HV> ObjectRecord<K, V> toObjectRecord(HashMapperProvider<HK, HV> provider,
MapRecord<K, HK, HV> source, Class<V> targetType) {
return source.toObjectRecord(provider.getHashMapper(targetType));
}
/**
* Map a {@link List} of {@link MapRecord}s to a {@link List} of {@link ObjectRecord}. Optimizes for empty,
* single-element and multi-element list transformation.l
*
* @param records the {@link MapRecord} that should be mapped.
* @param hashMapperProvider
* @param targetType the requested {@link Class target type}.
* @return the resulting {@link List} of {@link ObjectRecord} or {@literal null} if {@code records} was
* {@literal null}.
*/
@Nullable
static <K, V, HK, HV> List<ObjectRecord<K, V>> map(@Nullable List<MapRecord<K, HK, HV>> records,
HashMapperProvider<HK, HV> hashMapperProvider, Class<V> targetType) {
if (records == null) {
return null;
}
if (records.isEmpty()) {
return Collections.emptyList();
}
if (records.size() == 1) {
return Collections.singletonList(toObjectRecord(hashMapperProvider, records.get(0), targetType));
}
List<ObjectRecord<K, V>> transformed = new ArrayList<>(records.size());
HashMapper<V, HK, HV> hashMapper = hashMapperProvider.getHashMapper(targetType);
for (MapRecord<K, HK, HV> record : records) {
transformed.add(record.toObjectRecord(hashMapper));
}
return transformed;
}
@SuppressWarnings("unchecked")
final <V, HK, HV> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
return (HashMapper) doGetHashMapper(conversionService, targetType);
}
/**
* Returns the actual {@link HashMapper}. Can be overridden by subclasses.
*
* @param conversionService the used {@link ConversionService}.
* @param targetType the target type.
* @return
*/
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
return this.objectHashMapper != null ? objectHashMapper : this.mapper;
}
/**
* @param targetType
* @return {@literal true} if {@link Class targetType} is a simple type.
* @see org.springframework.data.convert.CustomConversions#isSimpleType(Class)
*/
boolean isSimpleType(Class<?> targetType) {
return customConversions.isSimpleType(targetType);
}
/**
* @return used {@link ConversionService}.
*/
ConversionService getConversionService() {
return this.conversionService;
}
}

View File

@@ -20,21 +20,21 @@ import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.StreamRecords;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Redis stream specific operations.
@@ -43,7 +43,7 @@ import org.springframework.lang.Nullable;
* @author Christoph Strobl
* @since 2.2
*/
public interface StreamOperations<K, HK, HV> {
public interface StreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
/**
* Acknowledge one or more records as processed.
@@ -91,8 +91,9 @@ public interface StreamOperations<K, HK, HV> {
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@SuppressWarnings("unchecked")
@Nullable
default RecordId add(K key, Map<HK, HV> content) {
default RecordId add(K key, Map<? extends HK, ? extends HV> content) {
return add(StreamRecords.newRecord().in(key).ofMap(content));
}
@@ -104,25 +105,29 @@ public interface StreamOperations<K, HK, HV> {
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
*/
@Nullable
RecordId add(MapRecord<K, HK, HV> record);
/**
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
*
* @param record must not be {@literal null}.
* @param <V>
* @return
*/
default <V> RecordId add(Record<K, V> record) {
return add(toMapRecord(record));
@SuppressWarnings("unchecked")
default RecordId add(MapRecord<K, ? extends HK, ? extends HV> record) {
return add((Record) record);
}
/**
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
* number of IDs passed in case certain IDs do not exist.
* Append the record, backed by the given value, to the stream. The value is mapped as hash and serialized.
*
* @param record must not be {@literal null}.
* @return the record Id. {@literal null} when used in pipeline / transaction.
* @see MapRecord
* @see ObjectRecord
*/
@SuppressWarnings("unchecked")
@Nullable
RecordId add(Record<K, ?> record);
/**
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
* the number of IDs passed in case certain IDs do not exist.
*
* @param key the stream key.
* @param recordIds stream record id's.
* @param recordIds stream record Id's.
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
*/
@@ -159,7 +164,7 @@ public interface StreamOperations<K, HK, HV> {
*
* @param key
* @param group name of the consumer group.
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
*/
default String createGroup(K key, String group) {
return createGroup(key, ReadOffset.latest(), group);
@@ -171,7 +176,7 @@ public interface StreamOperations<K, HK, HV> {
* @param key
* @param readOffset
* @param group name of the consumer group.
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
*/
@Nullable
String createGroup(K key, ReadOffset readOffset, String group);
@@ -231,31 +236,34 @@ public interface StreamOperations<K, HK, HV> {
@Nullable
List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
default <V> List<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
return range(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
/**
* Read all records from a stream within a specific {@link Range} as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
return range(targetType, key, range, Limit.unlimited());
}
/**
* Read records from one or more {@link StreamOffset}s.
* Read records from a stream within a specific {@link Range} applying a {@link Limit} as {@link ObjectRecord}.
*
* @param stream the streams to read from.
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
*/
@Nullable
default List<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
}
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
return read(targetType, StreamReadOptions.empty(), streams);
Assert.notNull(targetType, "Target type must not be null");
return StreamObjectMapper.map(range(key, range, limit), this, targetType);
}
/**
@@ -271,16 +279,15 @@ public interface StreamOperations<K, HK, HV> {
}
/**
* Read records from one or more {@link StreamOffset}s.
*
* @param readOptions read arguments.
* @param stream the streams to read from.
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param streams the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
*/
@Nullable
default List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(readOptions, new StreamOffset[] { stream });
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
return read(targetType, StreamReadOptions.empty(), streams);
}
/**
@@ -295,7 +302,7 @@ public interface StreamOperations<K, HK, HV> {
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read records from one or more {@link StreamOffset}s.
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param readOptions read arguments.
@@ -306,20 +313,10 @@ public interface StreamOperations<K, HK, HV> {
@Nullable
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(readOptions, streams).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K> stream) {
return read(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
Assert.notNull(targetType, "Target type must not be null");
return StreamObjectMapper.map(read(readOptions, streams), this, targetType);
}
/**
@@ -336,7 +333,7 @@ public interface StreamOperations<K, HK, HV> {
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param consumer consumer/group.
@@ -349,20 +346,6 @@ public interface StreamOperations<K, HK, HV> {
return read(targetType, consumer, StreamReadOptions.empty(), streams);
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
* @param consumer consumer/group.
* @param readOptions read arguments.
* @param stream the streams to read from.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
*/
@Nullable
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
return read(consumer, readOptions, new StreamOffset[] { stream });
}
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
*
@@ -376,7 +359,7 @@ public interface StreamOperations<K, HK, HV> {
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
/**
* Read records from one or more {@link StreamOffset}s using a consumer group.
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param consumer consumer/group.
@@ -388,8 +371,10 @@ public interface StreamOperations<K, HK, HV> {
@Nullable
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
StreamOffset<K>... streams) {
return read(consumer, readOptions, streams).stream().map(it -> toObjectRecord(it, targetType))
.collect(Collectors.toList());
Assert.notNull(targetType, "Target type must not be null");
return StreamObjectMapper.map(read(consumer, readOptions, streams), this, targetType);
}
/**
@@ -417,8 +402,35 @@ public interface StreamOperations<K, HK, HV> {
@Nullable
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
default <V> List<ObjectRecord<K, V>> reverseRange(K key, Range<String> range, Class<V> targetType) {
return reverseRange(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
/**
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
return reverseRange(targetType, key, range, Limit.unlimited());
}
/**
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
* {@link ObjectRecord}.
*
* @param targetType the target type of the payload.
* @param key the stream key.
* @param range must not be {@literal null}.
* @param limit must not be {@literal null}.
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
*/
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
Assert.notNull(targetType, "Target type must not be null");
return StreamObjectMapper.map(reverseRange(key, range, limit), this, targetType);
}
/**
@@ -439,31 +451,7 @@ public interface StreamOperations<K, HK, HV> {
* @param <V>
* @return the {@link HashMapper} suitable for a given type;
*/
@Override
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
/**
* App
*
* @param value
* @param <V>
* @return
*/
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
if (value instanceof ObjectRecord) {
ObjectRecord entry = ((ObjectRecord) value);
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
}
if (value instanceof MapRecord) {
return (MapRecord<K, HK, HV>) value;
}
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
}
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
return entry.toObjectRecord(getHashMapper(targetType));
}
}

View File

@@ -39,7 +39,7 @@ class DefaultRedisElementWriter<T> implements RedisElementWriter<T> {
@Override
public ByteBuffer write(T value) {
if (serializer != null) {
if (serializer != null && (value == null || serializer.canSerialize(value.getClass()))) {
return ByteBuffer.wrap(serializer.serialize(value));
}

View File

@@ -22,15 +22,17 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.util.Assert;
@@ -45,7 +47,7 @@ import org.springframework.util.ErrorHandler;
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListenerContainer<K, V> {
class DefaultStreamMessageListenerContainer<K, V extends Record<K, ?>> implements StreamMessageListenerContainer<K, V> {
private final Object lifecycleMonitor = new Object();
@@ -53,6 +55,8 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
private final ErrorHandler errorHandler;
private final StreamReadOptions readOptions;
private final RedisTemplate<K, ?> template;
private final StreamOperations<K, Object, Object> streamOperations;
private final StreamMessageListenerContainerOptions<K, V> containerOptions;
private final List<Subscription> subscriptions = new ArrayList<>();
@@ -74,6 +78,13 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
this.errorHandler = containerOptions.getErrorHandler();
this.readOptions = getStreamReadOptions(containerOptions);
this.template = createRedisTemplate(connectionFactory, containerOptions);
this.containerOptions = containerOptions;
if (containerOptions.getHashMapper() != null) {
this.streamOperations = this.template.opsForStream(containerOptions.getHashMapper());
} else {
this.streamOperations = this.template.opsForStream();
}
}
private static StreamReadOptions getStreamReadOptions(StreamMessageListenerContainerOptions<?, ?> options) {
@@ -92,9 +103,9 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
RedisTemplate<K, V> template = new RedisTemplate<>();
template.setKeySerializer(containerOptions.getKeySerializer());
template.setValueSerializer(containerOptions.getBodySerializer());
template.setHashKeySerializer(containerOptions.getKeySerializer());
template.setHashValueSerializer(containerOptions.getBodySerializer());
template.setValueSerializer(containerOptions.getKeySerializer());
template.setHashKeySerializer(containerOptions.getHashKeySerializer());
template.setHashValueSerializer(containerOptions.getHashValueSerializer());
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
@@ -193,9 +204,16 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
return doRegister(getReadTask(streamRequest, listener));
}
@SuppressWarnings("unchecked")
private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
StreamOperations<K, ?, ?> streamOperations = template.opsForStream();
BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> readFunction = getReadFunction(streamRequest);
return new StreamPollTask<>(streamRequest, listener, errorHandler, (BiFunction) readFunction);
}
@SuppressWarnings("unchecked")
private BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> getReadFunction(StreamReadRequest<K> streamRequest) {
if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
@@ -204,20 +222,20 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
StreamReadOptions readOptions = consumerStreamRequest.isAutoAck() ? this.readOptions : this.readOptions.noack();
Consumer consumer = consumerStreamRequest.getConsumer();
return new StreamPollTask<K, V>(consumerStreamRequest, listener, errorHandler, (key, offset) -> {
return (List<Record<K, V>>) (List) streamOperations.read(consumer, readOptions,
if (this.containerOptions.getHashMapper() != null) {
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), consumer, readOptions,
StreamOffset.create(key, offset));
// List<StreamMessage<K, V>> x = (List<StreamMessage<K, V>>)(List) streamOperations.read(consumer, readOptions,
// StreamOffset.create(key, offset));
// return x;
}
});
return (key, offset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, offset));
}
return new StreamPollTask<>(streamRequest, listener, errorHandler, (key, offset) -> {
return (List<Record<K, V>>) (List) streamOperations.read(readOptions, StreamOffset.create(key, offset));
});
if (this.containerOptions.getHashMapper() != null) {
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), readOptions,
StreamOffset.create(key, offset));
}
return (key, offset) -> streamOperations.read(readOptions, StreamOffset.create(key, offset));
}
private Subscription doRegister(Task task) {

View File

@@ -34,14 +34,14 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscription;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveStreamOperations;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
/**
* Default implementation of {@link StreamReceiver}.
@@ -49,11 +49,13 @@ import org.springframework.data.redis.serializer.RedisSerializationContext.Seria
* @author Mark Paluch
* @since 2.2
*/
class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
class DefaultStreamReceiver<K, V extends Record<K, ?>> implements StreamReceiver<K, V> {
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveRedisTemplate<K, ?> template;
private final ReactiveStreamOperations<K, Object, Object> streamOperations;
private final StreamReadOptions readOptions;
private final StreamReceiverOptions<K, V> receiverOptions;
/**
* Create a new {@link DefaultStreamReceiver} given {@link ReactiveRedisConnectionFactory} and
@@ -62,13 +64,14 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
* @param connectionFactory must not be {@literal null}.
* @param options must not be {@literal null}.
*/
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, HK, HV> options) {
@SuppressWarnings("unchecked")
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, V> options) {
receiverOptions = options;
RedisSerializationContext<HK, HV> serializationContext = RedisSerializationContext
.<HK, HV> newSerializationContext(options.getKeySerializer()) //
.key((SerializationPair<HK>) options.getKeySerializer()) //
.value((SerializationPair<HV>) options.getBodySerializer()) //
.build();
RedisSerializationContext<K, Object> serializationContext = RedisSerializationContext
.<K, Object> newSerializationContext(options.getKeySerializer()) //
.key(options.getKeySerializer()).hashKey(options.getHashKeySerializer())
.hashValue(options.getHashValueSerializer()).build();
StreamReadOptions readOptions = StreamReadOptions.empty().count(options.getBatchSize());
if (!options.getPollTimeout().isZero()) {
@@ -77,6 +80,12 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
this.readOptions = readOptions;
this.template = new ReactiveRedisTemplate(connectionFactory, serializationContext);
if (options.getHashMapper() != null) {
this.streamOperations = this.template.opsForStream(options.getHashMapper());
} else {
this.streamOperations = this.template.opsForStream();
}
}
/*
@@ -84,19 +93,27 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset) {
@SuppressWarnings("unchecked")
public Flux<V> receive(StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s)", streamOffset));
}
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction;
if (receiverOptions.getHashMapper() != null) {
readFunction = (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), readOptions,
StreamOffset.create(key, readOffset));
} else {
readFunction = (key, readOffset) -> streamOperations.read(readOptions, StreamOffset.create(key, readOffset));
}
return Flux.defer(() -> {
PollState pollState = PollState.standalone(streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(readOptions, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
return Flux.create(
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
});
}
@@ -105,19 +122,21 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
* @see org.springframework.data.redis.stream.StreamReceiver#receiveAutoAck(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
@SuppressWarnings("unchecked")
public Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset));
}
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
this.readOptions);
return Flux.defer(() -> {
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(consumer, readOptions, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
return Flux.create(
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
});
}
@@ -126,36 +145,47 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
*/
@Override
public Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset) {
@SuppressWarnings("unchecked")
public Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("receive(%s, %s)", consumer, streamOffset));
}
StreamReadOptions noack = readOptions.noack();
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
this.readOptions.noack());
return Flux.defer(() -> {
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
.<HK, HV> opsForStream().read(consumer, noack, StreamOffset.create(key, readOffset));
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
return Flux.create(
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
});
}
@SuppressWarnings("unchecked")
private BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> getConsumeReadFunction(Consumer consumer,
StreamReadOptions readOptions) {
if (receiverOptions.getHashMapper() != null) {
return (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), consumer, readOptions,
StreamOffset.create(key, readOffset));
}
return (key, readOffset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, readOffset));
}
/**
* A stateful Redis Stream subscription.
*/
@RequiredArgsConstructor
class StreamSubscription {
private final Queue<MapRecord<K, HK, HV>> overflow = Queues.<MapRecord<K, HK, HV>> small().get();
private final Queue<V> overflow = Queues.<V> small().get();
private final FluxSink<MapRecord<K, HK, HV>> sink;
private final FluxSink<V> sink;
private final K key;
private final PollState pollState;
private final BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction;
private final BiFunction<K, ReadOffset, Flux<V>> readFunction;
/**
* Arm the subscription so {@link Subscription#request(long) demand} activates polling.
@@ -250,15 +280,15 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset));
}
Flux<MapRecord<K, HK, HV>> poll = readFunction.apply(key, readOffset);
Flux<V> poll = readFunction.apply(key, readOffset);
poll.subscribe(getSubscriber());
}
}
private CoreSubscriber<MapRecord<K, HK, HV>> getSubscriber() {
private CoreSubscriber<V> getSubscriber() {
return new CoreSubscriber<MapRecord<K, HK, HV>>() {
return new CoreSubscriber<V>() {
@Override
public void onSubscribe(Subscription s) {
@@ -266,7 +296,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
}
@Override
public void onNext(MapRecord<K, HK, HV> message) {
public void onNext(V message) {
onStreamMessage(message);
}
@@ -294,7 +324,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
};
}
private void onStreamMessage(MapRecord<K, HK, HV> message) {
private void onStreamMessage(V message) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, message));
@@ -359,7 +389,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
if (demand == Long.MAX_VALUE) {
MapRecord<K, HK, HV> message = overflow.poll();
V message = overflow.poll();
if (message == null) {
if (logger.isDebugEnabled()) {
@@ -377,7 +407,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
} else if (pollState.setRequested(demand, demand - 1)) {
MapRecord<K, HK, HV> message = overflow.poll();
V message = overflow.poll();
if (message == null) {

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.Nullable;
/**
* @author Mark Paluch
*/
enum RawRedisSerializer implements RedisSerializer<byte[]> {
INSTANCE;
@Nullable
@Override
public byte[] serialize(@Nullable byte[] bytes) throws SerializationException {
return bytes;
}
@Nullable
@Override
public byte[] deserialize(@Nullable byte[] bytes) throws SerializationException {
return bytes;
}
}

View File

@@ -17,8 +17,8 @@ package org.springframework.data.redis.stream;
import java.util.Optional;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
/**
* Strategy to determine the first and subsequent {@link ReadOffset}.

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.stream.Record;
/**
* Listener interface to receive delivery of {@link Record messages}.
@@ -26,12 +26,12 @@ import org.springframework.data.redis.connection.RedisStreamCommands.Record;
* @since 2.2
*/
@FunctionalInterface
public interface StreamListener<K, V> {
public interface StreamListener<K, V extends Record<K, ?>> {
/**
* Callback invoked on receiving a {@link Record}.
*
* @param message never {@literal null}.
*/
void onMessage(Record<K, V> message);
void onMessage(V message);
}

View File

@@ -15,20 +15,21 @@
*/
package org.springframework.data.redis.stream;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.LoggingErrorHandler;
@@ -41,9 +42,9 @@ import org.springframework.util.ErrorHandler;
* implemented externally.
* <p/>
* Once created, a {@link StreamMessageListenerContainer} can subscribe to a Redis Stream and consume incoming
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and
* returns a {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually
* background polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and returns a
* {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually background
* polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
* serialization strategies. <br/>
* {@link StreamMessageListenerContainer} supports multiple modes of stream consumption:
* <ul>
@@ -83,17 +84,17 @@ import org.springframework.util.ErrorHandler;
* {@link StreamListener#onMessage(Record) listener callback}.
* <p/>
* {@link StreamMessageListenerContainer} tasks propagate errors during stream reads and
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors
* stop a {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows
* conditional subscription cancelling or continuing on all errors.
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors stop a
* {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows conditional
* subscription cancelling or continuing on all errors.
* <p/>
* See the following example code how to use {@link StreamMessageListenerContainer}:
*
* <pre class="code">
* RedisConnectionFactory factory = …;
*
* StreamMessageListenerContainer<String, String> container = StreamMessageListenerContainer.create(factory);
* Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), message -> …);
* StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(factory);
* Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), message -> …);
*
* container.start();
*
@@ -115,7 +116,7 @@ import org.springframework.util.ErrorHandler;
* @see RedisConnectionFactory
* @see StreamReceiver
*/
public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
public interface StreamMessageListenerContainer<K, V extends Record<K, ?>> extends SmartLifecycle {
/**
* Create a new {@link StreamMessageListenerContainer} using {@link StringRedisSerializer string serializers} given
@@ -124,7 +125,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static StreamMessageListenerContainer<String, Map<String, String>> create(RedisConnectionFactory connectionFactory) {
static StreamMessageListenerContainer<String, MapRecord<String, String, String>> create(
RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
@@ -140,7 +142,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param options must not be {@literal null}.
* @return the new {@link StreamMessageListenerContainer}.
*/
static <K, V> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory,
static <K, V extends Record<K, ?>> StreamMessageListenerContainer<K, V> create(
RedisConnectionFactory connectionFactory,
StreamMessageListenerContainerOptions<K, V> options) {
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
@@ -468,20 +471,38 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param <V> Stream value type.
* @see StreamMessageListenerContainerOptionsBuilder
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class StreamMessageListenerContainerOptions<K, V> {
class StreamMessageListenerContainerOptions<K, V extends Record<K, ?>> {
private final Duration pollTimeout;
private final int batchSize;
private final RedisSerializer<K> keySerializer;
private final RedisSerializer<V> bodySerializer;
private final RedisSerializer<Object> hashKeySerializer;
private final RedisSerializer<Object> hashValueSerializer;
private final @Nullable Class<Object> targetType;
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
private final ErrorHandler errorHandler;
private final Executor executor;
@SuppressWarnings("unchecked")
private StreamMessageListenerContainerOptions(Duration pollTimeout, int batchSize, RedisSerializer<K> keySerializer,
RedisSerializer<Object> hashKeySerializer, RedisSerializer<Object> hashValueSerializer,
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper, ErrorHandler errorHandler,
Executor executor) {
this.pollTimeout = pollTimeout;
this.batchSize = batchSize;
this.keySerializer = keySerializer;
this.hashKeySerializer = hashKeySerializer;
this.hashValueSerializer = hashValueSerializer;
this.targetType = (Class) targetType;
this.hashMapper = (HashMapper) hashMapper;
this.errorHandler = errorHandler;
this.executor = executor;
}
/**
* @return a new builder for {@link StreamMessageListenerContainerOptions}.
*/
static StreamMessageListenerContainerOptionsBuilder<String, Map<String, String>> builder() {
static StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, String>> builder() {
return new StreamMessageListenerContainerOptionsBuilder<>().serializer(StringRedisSerializer.UTF_8);
}
@@ -507,8 +528,26 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
return keySerializer;
}
public RedisSerializer<V> getBodySerializer() {
return bodySerializer;
public RedisSerializer<Object> getHashKeySerializer() {
return hashKeySerializer;
}
public RedisSerializer<Object> getHashValueSerializer() {
return hashValueSerializer;
}
@Nullable
public HashMapper<Object, Object, Object> getHashMapper() {
return hashMapper;
}
public Class<Object> getTargetType() {
if (this.targetType != null) {
return targetType;
}
return Object.class;
}
/**
@@ -532,12 +571,16 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param <K> Stream key and Stream field type
* @param <V> Stream value type
*/
class StreamMessageListenerContainerOptionsBuilder<K, V> {
@SuppressWarnings("unchecked")
class StreamMessageListenerContainerOptionsBuilder<K, V extends Record<K, ?>> {
private Duration pollTimeout = Duration.ofSeconds(2);
private int batchSize = 1;
private RedisSerializer<K> keySerializer;
private RedisSerializer<V> bodySerializer;
private RedisSerializer<Object> hashKeySerializer;
private RedisSerializer<Object> hashValueSerializer;
private @Nullable HashMapper<V, ?, ?> hashMapper;
private @Nullable Class<?> targetType;
private ErrorHandler errorHandler = LoggingErrorHandler.INSTANCE;
private Executor executor = new SimpleAsyncTaskExecutor();
@@ -601,15 +644,19 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
}
/**
* Configure a key and value serializer.
* Configure a key, hash key and hash value serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <T> StreamMessageListenerContainerOptionsBuilder<T, Map<T, T>> serializer(RedisSerializer<T> serializer) {
public <T> StreamMessageListenerContainerOptionsBuilder<T, MapRecord<T, T, T>> serializer(
RedisSerializer<T> serializer) {
Assert.notNull(serializer, "RedisSerializer must not be null");
this.keySerializer = (RedisSerializer) serializer;
this.bodySerializer = (RedisSerializer) serializer;
this.hashKeySerializer = (RedisSerializer) serializer;
this.hashValueSerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
@@ -619,21 +666,81 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <NK> StreamMessageListenerContainerOptionsBuilder<NK, V> keySerializer(RedisSerializer<NK> serializer) {
public <NK, NV extends Record<NK, ?>> StreamMessageListenerContainerOptionsBuilder<NK, NV> keySerializer(
RedisSerializer<NK> serializer) {
Assert.notNull(serializer, "RedisSerializer must not be null");
this.keySerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a value serializer.
* Configure a hash key serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <NV> StreamMessageListenerContainerOptionsBuilder<K, NV> bodySerializer(RedisSerializer<NV> serializer) {
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
RedisSerializer<HK> serializer) {
this.bodySerializer = (RedisSerializer) serializer;
Assert.notNull(serializer, "RedisSerializer must not be null");
this.hashKeySerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a hash value serializer.
*
* @param serializer must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
RedisSerializer<HK> serializer) {
Assert.notNull(serializer, "RedisSerializer must not be null");
this.hashValueSerializer = (RedisSerializer) serializer;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
@SuppressWarnings("unchecked")
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
Assert.notNull(targetType, "Target type must not be null");
this.targetType = targetType;
if (this.hashMapper == null) {
hashKeySerializer(RawRedisSerializer.INSTANCE);
hashValueSerializer(RawRedisSerializer.INSTANCE);
return (StreamMessageListenerContainerOptionsBuilder) objectMapper(new ObjectHashMapper());
}
return (StreamMessageListenerContainerOptionsBuilder) this;
}
/**
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
*
* @param hashMapper must not be {@literal null}.
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
*/
@SuppressWarnings("unchecked")
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(
HashMapper<NV, ?, ?> hashMapper) {
Assert.notNull(hashMapper, "HashMapper must not be null");
this.hashMapper = (HashMapper) hashMapper;
return (StreamMessageListenerContainerOptionsBuilder) this;
}
@@ -643,7 +750,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
* @return new {@link StreamMessageListenerContainerOptions}.
*/
public StreamMessageListenerContainerOptions<K, V> build() {
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer,
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer,
hashValueSerializer, targetType, hashMapper,
errorHandler, executor);
}
}

View File

@@ -24,10 +24,10 @@ import java.util.function.BiFunction;
import java.util.function.Predicate;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.ConsumerStreamReadRequest;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
import org.springframework.util.ErrorHandler;
@@ -38,19 +38,19 @@ import org.springframework.util.ErrorHandler;
* @author Mark Paluch
* @see 2.2
*/
class StreamPollTask<K, V> implements Task {
class StreamPollTask<K, V extends Record<K, ?>> implements Task {
private final StreamReadRequest<K> request;
private final StreamListener<K, V> listener;
private final ErrorHandler errorHandler;
private final Predicate<Throwable> cancelSubscriptionOnError;
private final BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction;
private final BiFunction<K, ReadOffset, List<V>> readFunction;
private final PollState pollState;
private volatile boolean isInEventLoop = false;
StreamPollTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener, ErrorHandler errorHandler,
BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction) {
BiFunction<K, ReadOffset, List<V>> readFunction) {
this.request = streamRequest;
this.listener = listener;
@@ -135,9 +135,9 @@ class StreamPollTask<K, V> implements Task {
// allow interruption
Thread.sleep(0);
List<Record<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
List<V> read = readFunction.apply(key, pollState.getCurrentReadOffset());
for (Record<K, V> message : read) {
for (V message : read) {
listener.onMessage(message);
pollState.updateReadOffset(message.getId().getValue());

View File

@@ -15,27 +15,33 @@
*/
package org.springframework.data.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
import java.time.Duration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.ObjectHashMapper;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A receiver to consume Redis Streams using reactive infrastructure.
* <p/>
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link org.springframework.data.redis.connection.RedisStreamCommands.Record
* messages}. Consider a {@link Flux} of {@link Record} infinite. Cancelling the
* {@link org.reactivestreams.Subscription} terminates eventually background polling. Messages are converted using
* {@link SerializationPair key and value serializers} to support various serialization strategies. <br/>
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link Record records}.
* Consider a {@link Flux} of {@link Record} infinite. Cancelling the {@link org.reactivestreams.Subscription}
* terminates eventually background polling. Records are converted using {@link SerializationPair key and value
* serializers} to support various serialization strategies. <br/>
* {@link StreamReceiver} supports three modes of stream consumption:
* <ul>
* <li>Standalone</li>
@@ -49,36 +55,36 @@ import org.springframework.util.Assert;
* <br/>
* <strong>Standalone</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
* seen {@link Record#getId() record Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
* {@link Record#getId() message Id}.</li>
* {@link Record#getId() record Id}.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
* ({@code $}) for subsequent reads.</li>
* </ul>
* <br/>
* <strong>Using {@link Consumer}</strong>
* <ul>
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
* last seen {@link Record#getId() message Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed message by the consumer ({@code >})
* and use the last consumed message by the consumer ({@code >}) for subsequent reads.</li>
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
* seen {@link Record#getId() record Id}.</li>
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed record by the consumer ({@code >})
* and use the last consumed record by the consumer ({@code >}) for subsequent reads.</li>
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
* ({@code $}) for subsequent reads.</li>
* </ul>
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped messages as messages can arrive in the
* time during polling is suspended. Use messagedId's as offset or {@link ReadOffset#lastConsumed()} to minimize the
* chance of message loss.</strong>
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped records as records can arrive in the time
* during polling is suspended. Use recorddId's as offset or {@link ReadOffset#lastConsumed()} to minimize the chance of
* record loss.</strong>
* <p/>
* See the following example code how to use {@link StreamReceiver}:
*
* <pre class="code">
* ReactiveRedisConnectionFactory factory = …;
*
* StreamReceiver<String, String> receiver = StreamReceiver.create(factory);
* Flux<StreamMessage<String, String>> messages = receiver.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")));
* StreamReceiver<String, String, String> receiver = StreamReceiver.create(factory);
* Flux<MapRecord<String, String, String>> records = receiver.receive(StreamOffset.fromStart("my-stream"));
*
* messageFlux.doOnNext(message -> …);
* recordFlux.doOnNext(record -> …);
* </pre>
*
* @author Mark Paluch
@@ -90,7 +96,7 @@ import org.springframework.util.Assert;
* @see ReactiveRedisConnectionFactory
* @see StreamMessageListenerContainer
*/
public interface StreamReceiver<K, HK, HV> {
public interface StreamReceiver<K, V extends Record<K, ?>> {
/**
* Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given
@@ -99,7 +105,8 @@ public interface StreamReceiver<K, HK, HV> {
* @param connectionFactory must not be {@literal null}.
* @return the new {@link StreamReceiver}.
*/
static StreamReceiver<String, String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
static StreamReceiver<String, MapRecord<String, String, String>> create(
ReactiveRedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
@@ -114,8 +121,8 @@ public interface StreamReceiver<K, HK, HV> {
* @param options must not be {@literal null}.
* @return the new {@link StreamReceiver}.
*/
static <K, HK, HV> StreamReceiver<K, HK, HV> create(ReactiveRedisConnectionFactory connectionFactory,
StreamReceiverOptions<K, HK, HV> options) {
static <K, V extends Record<K, ?>> StreamReceiver<K, V> create(ReactiveRedisConnectionFactory connectionFactory,
StreamReceiverOptions<K, V> options) {
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
Assert.notNull(options, "StreamReceiverOptions must not be null!");
@@ -124,25 +131,25 @@ public interface StreamReceiver<K, HK, HV> {
}
/**
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
* is closed when the returned {@link Flux} terminates.
* <p/>
* Every message must be acknowledged using
* Every record must be acknowledged using
* {@link org.springframework.data.redis.connection.ReactiveStreamCommands#xAck(ByteBuffer, String, String...)}
*
* @param streamOffset the stream along its offset.
* @return Flux of inbound {@link Record}s.
* @see StreamOffset#create(Object, ReadOffset)
*/
Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset);
Flux<V> receive(StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
* is closed when the returned {@link Flux} terminates.
* <p/>
* Every message is acknowledged when received.
* Every record is acknowledged when received.
*
* @param consumer consumer group, must not be {@literal null}.
* @param streamOffset the stream along its offset.
@@ -150,14 +157,14 @@ public interface StreamReceiver<K, HK, HV> {
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
* receiver is closed when the returned {@link Flux} terminates.
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
* is closed when the returned {@link Flux} terminates.
* <p/>
* Every message must be acknowledged using
* Every record must be acknowledged using
* {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)} after
* processing.
*
@@ -167,7 +174,7 @@ public interface StreamReceiver<K, HK, HV> {
* @see StreamOffset#create(Object, ReadOffset)
* @see ReadOffset#lastConsumed()
*/
Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset);
Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset);
/**
* Options for {@link StreamReceiver}.
@@ -176,32 +183,51 @@ public interface StreamReceiver<K, HK, HV> {
* @param <V> Stream value type.
* @see StreamReceiverOptionsBuilder
*/
class StreamReceiverOptions<K, HK, HV> {
class StreamReceiverOptions<K, V extends Record<K, ?>> {
private final Duration pollTimeout;
private final int batchSize;
private final SerializationPair<K> keySerializer;
private final SerializationPair<HK> bodySerializer;
private final SerializationPair<HV> vaueSerializer;
private final SerializationPair<Object> hashKeySerializer;
private final SerializationPair<Object> hashValueSerializer;
private final @Nullable Class<Object> targetType;
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
private StreamReceiverOptions(Duration pollTimeout, int batchSize, SerializationPair<K> keySerializer,
SerializationPair<HK> bodySerializer, SerializationPair<HV> valueSerializer) {
SerializationPair<Object> hashKeySerializer, SerializationPair<Object> hashValueSerializer,
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper) {
this.pollTimeout = pollTimeout;
this.batchSize = batchSize;
this.keySerializer = keySerializer;
this.bodySerializer = bodySerializer;
this.vaueSerializer = valueSerializer;
this.hashKeySerializer = hashKeySerializer;
this.hashValueSerializer = hashValueSerializer;
this.targetType = (Class) targetType;
this.hashMapper = (HashMapper) hashMapper;
}
/**
* @return a new builder for {@link StreamReceiverOptions}.
*/
static StreamReceiverOptionsBuilder<String, String, String> builder() {
public static StreamReceiverOptionsBuilder<String, MapRecord<String, String, String>> builder() {
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
return new StreamReceiverOptionsBuilder<>().serializer(serializer);
}
/**
* @return a new builder for {@link StreamReceiverOptions}.
*/
@SuppressWarnings("unchecked")
public static <T> StreamReceiverOptionsBuilder<String, ObjectRecord<String, T>> builder(
HashMapper<T, byte[], byte[]> hashMapper) {
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
SerializationPair<ByteBuffer> raw = SerializationPair.raw();
return new StreamReceiverOptionsBuilder<>().keySerializer(serializer).hashKeySerializer(raw)
.hashValueSerializer(raw).objectMapper(hashMapper);
}
/**
* Timeout for blocking polling using the {@code BLOCK} option during reads.
*
@@ -224,8 +250,26 @@ public interface StreamReceiver<K, HK, HV> {
return keySerializer;
}
public SerializationPair<HK> getBodySerializer() {
return bodySerializer;
public SerializationPair<Object> getHashKeySerializer() {
return hashKeySerializer;
}
public SerializationPair<Object> getHashValueSerializer() {
return hashValueSerializer;
}
@Nullable
public HashMapper<Object, Object, Object> getHashMapper() {
return hashMapper;
}
public Class<Object> getTargetType() {
if (this.targetType != null) {
return targetType;
}
return Object.class;
}
}
@@ -234,13 +278,15 @@ public interface StreamReceiver<K, HK, HV> {
*
* @param <K> Stream key and Stream field type.
*/
class StreamReceiverOptionsBuilder<K, HK, HV> {
class StreamReceiverOptionsBuilder<K, V extends Record<K, ?>> {
private Duration pollTimeout = Duration.ofSeconds(2);
private int batchSize = 1;
private SerializationPair<K> keySerializer;
private SerializationPair<HK> bodySerializer;
private SerializationPair<HV> valueSerializer;
private SerializationPair<Object> hashKeySerializer;
private SerializationPair<Object> hashValueSerializer;
private @Nullable HashMapper<V, ?, ?> hashMapper;
private @Nullable Class<?> targetType;
private StreamReceiverOptionsBuilder() {}
@@ -250,7 +296,7 @@ public interface StreamReceiver<K, HK, HV> {
* @param pollTimeout must not be {@literal null} or negative.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, HK, HV> pollTimeout(Duration pollTimeout) {
public StreamReceiverOptionsBuilder<K, V> pollTimeout(Duration pollTimeout) {
Assert.notNull(pollTimeout, "Poll timeout must not be null!");
Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!");
@@ -262,28 +308,48 @@ public interface StreamReceiver<K, HK, HV> {
/**
* Configure a batch size for the {@code COUNT} option during reading.
*
* @param messagesPerPoll must not be greater zero.
* @param recordsPerPoll must not be greater zero.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public StreamReceiverOptionsBuilder<K, HK, HV> batchSize(int messagesPerPoll) {
public StreamReceiverOptionsBuilder<K, V> batchSize(int recordsPerPoll) {
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
Assert.isTrue(recordsPerPoll > 0, "Batch size must be greater zero!");
this.batchSize = messagesPerPoll;
this.batchSize = recordsPerPoll;
return this;
}
/**
* Configure a key and value serializer.
* Configure a key, hash key and hash value serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <T> StreamReceiverOptionsBuilder<T, T, T> serializer(SerializationPair<T> pair) {
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(SerializationPair<T> pair) {
Assert.notNull(pair, "SerializationPair must not be null");
this.keySerializer = (SerializationPair) pair;
this.bodySerializer = (SerializationPair) pair;
this.valueSerializer = (SerializationPair) pair;
this.hashKeySerializer = (SerializationPair) pair;
this.hashValueSerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a key, hash key and hash value serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(
RedisSerializationContext<T, ?> serializationContext) {
Assert.notNull(serializationContext, "RedisSerializationContext must not be null");
this.keySerializer = (SerializationPair) serializationContext.getKeySerializationPair();
this.hashKeySerializer = serializationContext.getHashKeySerializationPair();
this.hashValueSerializer = serializationContext.getHashValueSerializationPair();
return (StreamReceiverOptionsBuilder) this;
}
@@ -293,21 +359,80 @@ public interface StreamReceiver<K, HK, HV> {
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NK> StreamReceiverOptionsBuilder<NK, HK, HV> keySerializer(SerializationPair<NK> pair) {
public <NK, NV extends Record<NK, ?>> StreamReceiverOptionsBuilder<NK, NV> keySerializer(
SerializationPair<NK> pair) {
Assert.notNull(pair, "SerializationPair must not be null");
this.keySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a value serializer.
* Configure a hash key serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <NV> StreamReceiverOptionsBuilder<K, HK, HV> bodySerializer(SerializationPair<NV> pair) {
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
SerializationPair<HK> pair) {
this.bodySerializer = (SerializationPair) pair;
Assert.notNull(pair, "SerializationPair must not be null");
this.hashKeySerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a hash value serializer.
*
* @param pair must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
SerializationPair<HK> pair) {
Assert.notNull(pair, "SerializationPair must not be null");
this.hashValueSerializer = (SerializationPair) pair;
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
*
* @param targetType must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
@SuppressWarnings("unchecked")
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
Assert.notNull(targetType, "Target type must not be null");
this.targetType = targetType;
if (this.hashMapper == null) {
hashKeySerializer(SerializationPair.raw());
hashValueSerializer(SerializationPair.raw());
return (StreamReceiverOptionsBuilder) objectMapper(new ObjectHashMapper());
}
return (StreamReceiverOptionsBuilder) this;
}
/**
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
*
* @param hashMapper must not be {@literal null}.
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
*/
@SuppressWarnings("unchecked")
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(HashMapper<NV, ?, ?> hashMapper) {
Assert.notNull(hashMapper, "HashMapper must not be null");
this.hashMapper = (HashMapper) hashMapper;
return (StreamReceiverOptionsBuilder) this;
}
@@ -316,8 +441,9 @@ public interface StreamReceiver<K, HK, HV> {
*
* @return new {@link StreamReceiverOptions}.
*/
public StreamReceiverOptions<K, HK, HV> build() {
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer, valueSerializer);
public StreamReceiverOptions<K, V> build() {
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer, hashValueSerializer,
targetType, hashMapper);
}
}
}