Add RedisTemplate methods for executing Redis scripts
DATAREDIS-199
This commit is contained in:
@@ -40,6 +40,7 @@ dependencies {
|
||||
exclude module: "commons-logging"
|
||||
}
|
||||
compile "org.springframework:spring-context-support:$springVersion"
|
||||
compile "org.springframework:spring-context:$springVersion"
|
||||
compile "org.springframework:spring-tx:$springVersion"
|
||||
compile("org.springframework:spring-oxm:$springVersion", optional)
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.redis.connection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a data type returned from Redis, currently used to denote the
|
||||
* expected return type of Redis scripting commands
|
||||
@@ -37,5 +39,21 @@ public enum ReturnType {
|
||||
STATUS,
|
||||
|
||||
/** Returned as byte[] **/
|
||||
VALUE
|
||||
VALUE;
|
||||
|
||||
public static ReturnType fromJavaType(Class<?> javaType) {
|
||||
if(javaType == null) {
|
||||
return ReturnType.STATUS;
|
||||
}
|
||||
if(javaType.isAssignableFrom(List.class)) {
|
||||
return ReturnType.MULTI;
|
||||
}
|
||||
if(javaType.isAssignableFrom(Boolean.class)) {
|
||||
return ReturnType.BOOLEAN;
|
||||
}
|
||||
if(javaType.isAssignableFrom(Long.class)) {
|
||||
return ReturnType.INTEGER;
|
||||
}
|
||||
return ReturnType.VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.core.query.SortQuery;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
|
||||
@@ -104,6 +105,40 @@ public interface RedisOperations<K, V> {
|
||||
*/
|
||||
List<Object> executePipelined(final SessionCallback<?> session, final RedisSerializer<?> resultSerializer);
|
||||
|
||||
/**
|
||||
* Executes the given {@link RedisScript}
|
||||
*
|
||||
* @param script
|
||||
* The script to execute
|
||||
* @param keys
|
||||
* Any keys that need to be passed to the script
|
||||
* @param args
|
||||
* Any args that need to be passed to the script
|
||||
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
|
||||
* null, likely indicating a throw-away status reply (i.e. "OK")
|
||||
*/
|
||||
<T> T execute(RedisScript<T> script, List<K> keys, Object... args);
|
||||
|
||||
/**
|
||||
* Executes the given {@link RedisScript}, using the provided {@link RedisSerializer}s to
|
||||
* serialize the script arguments and result.
|
||||
*
|
||||
* @param script
|
||||
* The script to execute
|
||||
* @param argsSerializer
|
||||
* The {@link RedisSerializer} to use for serializing args
|
||||
* @param resultSerializer
|
||||
* The {@link RedisSerializer} to use for serializing the script return value
|
||||
* @param keys
|
||||
* Any keys that need to be passed to the script
|
||||
* @param args
|
||||
* Any args that need to be passed to the script
|
||||
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
|
||||
* null, likely indicating a throw-away status reply (i.e. "OK")
|
||||
*/
|
||||
<T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
|
||||
List<K> keys, Object... args);
|
||||
|
||||
Boolean hasKey(K key);
|
||||
|
||||
void delete(K key);
|
||||
|
||||
@@ -37,6 +37,9 @@ import org.springframework.data.redis.connection.SortParameters;
|
||||
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
|
||||
import org.springframework.data.redis.core.query.QueryUtils;
|
||||
import org.springframework.data.redis.core.query.SortQuery;
|
||||
import org.springframework.data.redis.core.script.DefaultScriptExecutor;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.core.script.ScriptExecutor;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationUtils;
|
||||
@@ -83,6 +86,8 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
private RedisSerializer hashValueSerializer = null;
|
||||
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
|
||||
|
||||
private ScriptExecutor<K> scriptExecutor;
|
||||
|
||||
// cache singleton objects (where possible)
|
||||
private ValueOperations<K, V> valueOps;
|
||||
private ListOperations<K, V> listOps;
|
||||
@@ -124,6 +129,10 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
|
||||
}
|
||||
|
||||
if(scriptExecutor == null) {
|
||||
this.scriptExecutor = new DefaultScriptExecutor<K>(this);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@@ -269,6 +278,15 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
|
||||
return scriptExecutor.execute(script, keys, args);
|
||||
}
|
||||
|
||||
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
|
||||
List<K> keys, Object... args) {
|
||||
return scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args);
|
||||
}
|
||||
|
||||
private Object executeSession(SessionCallback<?> session) {
|
||||
return session.execute(this);
|
||||
}
|
||||
@@ -441,6 +459,14 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
this.stringSerializer = stringSerializer;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scriptExecutor The {@link ScriptExecutor} to use for executing Redis scripts
|
||||
*/
|
||||
public void setScriptExecutor(ScriptExecutor<K> scriptExecutor) {
|
||||
this.scriptExecutor = scriptExecutor;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private byte[] rawKey(Object key) {
|
||||
Assert.notNull(key, "non null key required");
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
import org.springframework.scripting.support.StaticScriptSource;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RedisScript}. Delegates to an underlying {@link ScriptSource} to
|
||||
* retrieve script text and detect if script has been modified (and thus should have SHA1
|
||||
* re-calculated). This class is best used as a Singleton to avoid re-calculation of SHA1 on every
|
||||
* script execution.
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
* @param <T>
|
||||
* The script result type. Should be one of Long, Boolean, List, or deserialized value
|
||||
* type. Can be null if the script returns a throw-away status (i.e "OK")
|
||||
*/
|
||||
public class DefaultRedisScript<T> implements RedisScript<T> {
|
||||
|
||||
private ScriptSource scriptSource;
|
||||
|
||||
private String sha1;
|
||||
|
||||
private Class<T> resultType;
|
||||
|
||||
private final Object shaModifiedMonitor = new Object();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scriptLocation
|
||||
* The location of the script
|
||||
* @param resultType
|
||||
* The Script result type
|
||||
*/
|
||||
public DefaultRedisScript(Resource scriptLocation, Class<T> resultType) {
|
||||
this(new ResourceScriptSource(scriptLocation), resultType);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param script
|
||||
* The script
|
||||
* @param resultType
|
||||
* The Script result type
|
||||
*/
|
||||
public DefaultRedisScript(String script, Class<T> resultType) {
|
||||
this(new StaticScriptSource(script), resultType);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scriptSource
|
||||
* The {@link ScriptSource} of the script
|
||||
* @param resultType
|
||||
* The Script result type
|
||||
*/
|
||||
public DefaultRedisScript(ScriptSource scriptSource, Class<T> resultType) {
|
||||
this.scriptSource = scriptSource;
|
||||
this.resultType = resultType;
|
||||
}
|
||||
|
||||
public String getSha1() {
|
||||
synchronized (shaModifiedMonitor) {
|
||||
if (sha1 == null || scriptSource.isModified()) {
|
||||
this.sha1 = DigestUtils.sha1DigestAsHex(getScriptAsString());
|
||||
}
|
||||
return sha1;
|
||||
}
|
||||
}
|
||||
|
||||
public Class<T> getResultType() {
|
||||
return this.resultType;
|
||||
}
|
||||
|
||||
public String getScriptAsString() {
|
||||
try {
|
||||
return scriptSource.getScriptAsString();
|
||||
} catch (IOException e) {
|
||||
throw new ScriptingException("Error reading script text", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.ReturnType;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ScriptExecutor}. Optimizes performance by attempting to execute
|
||||
* script first using evalsha, then falling back to eval if Redis has not yet cached the script.
|
||||
* Evalsha is not attempted if the script is executed in a pipeline or transaction.
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
* @param <K>
|
||||
* The type of keys that may be passed during script execution
|
||||
*/
|
||||
public class DefaultScriptExecutor<K> implements ScriptExecutor<K> {
|
||||
|
||||
private RedisTemplate<K, ?> template;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param template The {@link RedisTemplate} to use
|
||||
*/
|
||||
public DefaultScriptExecutor(RedisTemplate<K, ?> template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T execute(final RedisScript<T> script, final List<K> keys, final Object... args) {
|
||||
// use the Template's value serializer for args and result
|
||||
return execute(script, template.getValueSerializer(),
|
||||
(RedisSerializer<T>) template.getValueSerializer(), keys, args);
|
||||
}
|
||||
|
||||
public <T> T execute(final RedisScript<T> script, final RedisSerializer<?> argsSerializer, final RedisSerializer<T> resultSerializer,
|
||||
final List<K> keys, final Object... args) {
|
||||
return template.execute(new RedisCallback<T>() {
|
||||
public T doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
final ReturnType returnType = ReturnType.fromJavaType(script.getResultType());
|
||||
final byte[][] keysAndArgs = keysAndArgs(argsSerializer, keys, args);
|
||||
final int keySize = keys != null ? keys.size() : 0;
|
||||
if (connection.isPipelined() || connection.isQueueing()) {
|
||||
// We could script load first and then do evalsha to ensure sha is present,
|
||||
// but this adds a sha1 to exec/closePipeline results. Instead, just eval
|
||||
connection.eval(scriptBytes(script), returnType, keySize, keysAndArgs);
|
||||
return null;
|
||||
}
|
||||
return eval(connection, script, returnType, keySize, keysAndArgs,
|
||||
resultSerializer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType,
|
||||
int numKeys, byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {
|
||||
Object result;
|
||||
try {
|
||||
result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
|
||||
} catch (Exception e) {
|
||||
result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
|
||||
}
|
||||
if (script.getResultType() == null) {
|
||||
return null;
|
||||
}
|
||||
return deserializeResult(resultSerializer, result);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
protected byte[][] keysAndArgs(RedisSerializer argsSerializer, List<K> keys, Object[] args) {
|
||||
final int keySize = keys != null ? keys.size() : 0;
|
||||
byte[][] keysAndArgs = new byte[args.length + keySize][];
|
||||
int i = 0;
|
||||
if(keys != null) {
|
||||
for (K key : keys) {
|
||||
if (keySerializer() == null && key instanceof byte[]) {
|
||||
keysAndArgs[i++] = (byte[]) key;
|
||||
} else {
|
||||
keysAndArgs[i++] = keySerializer().serialize(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if (argsSerializer == null && arg instanceof byte[]) {
|
||||
keysAndArgs[i++] = (byte[]) arg;
|
||||
} else {
|
||||
keysAndArgs[i++] = argsSerializer.serialize(arg);
|
||||
}
|
||||
}
|
||||
return keysAndArgs;
|
||||
}
|
||||
|
||||
protected byte[] scriptBytes(RedisScript<?> script) {
|
||||
return template.getStringSerializer().serialize(script.getScriptAsString());
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected <T> T deserializeResult(RedisSerializer<T> resultSerializer, Object result) {
|
||||
if (result instanceof byte[]) {
|
||||
if (resultSerializer == null) {
|
||||
return (T) result;
|
||||
}
|
||||
return resultSerializer.deserialize((byte[]) result);
|
||||
}
|
||||
if (result instanceof List) {
|
||||
List results = new ArrayList();
|
||||
for (Object obj : (List) result) {
|
||||
results.add(deserializeResult(resultSerializer, obj));
|
||||
}
|
||||
return (T) results;
|
||||
}
|
||||
return (T) result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected RedisSerializer keySerializer() {
|
||||
return template.getKeySerializer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Utilties for working with {@link MessageDigest}
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
*/
|
||||
abstract public class DigestUtils {
|
||||
|
||||
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f' };
|
||||
|
||||
private static final Charset UTF8_CHARSET = Charset.forName("UTF8");
|
||||
|
||||
/**
|
||||
* Returns the SHA1 of the provided data
|
||||
*
|
||||
* @param data
|
||||
* The data to calculate, such as the contents of a file
|
||||
* @return The human-readable SHA1
|
||||
*/
|
||||
public static String sha1DigestAsHex(String data) {
|
||||
byte[] dataBytes = getDigest("SHA").digest(data.getBytes(UTF8_CHARSET));
|
||||
return new String(encodeHex(dataBytes));
|
||||
}
|
||||
|
||||
private static char[] encodeHex(byte[] data) {
|
||||
int l = data.length;
|
||||
char[] out = new char[l << 1];
|
||||
for (int i = 0, j = 0; i < l; i++) {
|
||||
out[j++] = HEX_CHARS[(0xF0 & data[i]) >>> 4];
|
||||
out[j++] = HEX_CHARS[0x0F & data[i]];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MessageDigest} with the given algorithm. Necessary because
|
||||
* {@code MessageDigest} is not thread-safe.
|
||||
*/
|
||||
private static MessageDigest getDigest(String algorithm) {
|
||||
try {
|
||||
return MessageDigest.getInstance(algorithm);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException("Could not find MessageDigest with algorithm \""
|
||||
+ algorithm + "\"", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
/**
|
||||
* A script to be executed using the <a href="http://redis.io/commands/eval">Redis scripting
|
||||
* support</a> available as of version 2.6
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
* @param <T>
|
||||
* The script result type. Should be one of Long, Boolean, List, or deserialized value
|
||||
* type. Can be null if the script returns a throw-away status (i.e "OK")
|
||||
*/
|
||||
public interface RedisScript<T> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The SHA1 of the script, used for executing Redis evalsha command
|
||||
*/
|
||||
String getSha1();
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The script result type. Should be one of Long, Boolean, List, or deserialized value
|
||||
* type. Can be null if the script returns a throw-away status (i.e "OK")
|
||||
*/
|
||||
Class<T> getResultType();
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The script contents
|
||||
*/
|
||||
String getScriptAsString();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Executes {@link RedisScript}s
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
* @param <K>
|
||||
* The type of keys that may be passed during script execution
|
||||
*/
|
||||
public interface ScriptExecutor<K> {
|
||||
|
||||
/**
|
||||
* Executes the given {@link RedisScript}
|
||||
*
|
||||
* @param script
|
||||
* The script to execute
|
||||
* @param keys
|
||||
* Any keys that need to be passed to the script
|
||||
* @param args
|
||||
* Any args that need to be passed to the script
|
||||
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
|
||||
* null, likely indicating a throw-away status reply (i.e. "OK")
|
||||
*/
|
||||
<T> T execute(RedisScript<T> script, List<K> keys, Object... args);
|
||||
|
||||
/**
|
||||
* Executes the given {@link RedisScript}, using the provided {@link RedisSerializer}s to
|
||||
* serialize the script arguments and result.
|
||||
*
|
||||
* @param script
|
||||
* The script to execute
|
||||
* @param argsSerializer
|
||||
* The {@link RedisSerializer} to use for serializing args
|
||||
* @param resultSerializer
|
||||
* The {@link RedisSerializer} to use for serializing the script return value
|
||||
* @param keys
|
||||
* Any keys that need to be passed to the script
|
||||
* @param args
|
||||
* Any args that need to be passed to the script
|
||||
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
|
||||
* null, likely indicating a throw-away status reply (i.e. "OK")
|
||||
*/
|
||||
<T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
|
||||
List<K> keys, Object... args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
* {@link RuntimeException} thrown when issues occur with {@link RedisScript}s
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ScriptingException extends NestedRuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new <code>ScriptingException</code> instance.
|
||||
*
|
||||
* @param msg
|
||||
* @param cause
|
||||
*/
|
||||
public ScriptingException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new <code>ScriptingException</code> instance.
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
public ScriptingException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,12 @@ import org.springframework.data.redis.connection.StringRedisConnection;
|
||||
import org.springframework.data.redis.connection.srp.SrpConnectionFactory;
|
||||
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
|
||||
import org.springframework.data.redis.core.query.SortQueryBuilder;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scripting.support.StaticScriptSource;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -608,6 +612,16 @@ public class RedisTemplateTests<K,V> {
|
||||
redisTemplate.convertAndSend("Channel", value1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecuteScriptCustomSerializers() {
|
||||
assumeTrue(RedisTestProfileValueSource.matches("redisVersion", "2.6"));
|
||||
K key1 = keyFactory.instance();
|
||||
final RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"return 'Hey'"), String.class);
|
||||
assertEquals("Hey", redisTemplate.execute(script, redisTemplate.getValueSerializer(), new StringRedisSerializer(),
|
||||
Collections.singletonList(key1)));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
if(serializer == null && value instanceof byte[]) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
import org.springframework.scripting.support.StaticScriptSource;
|
||||
|
||||
/**
|
||||
* Test of {@link DefaultRedisScript}
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
*/
|
||||
public class DefaultRedisScriptTests {
|
||||
|
||||
@Test
|
||||
public void testGetSha1() {
|
||||
StaticScriptSource script = new StaticScriptSource("return KEYS[1]");
|
||||
RedisScript<String> redisScript = new DefaultRedisScript<String>(script, String.class);
|
||||
String sha1 = redisScript.getSha1();
|
||||
// Ensure multiple calls return same sha
|
||||
assertEquals(sha1, redisScript.getSha1());
|
||||
script.setScript("return KEYS[2]");
|
||||
// Sha should now be different as script text has changed
|
||||
assertFalse(sha1.equals(redisScript.getSha1()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetScriptAsString() {
|
||||
RedisScript<String> redisScript = new DefaultRedisScript<String>("return ARGS[1]",
|
||||
String.class);
|
||||
assertEquals("return ARGS[1]", redisScript.getScriptAsString());
|
||||
}
|
||||
|
||||
@Test(expected = ScriptingException.class)
|
||||
public void testGetScriptAsStringError() {
|
||||
RedisScript<Long> redisScript = new DefaultRedisScript<Long>(new ResourceScriptSource(
|
||||
new ClassPathResource("nonexistent")), Long.class);
|
||||
redisScript.getScriptAsString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2013 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.script;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.Person;
|
||||
import org.springframework.data.redis.RedisTestProfileValueSource;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.SessionCallback;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scripting.support.StaticScriptSource;
|
||||
import org.springframework.test.annotation.IfProfileValue;
|
||||
import org.springframework.test.annotation.ProfileValueSourceConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* Integration test of {@link DefaultScriptExecutor}
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@ProfileValueSourceConfiguration(RedisTestProfileValueSource.class)
|
||||
@IfProfileValue(name = "redisVersion", value = "2.6")
|
||||
public class DefaultScriptExecutorTests {
|
||||
|
||||
@Autowired
|
||||
private RedisConnectionFactory connFactory;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private RedisTemplate template;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@After
|
||||
public void tearDown() {
|
||||
template.execute(new RedisCallback<Object>() {
|
||||
public Object doInRedis(RedisConnection connection) {
|
||||
connection.flushDb();
|
||||
connection.scriptFlush();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteLongResult() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript<Long> script = new DefaultRedisScript<Long>(new ClassPathResource(
|
||||
"org/springframework/data/redis/core/script/increment.lua"), Long.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
Long result = scriptExecutor.execute(script, Collections.singletonList("mykey"));
|
||||
assertNull(result);
|
||||
template.boundValueOps("mykey").set("2");
|
||||
assertEquals(Long.valueOf(3),
|
||||
scriptExecutor.execute(script, Collections.singletonList("mykey")));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteBooleanResult() {
|
||||
this.template = new RedisTemplate<String, Long>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript<Boolean> script = new DefaultRedisScript<Boolean>(new ClassPathResource(
|
||||
"org/springframework/data/redis/core/script/cas.lua"), Boolean.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
template.boundValueOps("counter").set(0l);
|
||||
Boolean valueSet = scriptExecutor.execute(script, Collections.singletonList("counter"), 0,
|
||||
3);
|
||||
assertTrue(valueSet);
|
||||
assertFalse(scriptExecutor.execute(script, Collections.singletonList("counter"), 0, 3));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
public void testExecuteListResultCustomArgsSerializer() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
template.boundListOps("mylist").leftPushAll("a", "b", "c", "d");
|
||||
RedisScript<List<String>> script = new DefaultRedisScript(new ClassPathResource(
|
||||
"org/springframework/data/redis/core/script/bulkpop.lua"), List.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
List<String> result = scriptExecutor
|
||||
.execute(script, new GenericToStringSerializer<Long>(Long.class),
|
||||
template.getValueSerializer(), Collections.singletonList("mylist"), 1l);
|
||||
assertEquals(Collections.singletonList("a"), result);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
public void testExecuteMixedListResult() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript<List<Object>> script = new DefaultRedisScript(new ClassPathResource(
|
||||
"org/springframework/data/redis/core/script/popandlength.lua"), List.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
List<Object> results = scriptExecutor.execute(script, Collections.singletonList("mylist"));
|
||||
assertEquals(Arrays.asList(new Object[] { null, 0l }), results);
|
||||
template.boundListOps("mylist").leftPushAll("a", "b");
|
||||
assertEquals(Arrays.asList(new Object[] { "a", 1l }),
|
||||
scriptExecutor.execute(script, Collections.singletonList("mylist")));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteValueResult() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"return redis.call('GET',KEYS[1])"), String.class);
|
||||
template.opsForValue().set("foo", "bar");
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
assertEquals("bar", scriptExecutor.execute(script, Collections.singletonList("foo")));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
public void testExecuteStatusResult() {
|
||||
this.template = new RedisTemplate<String, Long>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript script = new DefaultRedisScript(new StaticScriptSource(
|
||||
"return redis.call('SET',KEYS[1], ARGV[1])"), null);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
assertNull(scriptExecutor.execute(script, Collections.singletonList("foo"), 3l));
|
||||
assertEquals(Long.valueOf(3), template.opsForValue().get("foo"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteCustomResultSerializer() {
|
||||
JacksonJsonRedisSerializer<Person> personSerializer = new JacksonJsonRedisSerializer<Person>(
|
||||
Person.class);
|
||||
this.template = new RedisTemplate<String, Person>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(personSerializer);
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"redis.call('SET',KEYS[1], ARGV[1])\nreturn 'FOO'"), String.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
Person joe = new Person("Joe", "Schmoe", 23);
|
||||
String result = scriptExecutor.execute(script, personSerializer,
|
||||
new StringRedisSerializer(), Collections.singletonList("bar"), joe);
|
||||
assertEquals("FOO", result);
|
||||
assertEquals(joe, template.boundValueOps("bar").get());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecutePipelined() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
final RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"return KEYS[1]"), String.class);
|
||||
List<Object> results = template.executePipelined(new SessionCallback<String>() {
|
||||
@SuppressWarnings("rawtypes")
|
||||
public String execute(RedisOperations operations) throws DataAccessException {
|
||||
return (String) operations.execute(script, Collections.singletonList("foo"));
|
||||
}
|
||||
|
||||
});
|
||||
// Result is deserialized by RedisTemplate as part of executePipelined
|
||||
assertEquals(Collections.singletonList("foo"), results);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteTx() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
final RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"return 'bar'..KEYS[1]"), String.class);
|
||||
List<Object> results = (List<Object>) template.execute(new SessionCallback<List<Object>>() {
|
||||
@SuppressWarnings("rawtypes")
|
||||
public List<Object> execute(RedisOperations operations) throws DataAccessException {
|
||||
operations.multi();
|
||||
operations.execute(script, Collections.singletonList("foo"));
|
||||
return operations.exec();
|
||||
}
|
||||
|
||||
});
|
||||
// Result is deserialized by RedisTemplate as part of exec
|
||||
assertEquals(Collections.singletonList("barfoo"), results);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExecuteCachedNullKeys() {
|
||||
this.template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connFactory);
|
||||
template.afterPropertiesSet();
|
||||
final RedisScript<String> script = new DefaultRedisScript<String>(new StaticScriptSource(
|
||||
"return 'HELLO'"), String.class);
|
||||
ScriptExecutor<String> scriptExecutor = new DefaultScriptExecutor<String>(template);
|
||||
// Execute script twice, second time should be from cache
|
||||
assertEquals("HELLO", scriptExecutor.execute(script, null));
|
||||
assertEquals("HELLO", scriptExecutor.execute(script, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="lettuceConnectionFactory "
|
||||
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
|
||||
<property name="hostName">
|
||||
<bean class="org.springframework.data.redis.SettingsUtils" factory-method="getHost" />
|
||||
</property>
|
||||
<property name="port">
|
||||
<bean class="org.springframework.data.redis.SettingsUtils" factory-method="getPort" />
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,7 @@
|
||||
local size=redis.call('LLEN',KEYS[1])
|
||||
local max = math.min(size,ARGV[1])
|
||||
local t={}
|
||||
for variable = 1, max, 1 do
|
||||
table.insert(t,redis.call('RPOP',KEYS[1]))
|
||||
end
|
||||
return t
|
||||
@@ -0,0 +1,7 @@
|
||||
local current = redis.call('GET', KEYS[1])
|
||||
if current == ARGV[1]
|
||||
then
|
||||
redis.call('SET', KEYS[1], ARGV[2])
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -0,0 +1,5 @@
|
||||
if redis.call("EXISTS",KEYS[1]) == 1 then
|
||||
return redis.call("INCR",KEYS[1])
|
||||
else
|
||||
return nil
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
local t={}
|
||||
table.insert(t,redis.call('RPOP',KEYS[1]))
|
||||
table.insert(t,redis.call('LLEN',KEYS[1]))
|
||||
return t
|
||||
@@ -11,6 +11,7 @@ Import-Template:
|
||||
org.springframework.core.*;version="[3.1.0, 4.0.0)",
|
||||
org.springframework.dao.*;version="[3.1.0, 4.0.0)",
|
||||
org.springframework.scheduling.*;resolution:="optional";version="[3.1.0, 4.0.0)",
|
||||
org.springframework.scripting.*;resolution:="optional";version="[3.1.0, 4.0.0)",
|
||||
org.springframework.util.*;version="[3.1.0, 4.0.0)",
|
||||
org.springframework.oxm.*;resolution:="optional";version="[3.1.0, 4.0.0)",
|
||||
org.springframework.transaction.support.*;version="[3.1.0, 4.0.0)",
|
||||
|
||||
Reference in New Issue
Block a user