INT-2918: Add Redis Outbound Command Gateway
JIRA: https://jira.springsource.org/browse/INT-2918 INT-2918: Improvement according PR comments INT-2918: Polishing according PR comments Minor Doc Polishing INT-2918: Change `notNull` message Polishing
This commit is contained in:
committed by
Gary Russell
parent
c11e3ba3b5
commit
945455f360
@@ -99,7 +99,7 @@ subprojects { subproject ->
|
||||
smackVersion = '3.2.1'
|
||||
springAmqpVersion = project.hasProperty('springAmqpVersion') ? project.springAmqpVersion : '1.3.0.RELEASE'
|
||||
springDataMongoVersion = '1.1.1.RELEASE'
|
||||
springDataRedisVersion = '1.1.1.RELEASE'
|
||||
springDataRedisVersion = '1.2.1.RELEASE'
|
||||
springGemfireVersion = '1.3.1.RELEASE'
|
||||
springSecurityVersion = '3.1.3.RELEASE'
|
||||
springSocialTwitterVersion = '1.1.0.M4'
|
||||
|
||||
@@ -34,5 +34,6 @@ public class RedisNamespaceHandler extends AbstractIntegrationNamespaceHandler {
|
||||
registerBeanDefinitionParser("outbound-channel-adapter", new RedisOutboundChannelAdapterParser());
|
||||
registerBeanDefinitionParser("queue-inbound-channel-adapter", new RedisQueueInboundChannelAdapterParser());
|
||||
registerBeanDefinitionParser("queue-outbound-channel-adapter", new RedisQueueOutboundChannelAdapterParser());
|
||||
registerBeanDefinitionParser("outbound-gateway", new RedisOutboundGatewayParser());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.config;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
|
||||
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
|
||||
import org.springframework.integration.redis.outbound.ExpressionArgumentsStrategy;
|
||||
import org.springframework.integration.redis.outbound.RedisOutboundGateway;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Parser for the {@code <int-redis:outbound-gateway/>} component.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
*/
|
||||
public class RedisOutboundGatewayParser extends AbstractConsumerEndpointParser {
|
||||
|
||||
@Override
|
||||
protected String getInputChannelAttributeName() {
|
||||
return "request-channel";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RedisOutboundGateway.class);
|
||||
|
||||
String redisTemplate = element.getAttribute("redis-template");
|
||||
String connectionFactory = element.getAttribute("connection-factory");
|
||||
if (StringUtils.hasText(redisTemplate) && StringUtils.hasText(connectionFactory)) {
|
||||
parserContext.getReaderContext().error("Only one of '" + redisTemplate + "' or '"
|
||||
+ connectionFactory + "' is allowed.", element);
|
||||
}
|
||||
if (StringUtils.hasText(redisTemplate)) {
|
||||
builder.addConstructorArgReference(redisTemplate);
|
||||
}
|
||||
else {
|
||||
if (!StringUtils.hasText(connectionFactory)) {
|
||||
connectionFactory = "redisConnectionFactory";
|
||||
}
|
||||
builder.addConstructorArgReference(connectionFactory);
|
||||
}
|
||||
|
||||
String argumentExpressions = element.getAttribute("argument-expressions");
|
||||
boolean hasArgumentExpressions = StringUtils.hasText(argumentExpressions);
|
||||
String argumentsStrategy = element.getAttribute("arguments-strategy");
|
||||
boolean hasArgumentStrategy = element.hasAttribute("arguments-strategy");
|
||||
|
||||
if (hasArgumentExpressions & hasArgumentStrategy) {
|
||||
parserContext.getReaderContext()
|
||||
.error("'argument-expressions' and 'arguments-strategy' are mutually exclusive.", element);
|
||||
}
|
||||
|
||||
if (hasArgumentExpressions) {
|
||||
BeanDefinitionBuilder argumentsBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionArgumentsStrategy.class)
|
||||
.addConstructorArgValue(argumentExpressions)
|
||||
.addConstructorArgValue(element.getAttribute("use-command-variable"));
|
||||
builder.addPropertyValue("argumentsStrategy", argumentsBuilder.getBeanDefinition());
|
||||
}
|
||||
else if (StringUtils.hasLength(argumentsStrategy)) {
|
||||
builder.addPropertyReference("argumentsStrategy", argumentsStrategy);
|
||||
}
|
||||
else if (hasArgumentStrategy) {
|
||||
builder.addPropertyValue("argumentsStrategy", null);
|
||||
}
|
||||
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "requires-reply");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout", "sendTimeout");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "command-expression");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "arguments-serializer");
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.outbound;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface ArgumentsStrategy {
|
||||
|
||||
Object[] resolve(String command, Message<?> message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.outbound;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.expression.IntegrationEvaluationContextAware;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
*/
|
||||
public class ExpressionArgumentsStrategy implements ArgumentsStrategy, IntegrationEvaluationContextAware, BeanFactoryAware {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
private final Expression[] argumentExpressions;
|
||||
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
private final boolean useCommandVariable;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
public ExpressionArgumentsStrategy(String[] argumentExpressions) {
|
||||
this(argumentExpressions, false);
|
||||
}
|
||||
|
||||
public ExpressionArgumentsStrategy(String[] argumentExpressions, boolean useCommandVariable) {
|
||||
Assert.notNull(argumentExpressions, "'argumentExpressions' must not be null");
|
||||
Assert.noNullElements(argumentExpressions, "'argumentExpressions' cannot have null values.");
|
||||
List<Expression> expressions = new LinkedList<Expression>();
|
||||
for (String argumentExpression : argumentExpressions) {
|
||||
expressions.add(PARSER.parseExpression(argumentExpression));
|
||||
}
|
||||
this.argumentExpressions = expressions.toArray(new Expression[expressions.size()]);
|
||||
this.useCommandVariable = useCommandVariable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
|
||||
Assert.notNull(evaluationContext, "'evaluationContext' must not be null");
|
||||
this.evaluationContext = evaluationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] resolve(String command, Message<?> message) {
|
||||
EvaluationContext evaluationContext = this.evaluationContext;
|
||||
|
||||
if (this.useCommandVariable) {
|
||||
evaluationContext = IntegrationContextUtils.getEvaluationContext(this.beanFactory);
|
||||
evaluationContext.setVariable("cmd", command);
|
||||
}
|
||||
|
||||
List<Object> arguments = new ArrayList<Object>();
|
||||
for (Expression argumentExpression : this.argumentExpressions) {
|
||||
Object argument = argumentExpression.getValue(evaluationContext, message);
|
||||
if (argument != null) {
|
||||
arguments.add(argument);
|
||||
}
|
||||
}
|
||||
return arguments.toArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.outbound;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.expression.IntegrationEvaluationContextAware;
|
||||
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
|
||||
import org.springframework.integration.redis.support.RedisHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* The Gateway component implementation to perform Redis commands with provided arguments and to return command result.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
*/
|
||||
public class RedisOutboundGateway extends AbstractReplyProducingMessageHandler
|
||||
implements IntegrationEvaluationContextAware {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
private final RedisTemplate<?, ?> redisTemplate;
|
||||
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
private volatile RedisSerializer<Object> argumentsSerializer = new GenericToStringSerializer<Object>(Object.class);
|
||||
|
||||
private volatile Expression commandExpression = PARSER.parseExpression("headers[" + RedisHeaders.COMMAND + "]");
|
||||
|
||||
private volatile ArgumentsStrategy argumentsStrategy = new PayloadArgumentsStrategy();
|
||||
|
||||
public RedisOutboundGateway(RedisTemplate<?, ?> redisTemplate) {
|
||||
Assert.notNull(redisTemplate, "'redisTemplate' must not be null");
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
public RedisOutboundGateway(RedisConnectionFactory connectionFactory) {
|
||||
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
|
||||
this.redisTemplate = new RedisTemplate<Object, Object>();
|
||||
this.redisTemplate.setConnectionFactory(connectionFactory);
|
||||
this.redisTemplate.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
|
||||
Assert.notNull(evaluationContext, "'evaluationContext' must not be null");
|
||||
this.evaluationContext = evaluationContext;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setArgumentsSerializer(RedisSerializer<?> serializer) {
|
||||
Assert.notNull(serializer, "'serializer' must not be null");
|
||||
this.argumentsSerializer = (RedisSerializer<Object>) serializer;
|
||||
}
|
||||
|
||||
public void setCommandExpression(String commandExpression) {
|
||||
Assert.hasText(commandExpression, "'commandExpression' must not be an empty string");
|
||||
this.commandExpression = PARSER.parseExpression(commandExpression);
|
||||
}
|
||||
|
||||
public void setArgumentsStrategy(ArgumentsStrategy argumentsStrategy) {
|
||||
this.argumentsStrategy = argumentsStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
final String command = this.commandExpression.getValue(this.evaluationContext, requestMessage, String.class);
|
||||
Assert.notNull(command, "The 'command' must not evaluate to 'null'.");
|
||||
byte[][] args = null;
|
||||
if (this.argumentsStrategy != null) {
|
||||
Object[] arguments = this.argumentsStrategy.resolve(command, requestMessage);
|
||||
if (!ObjectUtils.isEmpty(arguments)) {
|
||||
args = new byte[arguments.length][];
|
||||
|
||||
for (int i = 0; i < arguments.length; i++) {
|
||||
Object argument = arguments[i];
|
||||
byte[] arg = null;
|
||||
if (argument instanceof byte[]) {
|
||||
arg = (byte[]) argument;
|
||||
}
|
||||
else {
|
||||
arg = this.argumentsSerializer.serialize(argument);
|
||||
}
|
||||
args[i] = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final byte[][] actualArgs = args;
|
||||
|
||||
return this.redisTemplate.execute(new RedisCallback<Object>() {
|
||||
|
||||
@Override
|
||||
public Object doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
return connection.execute(command, actualArgs);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private class PayloadArgumentsStrategy implements ArgumentsStrategy {
|
||||
|
||||
@Override
|
||||
public Object[] resolve(String command, Message<?> message) {
|
||||
Object payload = message.getPayload();
|
||||
if (payload instanceof Object[]) {
|
||||
return (Object[]) payload;
|
||||
}
|
||||
else {
|
||||
return new Object[]{payload};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.support;
|
||||
|
||||
/**
|
||||
@@ -6,6 +22,7 @@ package org.springframework.integration.redis.support;
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @since 2.2
|
||||
*/
|
||||
public class RedisHeaders {
|
||||
@@ -20,4 +37,6 @@ public class RedisHeaders {
|
||||
|
||||
public static final String ZSET_INCREMENT_SCORE = PREFIX + "zsetIncrementScore";
|
||||
|
||||
public static final String COMMAND = PREFIX + "command";
|
||||
|
||||
}
|
||||
|
||||
@@ -499,6 +499,163 @@
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
<xsd:element name="outbound-gateway">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Defines an outbound Redis Command Sending Gateway.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:choice minOccurs="0" maxOccurs="2">
|
||||
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
|
||||
<xsd:attribute name="connection-factory" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Reference to a RedisConnectionFactory. If none is provided, the default
|
||||
bean name for the reference will be "redisConnectionFactory".
|
||||
Mutually exclusive with 'redis-template' attribute.
|
||||
</xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.data.redis.connection.RedisConnectionFactory"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="redis-template" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
RedisTemplate to be used with this gateway.
|
||||
Mutually exclusive with 'connection-factory' attribute.
|
||||
</xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.data.redis.core.RedisTemplate"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="request-channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.messaging.MessageChannel"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="requires-reply" type="xsd:string" use="optional" default="true">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specify whether this outbound gateway must return a non-null value. This value is
|
||||
'false' by default, otherwise a ReplyRequiredException will be thrown when
|
||||
the underlying service returns a null value.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="reply-channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.messaging.MessageChannel"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="reply-timeout" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Allows you to specify how long this gateway will wait for
|
||||
the reply message to be sent successfully to the reply channel
|
||||
before throwing an exception. This attribute only applies when the
|
||||
channel might block, for example when using a bounded queue channel that
|
||||
is currently full.
|
||||
|
||||
Also, keep in mind that when sending to a DirectChannel, the
|
||||
invocation will occur in the sender's thread. Therefore,
|
||||
the failing of the send operation may be caused by other
|
||||
components further downstream.
|
||||
|
||||
The "reply-timeout" attribute maps to the "sendTimeout" property of the
|
||||
underlying 'MessagingTemplate' instance (org.springframework.integration.core.MessagingTemplate).
|
||||
|
||||
The attribute will default, if not specified, to '-1', meaning that
|
||||
by default, the Gateway will wait indefinitely. The value is
|
||||
specified in milliseconds.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="order" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Specifies the order for invocation when this adapter is connected as a
|
||||
subscriber to a SubscribableChannel.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="arguments-serializer" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to an instance of org.springframework.data.redis.serializer.RedisSerializer.
|
||||
Used to serialize each command argument to byte[] if necessary.
|
||||
</xsd:documentation>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.data.redis.serializer.RedisSerializer"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="command-expression" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
SpEL expression that returns the command key. Default is the 'redis_command' message header.
|
||||
The command must not be evaluated to 'null'.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="argument-expressions" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Comma-separated SpEL expressions that will be evaluated as command arguments.
|
||||
Mutually exclusive with the 'arguments-strategy' attribute. If 'use-command-variable' is
|
||||
specified to 'true', the '#cmd' variable will be presented within evaluation context.
|
||||
Argument expressions may evaluate to 'null', to support a variable number of arguments.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="use-command-variable" type="xsd:string" default="false">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specifies, if the evaluated Redis command string will be made available
|
||||
as the '#cmd' variable in the SpEL evaluation context in the
|
||||
org.springframework.integration.redis.outbound.ExpressionArgumentsStrategy
|
||||
when 'argument-expressions' is
|
||||
configured, otherwise this attribute is ignored.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="arguments-strategy" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
<xsd:documentation>
|
||||
Reference to an instance of org.springframework.integration.redis.outbound.ArgumentsStrategy.
|
||||
Mutually exclusive with the 'argument-expressions' attribute. By default the 'payload' is used
|
||||
as the command argument(s). To disable any strategy (no arguments) this attribute should be
|
||||
configured as an empty string.
|
||||
</xsd:documentation>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="org.springframework.integration.redis.outbound.ArgumentsStrategy"/>
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
<xsd:complexType name="redisAdapterType">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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:int="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-redis="http://www.springframework.org/schema/integration/redis"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration
|
||||
http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/integration/redis
|
||||
http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd">
|
||||
|
||||
<int:channel id="replyChannel">
|
||||
<int:queue/>
|
||||
</int:channel>
|
||||
|
||||
<int:channel id="pingChannel"/>
|
||||
|
||||
<int:channel id="leftPushChannel"/>
|
||||
|
||||
<int:channel id="leftPushRightPopChannel"/>
|
||||
|
||||
<int:channel id="incrementAtomicIntegerChannel"/>
|
||||
|
||||
<int:channel id="getCommandChannel"/>
|
||||
|
||||
<int:channel id="setDelCommandChannel"/>
|
||||
|
||||
<int:channel id="mgetCommandChannel"/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="pingChannel" reply-channel="replyChannel"
|
||||
arguments-strategy=""/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="leftPushRightPopChannel" reply-channel="replyChannel"
|
||||
connection-factory="redisConnectionFactory"
|
||||
argument-expressions="headers.queue, #cmd == 'LPUSH' ? payload : null"
|
||||
use-command-variable="true"/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="incrementAtomicIntegerChannel" reply-channel="replyChannel"
|
||||
command-expression="payload"
|
||||
argument-expressions="'si.test.RedisAtomicInteger'"/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="setDelCommandChannel" reply-channel="replyChannel"/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="getCommandChannel" reply-channel="replyChannel"
|
||||
command-expression="'GET'"/>
|
||||
|
||||
<int-redis:outbound-gateway request-channel="mgetCommandChannel" reply-channel="replyChannel"
|
||||
command-expression="'MGET'"/>
|
||||
|
||||
|
||||
<bean id="redisConnectionFactory"
|
||||
class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
|
||||
<property name="port" value="#{T(org.springframework.integration.redis.rules.RedisAvailableRule).REDIS_PORT}"/>
|
||||
</bean>
|
||||
|
||||
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
|
||||
<constructor-arg ref="redisConnectionFactory"/>
|
||||
</bean>
|
||||
|
||||
<bean id="atomicInteger" class="org.springframework.data.redis.support.atomic.RedisAtomicInteger">
|
||||
<constructor-arg value="si.test.RedisAtomicInteger"/>
|
||||
<constructor-arg ref="redisConnectionFactory"/>
|
||||
<constructor-arg value="10"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2014 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.integration.redis.outbound;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
|
||||
import org.springframework.integration.handler.ReplyRequiredException;
|
||||
import org.springframework.integration.redis.rules.RedisAvailable;
|
||||
import org.springframework.integration.redis.rules.RedisAvailableTests;
|
||||
import org.springframework.integration.redis.support.RedisHeaders;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.lambdaworks.redis.protocol.CommandType;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class RedisOutboundGatewayTests extends RedisAvailableTests {
|
||||
|
||||
@Autowired
|
||||
private PollableChannel replyChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel pingChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel leftPushRightPopChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel incrementAtomicIntegerChannel;
|
||||
|
||||
@Autowired
|
||||
private RedisAtomicInteger atomicInteger;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel setDelCommandChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel getCommandChannel;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel mgetCommandChannel;
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testPingPongCommand() {
|
||||
this.pingChannel.send(MessageBuilder.withPayload("foo").setHeader(RedisHeaders.COMMAND, CommandType.PING).build());
|
||||
Message<?> receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertTrue(Arrays.equals("PONG".getBytes(), (byte[]) receive.getPayload()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testPushAndPopCommands() {
|
||||
final String queueName = "si.test.testRedisOutboundGateway";
|
||||
String payload = "testing";
|
||||
this.leftPushRightPopChannel.send(MessageBuilder.withPayload(payload)
|
||||
.setHeader(RedisHeaders.COMMAND, CommandType.LPUSH)
|
||||
.setHeader("queue", queueName)
|
||||
.build());
|
||||
Message<?> receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
|
||||
this.leftPushRightPopChannel.send(MessageBuilder.withPayload(payload)
|
||||
.setHeader(RedisHeaders.COMMAND, CommandType.RPOP)
|
||||
.setHeader("queue", queueName)
|
||||
.build());
|
||||
receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertTrue(Arrays.equals(payload.getBytes(), (byte[]) receive.getPayload()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testIncrementAtomicCommand() {
|
||||
this.incrementAtomicIntegerChannel.send(MessageBuilder.withPayload(CommandType.INCR).build());
|
||||
System.out.println();
|
||||
Message<?> receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertEquals(11L, receive.getPayload());
|
||||
assertEquals(11, this.atomicInteger.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testGetCommand() {
|
||||
this.setDelCommandChannel.send(MessageBuilder.withPayload(new String[]{"foo", "bar"}).setHeader(RedisHeaders.COMMAND, CommandType.SET).build());
|
||||
Message<?> receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertEquals("OK", receive.getPayload());
|
||||
|
||||
this.getCommandChannel.send(MessageBuilder.withPayload("foo").build());
|
||||
receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertTrue(Arrays.equals("bar".getBytes(), (byte[]) receive.getPayload()));
|
||||
|
||||
this.setDelCommandChannel.send(MessageBuilder.withPayload("foo").setHeader(RedisHeaders.COMMAND, CommandType.DEL).build());
|
||||
receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertEquals(1L, receive.getPayload());
|
||||
|
||||
try {
|
||||
this.getCommandChannel.send(MessageBuilder.withPayload("foo").build());
|
||||
fail("ReplyRequiredException expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertThat(e, Matchers.instanceOf(ReplyRequiredException.class));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testMGetCommand() {
|
||||
RedisConnection connection = this.getConnectionFactoryForTest().getConnection();
|
||||
byte[] value1 = "bar1".getBytes();
|
||||
byte[] value2 = "bar2".getBytes();
|
||||
connection.set("foo1".getBytes(), value1);
|
||||
connection.set("foo2".getBytes(), value2);
|
||||
this.mgetCommandChannel.send(MessageBuilder.withPayload(new String [] {"foo1", "foo2"}).build());
|
||||
Message<?> receive = this.replyChannel.receive(1000);
|
||||
assertNotNull(receive);
|
||||
assertThat((List<byte[]>) receive.getPayload(), Matchers.contains(value1, value2));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -660,4 +660,114 @@ the serialization of values, you may want to consider providing your own
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section id="redis-outbound-gateway">
|
||||
<title>Redis Outbound Command Gateway</title>
|
||||
<para>
|
||||
Since <emphasis>Spring Integration 4.0</emphasis>, the Redis Command Gateway is available to
|
||||
perform any standard Redis command using generic
|
||||
<code>RedisConnection#execute</code>
|
||||
method:
|
||||
<programlisting language="xml"><![CDATA[<int-redis:outbound-gateway
|
||||
request-channel="" ]]><co id="redis-o-g-request-channel"/><![CDATA[
|
||||
reply-channel="" ]]><co id="redis-o-g-reply-channel"/><![CDATA[
|
||||
requires-reply="" ]]><co id="redis-o-g-requires-reply"/><![CDATA[
|
||||
reply-timeout="" ]]><co id="redis-o-g-reply-timeout"/><![CDATA[
|
||||
connection-factory="" ]]><co id="redis-o-g-connectionFactory"/><![CDATA[
|
||||
redis-template="" ]]><co id="redis-o-g-template"/><![CDATA[
|
||||
arguments-serializer="" ]]><co id="redis-o-g-arguments-serializer"/><![CDATA[
|
||||
command-expression="" ]]><co id="redis-o-g-command-expression"/><![CDATA[
|
||||
argument-expressions="" ]]><co id="redis-o-g-argument-expressions"/><![CDATA[
|
||||
use-command-variable="" ]]><co id="redis-o-g-use-command-variable"/><![CDATA[
|
||||
arguments-strategy="" />]]><co id="redis-o-g-arguments-strategy"/>
|
||||
</programlisting>
|
||||
<calloutlist>
|
||||
<callout arearefs="redis-o-g-request-channel">
|
||||
<para>
|
||||
The <interfacename>MessageChannel</interfacename> from which this Endpoint receives<interfacename>Message</interfacename>s.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-reply-channel">
|
||||
<para>
|
||||
The <interfacename>MessageChannel</interfacename> where this Endpoint sends reply<interfacename>Message</interfacename>s.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-requires-reply">
|
||||
<para>
|
||||
Specify whether this outbound gateway must return a non-null value. This value is
|
||||
<code>false</code> by default, otherwise a ReplyRequiredException will be thrown when
|
||||
the Redis returns a <code>null</code> value.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-reply-timeout">
|
||||
<para>
|
||||
The timeout in milliseconds to wait until the reply message will be sent or not. Typically is
|
||||
applied for queue-based limited reply-channels.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-connectionFactory">
|
||||
<para>
|
||||
A reference to a <interfacename>RedisConnectionFactory</interfacename>bean.
|
||||
Defaults to <code>redisConnectionFactory</code>. Mutually exclusive with 'redis-template' attribute.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-template">
|
||||
<para>
|
||||
A reference to a <classname>RedisTemplate</classname> bean.
|
||||
Mutually exclusive with 'connection-factory' attribute.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-arguments-serializer">
|
||||
<para>
|
||||
Reference to an instance of<interfacename>org.springframework.data.redis.serializer.RedisSerializer</interfacename>.
|
||||
Used to serialize each command argument to byte[] if necessary.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-command-expression">
|
||||
<para>
|
||||
The SpEL expression that returns the command key. Default is the <code>redis_command</code> message header.
|
||||
Must not evaluate to <code>null</code>.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-argument-expressions">
|
||||
<para>
|
||||
Comma-separate SpEL expressions that will be evaluated as command arguments.
|
||||
Mutually exclusive with the <code>arguments-strategy</code> attribute. If neither of them is provided
|
||||
the <code>payload</code> is used as the command argument(s).
|
||||
Argument expressions may evaluate to 'null', to support a variable number of arguments.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-use-command-variable">
|
||||
<para>
|
||||
A <code>boolean</code> flag to specify if the evaluated Redis command string will be
|
||||
made available as the <code>#cmd</code> variable
|
||||
in the expression evaluation context in the
|
||||
<classname>org.springframework.integration.redis.outbound.ExpressionArgumentsStrategy</classname>
|
||||
when <code>argument-expressions</code> is configured, otherwise this attribute is ignored.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs="redis-o-g-arguments-strategy">
|
||||
<para>
|
||||
Reference to an instance of <interfacename>org.springframework.integration.redis.outbound.ArgumentsStrategy</interfacename>.
|
||||
Mutually exclusive with <code>argument-expressions</code> attribute. If neither of them is provided
|
||||
the <code>payload</code> is used as the command argument(s).
|
||||
</para>
|
||||
</callout>
|
||||
</calloutlist>
|
||||
</para>
|
||||
<para>
|
||||
The <code><int-redis:outbound-gateway></code> can be used as a common component to perform any desired
|
||||
Redis operation. For example to get incremented value from Redis Atomic Number:
|
||||
<programlisting language="xml"><![CDATA[<int-redis:outbound-gateway request-channel="requestChannel"
|
||||
reply-channel="replyChannel"
|
||||
command-expression="'INCR'"/>]]></programlisting>
|
||||
where the Message <code>payload</code> should be a name of <code>redisCounter</code>, which may be provided
|
||||
by <classname>org.springframework.data.redis.support.atomic.RedisAtomicInteger</classname> bean definition.
|
||||
</para>
|
||||
<para>
|
||||
The <code>RedisConnection#execute</code> has a generic <classname>Object</classname> as return type and real
|
||||
result depends on command type, for example <code>MGET</code> returns a <code>List<byte[]></code>.
|
||||
For more information about commands, their arguments and result type see
|
||||
<ulink url="http://redis.io/commands">Redis Specification</ulink>.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
@@ -117,6 +117,14 @@
|
||||
For more information, see <xref linkend="security"/>.
|
||||
</para>
|
||||
</section>
|
||||
<section id="4.0-redis-outbound-gateway">
|
||||
<title>Redis Command Gateway</title>
|
||||
<para>
|
||||
The Redis support now provides the <code><outbound-gateway></code> component
|
||||
to perform generic Redis commands using the <code>RedisConnection#execute</code> method.
|
||||
For more information, see <xref linkend="redis-outbound-gateway"/>.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="4.0-general">
|
||||
|
||||
Reference in New Issue
Block a user