GH-109: Kinesis MH: Use MessageConverter
Fixes https://github.com/spring-projects/spring-integration-aws/issues/109 The plain `Converter` is not able to deal with `MessageHeaders` as a context for target payload conversion. * Use `MessageConverter` abstraction to get a request message context for target conversion * Deprecate a `setConverter()` in favor of a new `setMessageConverter()` * Use delegating `ConvertingFromMessageConverter` for default serialization behavior and for backward compatibility * Add `flushDuration` option into `KplMessageHandler` for calling a `kinesisProducer.flush()` periodically * Call `kinesisProducer.flush()` immediately after `addUserRecord()` `if (this.flushDuration.toMillis() <= 0) {`
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2017-2019 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
|
||||
*
|
||||
* https://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.aws.outbound;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A simple {@link MessageConverter} that delegates to a {@link Converter}.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
class ConvertingFromMessageConverter implements MessageConverter {
|
||||
|
||||
private final Converter<Object, ?> delegate;
|
||||
|
||||
ConvertingFromMessageConverter(Converter<Object, ?> delegate) {
|
||||
Assert.notNull(delegate, "'delegate' must not be null");
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(Message<?> message, Class<?> targetClass) {
|
||||
return this.delegate.convert(message.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> toMessage(Object payload, MessageHeaders headers) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.springframework.integration.support.MutableMessage;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -49,7 +50,9 @@ import com.amazonaws.services.kinesis.model.PutRecordsResult;
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Jacob Severson
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @see AmazonKinesisAsync#putRecord(PutRecordRequest)
|
||||
* @see AmazonKinesisAsync#putRecords(PutRecordsRequest)
|
||||
* @see com.amazonaws.handlers.AsyncHandler
|
||||
@@ -58,7 +61,7 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
|
||||
private final AmazonKinesisAsync amazonKinesis;
|
||||
|
||||
private Converter<Object, byte[]> converter = new SerializingConverter();
|
||||
private MessageConverter messageConverter = new ConvertingFromMessageConverter(new SerializingConverter());
|
||||
|
||||
private Expression streamExpression;
|
||||
|
||||
@@ -79,10 +82,21 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
* Specify a {@link Converter} to serialize {@code payload} to the {@code byte[]} if
|
||||
* that isn't {@code byte[]} already.
|
||||
* @param converter the {@link Converter} to use; cannot be null.
|
||||
* @deprecated since 2.3 in favor of {@link #setMessageConverter}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setConverter(Converter<Object, byte[]> converter) {
|
||||
Assert.notNull(converter, "'converter' must not be null.");
|
||||
this.converter = converter;
|
||||
setMessageConverter(new ConvertingFromMessageConverter(converter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MessageConverter} for converting payload to {@code byte[]} for Kinesis record.
|
||||
* @param messageConverter the {@link MessageConverter} to use.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setMessageConverter(MessageConverter messageConverter) {
|
||||
Assert.notNull(messageConverter, "'messageConverter' must not be null.");
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
public void setStream(String stream) {
|
||||
@@ -211,7 +225,10 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
}
|
||||
}
|
||||
else {
|
||||
byte[] bytes = payload instanceof byte[] ? (byte[]) payload : this.converter.convert(payload);
|
||||
byte[] bytes =
|
||||
(byte[]) (payload instanceof byte[]
|
||||
? payload
|
||||
: this.messageConverter.fromMessage(message, byte[].class));
|
||||
Assert.notNull(bytes, "payload cannot be null");
|
||||
if (this.embeddedHeadersMapper != null) {
|
||||
messageToEmbed = new MutableMessage<>(bytes, messageHeaders);
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
package org.springframework.integration.aws.outbound;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.expression.Expression;
|
||||
@@ -33,6 +36,7 @@ import org.springframework.integration.support.MutableMessage;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -56,16 +60,18 @@ import com.google.common.util.concurrent.MoreExecutors;
|
||||
*
|
||||
* @author Arnaud Lecollaire
|
||||
* @author Artem Bilan
|
||||
* @since 2.2.0
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @see AmazonKinesisAsync#putRecord(PutRecordRequest)
|
||||
* @see AmazonKinesisAsync#putRecords(PutRecordsRequest)
|
||||
* @see com.amazonaws.handlers.AsyncHandler
|
||||
*/
|
||||
public class KplMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
public class KplMessageHandler extends AbstractAwsMessageHandler<Void> implements Lifecycle {
|
||||
|
||||
private final KinesisProducer kinesisProducer;
|
||||
|
||||
private Converter<Object, byte[]> converter = new SerializingConverter();
|
||||
private MessageConverter messageConverter = new ConvertingFromMessageConverter(new SerializingConverter());
|
||||
|
||||
private Expression streamExpression;
|
||||
|
||||
@@ -77,6 +83,12 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
|
||||
private OutboundMessageMapper<byte[]> embeddedHeadersMapper;
|
||||
|
||||
private Duration flushDuration = Duration.ofMillis(0);
|
||||
|
||||
private volatile boolean running;
|
||||
|
||||
private volatile ScheduledFuture<?> flushFuture;
|
||||
|
||||
public KplMessageHandler(KinesisProducer kinesisProducer) {
|
||||
Assert.notNull(kinesisProducer, "'kinesisProducer' must not be null.");
|
||||
this.kinesisProducer = kinesisProducer;
|
||||
@@ -86,10 +98,21 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
* Specify a {@link Converter} to serialize {@code payload} to the {@code byte[]} if
|
||||
* that isn't {@code byte[]} already.
|
||||
* @param converter the {@link Converter} to use; cannot be null.
|
||||
@deprecated since 2.3 in favor of {@link #setMessageConverter}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setConverter(Converter<Object, byte[]> converter) {
|
||||
Assert.notNull(converter, "'converter' must not be null.");
|
||||
this.converter = converter;
|
||||
setMessageConverter(new ConvertingFromMessageConverter(converter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MessageConverter} for converting payload to {@code byte[]} for Kinesis record.
|
||||
* @param messageConverter the {@link MessageConverter} to use.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setMessageConverter(MessageConverter messageConverter) {
|
||||
Assert.notNull(messageConverter, "'messageConverter' must not be null.");
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
public void setStream(String stream) {
|
||||
@@ -159,25 +182,59 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
+ "Consider to use 'OutboundMessageMapper<byte[]>' for embedding headers into the record data.");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
if (!this.running) {
|
||||
if (this.flushDuration.toMillis() > 0) {
|
||||
this.flushFuture = getTaskScheduler()
|
||||
.scheduleAtFixedRate(this.kinesisProducer::flush, this.flushDuration);
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
if (this.running) {
|
||||
this.running = false;
|
||||
if (this.flushFuture != null) {
|
||||
this.flushFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Future<?> handleMessageToAws(Message<?> message) {
|
||||
if (message.getPayload() instanceof PutRecordsRequest) {
|
||||
throw new UnsupportedOperationException("not implemented");
|
||||
}
|
||||
else if (message.getPayload() instanceof UserRecord) {
|
||||
return handleUserRecord(message, buildPutRecordRequest(message), (UserRecord) message.getPayload());
|
||||
}
|
||||
else {
|
||||
final PutRecordRequest putRecordRequest = (message.getPayload() instanceof PutRecordRequest)
|
||||
? (PutRecordRequest) message.getPayload() : buildPutRecordRequest(message);
|
||||
try {
|
||||
if (message.getPayload() instanceof PutRecordsRequest) {
|
||||
throw new UnsupportedOperationException("not implemented");
|
||||
}
|
||||
else if (message.getPayload() instanceof UserRecord) {
|
||||
return handleUserRecord(message, buildPutRecordRequest(message), (UserRecord) message.getPayload());
|
||||
}
|
||||
else {
|
||||
final PutRecordRequest putRecordRequest = (message.getPayload() instanceof PutRecordRequest)
|
||||
? (PutRecordRequest) message.getPayload() : buildPutRecordRequest(message);
|
||||
|
||||
// convert the PutRecordRequest to a UserRecord
|
||||
UserRecord userRecord = new UserRecord();
|
||||
userRecord.setExplicitHashKey(putRecordRequest.getExplicitHashKey());
|
||||
userRecord.setData(putRecordRequest.getData());
|
||||
userRecord.setPartitionKey(putRecordRequest.getPartitionKey());
|
||||
userRecord.setStreamName(putRecordRequest.getStreamName());
|
||||
return handleUserRecord(message, putRecordRequest, userRecord);
|
||||
// convert the PutRecordRequest to a UserRecord
|
||||
UserRecord userRecord = new UserRecord();
|
||||
userRecord.setExplicitHashKey(putRecordRequest.getExplicitHashKey());
|
||||
userRecord.setData(putRecordRequest.getData());
|
||||
userRecord.setPartitionKey(putRecordRequest.getPartitionKey());
|
||||
userRecord.setStreamName(putRecordRequest.getStreamName());
|
||||
return handleUserRecord(message, putRecordRequest, userRecord);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.flushDuration.toMillis() <= 0) {
|
||||
this.kinesisProducer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +302,10 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> {
|
||||
}
|
||||
}
|
||||
else {
|
||||
byte[] bytes = payload instanceof byte[] ? (byte[]) payload : this.converter.convert(payload);
|
||||
byte[] bytes =
|
||||
(byte[]) (payload instanceof byte[]
|
||||
? payload
|
||||
: this.messageConverter.fromMessage(message, byte[].class));
|
||||
Assert.notNull(bytes, "payload cannot be null");
|
||||
if (this.embeddedHeadersMapper != null) {
|
||||
messageToEmbed = new MutableMessage<>(bytes, messageHeaders);
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.aws.support.AwsHeaders;
|
||||
@@ -44,6 +43,8 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -178,12 +179,13 @@ public class KinesisMessageHandlerTests {
|
||||
KinesisMessageHandler kinesisMessageHandler = new KinesisMessageHandler(amazonKinesis());
|
||||
kinesisMessageHandler.setSync(true);
|
||||
kinesisMessageHandler.setAsyncHandler(asyncHandler());
|
||||
kinesisMessageHandler.setConverter(new Converter<Object, byte[]>() {
|
||||
kinesisMessageHandler.setMessageConverter(new MessageConverter() {
|
||||
|
||||
private SerializingConverter serializingConverter = new SerializingConverter();
|
||||
|
||||
@Override
|
||||
public byte[] convert(Object source) {
|
||||
public Object fromMessage(Message<?> message, Class<?> targetClass) {
|
||||
Object source = message.getPayload();
|
||||
if (source instanceof String) {
|
||||
return ((String) source).getBytes();
|
||||
}
|
||||
@@ -192,6 +194,11 @@ public class KinesisMessageHandlerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> toMessage(Object payload, MessageHeaders headers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
kinesisMessageHandler.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo"));
|
||||
return kinesisMessageHandler;
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.aws.support.AwsHeaders;
|
||||
@@ -41,7 +40,9 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -173,10 +174,10 @@ public class KinesisProducingMessageHandlerTests {
|
||||
handler.onSuccess(new PutRecordsRequest(), new PutRecordsResult());
|
||||
return mock(Future.class);
|
||||
}).willAnswer(invocation -> {
|
||||
AsyncHandler<?, ?> handler = invocation.getArgument(1);
|
||||
handler.onError(new RuntimeException("putRecordsRequestEx"));
|
||||
return mock(Future.class);
|
||||
});
|
||||
AsyncHandler<?, ?> handler = invocation.getArgument(1);
|
||||
handler.onError(new RuntimeException("putRecordsRequestEx"));
|
||||
return mock(Future.class);
|
||||
});
|
||||
|
||||
return mock;
|
||||
}
|
||||
@@ -198,12 +199,13 @@ public class KinesisProducingMessageHandlerTests {
|
||||
kinesisMessageHandler.setSync(true);
|
||||
kinesisMessageHandler.setOutputChannel(successChannel());
|
||||
kinesisMessageHandler.setFailureChannel(errorChannel());
|
||||
kinesisMessageHandler.setConverter(new Converter<Object, byte[]>() {
|
||||
kinesisMessageHandler.setMessageConverter(new MessageConverter() {
|
||||
|
||||
private SerializingConverter serializingConverter = new SerializingConverter();
|
||||
|
||||
@Override
|
||||
public byte[] convert(Object source) {
|
||||
public Object fromMessage(Message<?> message, Class<?> targetClass) {
|
||||
Object source = message.getPayload();
|
||||
if (source instanceof String) {
|
||||
return ((String) source).getBytes();
|
||||
}
|
||||
@@ -212,6 +214,11 @@ public class KinesisProducingMessageHandlerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> toMessage(Object payload, MessageHeaders headers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
return kinesisMessageHandler;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user