/*
* Copyright 2017 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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.reactivestreams.Publisher;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Central abstraction for reactive Redis data access implementing {@link ReactiveRedisOperations}.
* <p/>
* Performs automatic serialization/deserialization between the given objects and the underlying binary data in the
* Redis store.
* <p/>
* Note that while the template is generified, it is up to the serializers/deserializers to properly convert the given
* Objects to and from binary data.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
* @param <K> the Redis key type against which the template works (usually a String)
* @param <V> the Redis value type against which the template works
*/
public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V> {
private final ReactiveRedisConnectionFactory connectionFactory;
private final RedisSerializationContext<K, V> serializationContext;
private final boolean exposeConnection;
/**
* Creates new {@link ReactiveRedisTemplate} using given {@link ReactiveRedisConnectionFactory} and
* {@link RedisSerializationContext}.
*
* @param connectionFactory must not be {@literal null}.
* @param serializationContext must not be {@literal null}.
*/
public ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory,
RedisSerializationContext<K, V> serializationContext) {
this(connectionFactory, serializationContext, false);
}
/**
* Creates new {@link ReactiveRedisTemplate} using given {@link ReactiveRedisConnectionFactory} and
* {@link RedisSerializationContext}.
*
* @param connectionFactory must not be {@literal null}.
* @param serializationContext must not be {@literal null}.
* @param exposeConnection flag indicating to expose the connection used.
*/
public ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory,
RedisSerializationContext<K, V> serializationContext, boolean exposeConnection) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
Assert.notNull(serializationContext, "SerializationContext must not be null!");
this.connectionFactory = connectionFactory;
this.serializationContext = serializationContext;
this.exposeConnection = exposeConnection;
}
/**
* Returns the connectionFactory.
*
* @return Returns the connectionFactory
*/
public ReactiveRedisConnectionFactory getConnectionFactory() {
return connectionFactory;
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForValue()
*/
@Override
public ReactiveValueOperations<K, V> opsForValue() {
return opsForValue(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForValue(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveValueOperations<K1, V1> opsForValue(RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveValueOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForList()
*/
@Override
public ReactiveListOperations<K, V> opsForList() {
return opsForList(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForList(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveListOperations<K1, V1> opsForList(RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveListOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForSet()
*/
public ReactiveSetOperations<K, V> opsForSet() {
return opsForSet(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForSet(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveSetOperations<K1, V1> opsForSet(RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveSetOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForZSet()
*/
public ReactiveZSetOperations<K, V> opsForZSet() {
return opsForZSet(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForZSet(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveZSetOperations<K1, V1> opsForZSet(RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveZSetOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHyperLogLog()
*/
@Override
public ReactiveHyperLogLogOperations<K, V> opsForHyperLogLog() {
return opsForHyperLogLog(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHyperLogLog(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveHyperLogLogOperations<K1, V1> opsForHyperLogLog(
RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveHyperLogLogOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHash()
*/
@Override
public <HK, HV> ReactiveHashOperations<K, HK, HV> opsForHash() {
return opsForHash(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHash(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, HK, HV> ReactiveHashOperations<K1, HK, HV> opsForHash(
RedisSerializationContext<K1, ?> serializationContext) {
return new DefaultReactiveHashOperations<>(this, serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForGeo()
*/
@Override
public ReactiveGeoOperations<K, V> opsForGeo() {
return opsForGeo(serializationContext);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForGeo(org.springframework.data.redis.serializer.RedisSerializationContext)
*/
@Override
public <K1, V1> ReactiveGeoOperations<K1, V1> opsForGeo(RedisSerializationContext<K1, V1> serializationContext) {
return new DefaultReactiveGeoOperations<>(this, serializationContext);
}
// -------------------------------------------------------------------------
// Execution methods
// -------------------------------------------------------------------------
public <T> Flux<T> execute(ReactiveRedisCallback<T> action) {
return execute(action, exposeConnection);
}
/**
* Executes the given action object within a connection that can be exposed or not. Additionally, the connection can
* be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios).
*
* @param <T> return type
* @param action callback object to execute
* @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code
* @return object returned by the action
*/
public <T> Flux<T> execute(ReactiveRedisCallback<T> action, boolean exposeConnection) {
Assert.notNull(action, "Callback object must not be null");
ReactiveRedisConnectionFactory factory = getConnectionFactory();
ReactiveRedisConnection conn = factory.getReactiveConnection();
try {
ReactiveRedisConnection connToUse = preProcessConnection(conn, false);
ReactiveRedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
Publisher<T> result = action.doInRedis(connToExpose);
return Flux.from(postProcessResult(result, connToUse, false));
} finally {
conn.close();
}
}
/**
* Create a reusable Flux for a {@link ReactiveRedisCallback}. Callback is executed within a connection context. The
* connection is released outside the callback.
*
* @param callback must not be {@literal null}
* @return a {@link Flux} wrapping the {@link ReactiveRedisCallback}.
*/
public <T> Flux<T> createFlux(ReactiveRedisCallback<T> callback) {
Assert.notNull(callback, "ReactiveRedisCallback must not be null!");
return Flux.defer(() -> doInConnection(callback, exposeConnection));
}
/**
* Create a reusable Mono for a {@link ReactiveRedisCallback}. Callback is executed within a connection context. The
* connection is released outside the callback.
*
* @param callback must not be {@literal null}
* @return a {@link Mono} wrapping the {@link ReactiveRedisCallback}.
*/
public <T> Mono<T> createMono(final ReactiveRedisCallback<T> callback) {
Assert.notNull(callback, "ReactiveRedisCallback must not be null!");
return Mono.defer(() -> Mono.from(doInConnection(callback, exposeConnection)));
}
/**
* Executes the given action object within a connection that can be exposed or not. Additionally, the connection can
* be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios).
*
* @param <T> return type
* @param action callback object to execute
* @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code
* @return object returned by the action
*/
private <T> Publisher<T> doInConnection(ReactiveRedisCallback<T> action, boolean exposeConnection) {
Assert.notNull(action, "Callback object must not be null");
ReactiveRedisConnectionFactory factory = getConnectionFactory();
ReactiveRedisConnection conn = factory.getReactiveConnection();
ReactiveRedisConnection connToUse = preProcessConnection(conn, false);
ReactiveRedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
Publisher<T> result = action.doInRedis(connToExpose);
return Flux.from(postProcessResult(result, connToUse, false)).doAfterTerminate(conn::close);
}
// -------------------------------------------------------------------------
// Methods dealing with Redis keys
// -------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#hasKey(java.lang.Object)
*/
public Mono<Boolean> hasKey(K key) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.keyCommands().exists(rawKey(key)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#type(java.lang.Object)
*/
public Mono<DataType> type(K key) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.keyCommands().type(rawKey(key)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#keys(java.lang.Object)
*/
public Flux<K> keys(K pattern) {
Assert.notNull(pattern, "Pattern must not be null!");
return createFlux(connection -> connection.keyCommands().keys(rawKey(pattern))) //
.flatMap(Flux::fromIterable) //
.map(this::readKey);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#randomKey()
*/
public Mono<K> randomKey() {
return createMono(connection -> connection.keyCommands().randomKey()).map(this::readKey);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#rename(java.lang.Object, java.lang.Object)
*/
public Mono<Boolean> rename(K oldKey, K newKey) {
Assert.notNull(oldKey, "Old key must not be null!");
Assert.notNull(newKey, "New Key must not be null!");
return createMono(connection -> connection.keyCommands().rename(rawKey(oldKey), rawKey(newKey)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#renameIfAbsent(java.lang.Object, java.lang.Object)
*/
public Mono<Boolean> renameIfAbsent(K oldKey, K newKey) {
Assert.notNull(oldKey, "Old key must not be null!");
Assert.notNull(newKey, "New Key must not be null!");
return createMono(connection -> connection.keyCommands().renameNX(rawKey(oldKey), rawKey(newKey)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#delete(java.lang.Object[])
*/
@SafeVarargs
public final Mono<Long> delete(K... keys) {
Assert.notNull(keys, "Keys must not be null!");
Assert.notEmpty(keys, "Keys must not be empty!");
Assert.noNullElements(keys, "Keys must not contain null elements!");
if (keys.length == 1) {
return createMono(connection -> connection.keyCommands().del(rawKey(keys[0])));
}
Mono<List<ByteBuffer>> listOfKeys = Flux.fromArray(keys).map(this::rawKey).collectList();
return createMono(connection -> listOfKeys.flatMap(rawKeys -> connection.keyCommands().mDel(rawKeys)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#delete(org.reactivestreams.Publisher)
*/
public Mono<Long> delete(Publisher<K> keys) {
Assert.notNull(keys, "Keys must not be null!");
return createMono(connection -> connection.keyCommands() //
.del(Flux.from(keys).map(this::rawKey).map(KeyCommand::new)) //
.map(CommandResponse::getOutput));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#expire(java.lang.Object, java.time.Duration)
*/
@Override
public Mono<Boolean> expire(K key, Duration timeout) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(timeout, "Timeout must not be null!");
if (timeout.getNano() == 0) {
return createMono(connection -> connection.keyCommands() //
.expire(rawKey(key), timeout));
}
return createMono(connection -> connection.keyCommands().pExpire(rawKey(key), timeout));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#expireAt(java.lang.Object, java.time.Instant)
*/
@Override
public Mono<Boolean> expireAt(K key, Instant expireAt) {
Assert.notNull(key, "Key must not be null!");
Assert.notNull(expireAt, "Expire at must not be null!");
if (expireAt.getNano() == 0) {
return createMono(connection -> connection.keyCommands() //
.expireAt(rawKey(key), expireAt));
}
return createMono(connection -> connection.keyCommands().pExpireAt(rawKey(key), expireAt));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#persist(java.lang.Object)
*/
@Override
public Mono<Boolean> persist(K key) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.keyCommands().persist(rawKey(key)));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#getExpire(java.lang.Object)
*/
@Override
public Mono<Duration> getExpire(K key) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.keyCommands().pTtl(rawKey(key)).flatMap(expiry -> {
if (expiry == -1) {
return Mono.just(Duration.ZERO);
}
if (expiry == -2) {
return Mono.empty();
}
return Mono.just(Duration.ofMillis(expiry));
}));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#move(java.lang.Object, int)
*/
@Override
public Mono<Boolean> move(K key, int dbIndex) {
Assert.notNull(key, "Key must not be null!");
return createMono(connection -> connection.keyCommands().move(rawKey(key), dbIndex));
}
// -------------------------------------------------------------------------
// Implementation hooks and helper methods
// -------------------------------------------------------------------------
/**
* Processes the connection (before any settings are executed on it). Default implementation returns the connection as
* is.
*
* @param connection must not be {@literal null}.
* @param existingConnection
*/
protected ReactiveRedisConnection preProcessConnection(ReactiveRedisConnection connection,
boolean existingConnection) {
return connection;
}
/**
* Processes the result before returning the {@link Publisher}. Default implementation returns the result as is.
*
* @param result must not be {@literal null}.
* @param connection must not be {@literal null}.
* @param existingConnection
* @return
*/
protected <T> Publisher<T> postProcessResult(Publisher<T> result, ReactiveRedisConnection connection,
boolean existingConnection) {
return result;
}
protected ReactiveRedisConnection createRedisConnectionProxy(ReactiveRedisConnection reactiveRedisConnection) {
Class<?>[] ifcs = ClassUtils.getAllInterfacesForClass(reactiveRedisConnection.getClass(),
getClass().getClassLoader());
return (ReactiveRedisConnection) Proxy.newProxyInstance(reactiveRedisConnection.getClass().getClassLoader(), ifcs,
new CloseSuppressingInvocationHandler(reactiveRedisConnection));
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.ReactiveRedisOperations#serialization()
*/
@Override
public RedisSerializationContext<K, V> getSerializationContext() {
return serializationContext;
}
private ByteBuffer rawKey(K key) {
return getSerializationContext().getKeySerializationPair().getWriter().write(key);
}
private K readKey(ByteBuffer buffer) {
return getSerializationContext().getKeySerializationPair().getReader().read(buffer);
}
}