Add RedisTemplate methods for executing Redis scripts

DATAREDIS-199
This commit is contained in:
Jennifer Hickey
2013-08-04 16:43:00 -07:00
parent faa78cdfaa
commit 7d387ab529
19 changed files with 914 additions and 1 deletions

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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[]) {

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
if redis.call("EXISTS",KEYS[1]) == 1 then
return redis.call("INCR",KEYS[1])
else
return nil
end

View File

@@ -0,0 +1,4 @@
local t={}
table.insert(t,redis.call('RPOP',KEYS[1]))
table.insert(t,redis.call('LLEN',KEYS[1]))
return t

View File

@@ -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)",