GH-1225: @KL - skip conversion when not needed
Resolves https://github.com/spring-projects/spring-kafka/issues/1225 When no argument is sourced from a `Message<?>` (no `@Headers`, `@Payload`, etc - only `ConsumerRecord`, `Acknowledgment` or `Consumer`) then don't convert. * Fix logging - don't use supplier with is...().
This commit is contained in:
committed by
Artem Bilan
parent
f86110e831
commit
da9dc28050
@@ -28,7 +28,6 @@ import org.springframework.kafka.listener.BatchAcknowledgingConsumerAwareMessage
|
||||
import org.springframework.kafka.listener.KafkaListenerErrorHandler;
|
||||
import org.springframework.kafka.listener.ListenerExecutionFailedException;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.kafka.support.KafkaNull;
|
||||
import org.springframework.kafka.support.converter.BatchMessageConverter;
|
||||
import org.springframework.kafka.support.converter.BatchMessagingMessageConverter;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -59,8 +58,6 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
public class BatchMessagingMessageListenerAdapter<K, V> extends MessagingMessageListenerAdapter<K, V>
|
||||
implements BatchAcknowledgingConsumerAwareMessageListener<K, V> {
|
||||
|
||||
private static final Message<KafkaNull> NULL_MESSAGE = new GenericMessage<>(KafkaNull.INSTANCE);
|
||||
|
||||
private BatchMessageConverter batchMessageConverter = new BatchMessagingMessageConverter();
|
||||
|
||||
private KafkaListenerErrorHandler errorHandler;
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.springframework.kafka.listener.ConsumerSeekAware;
|
||||
import org.springframework.kafka.listener.ListenerExecutionFailedException;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.kafka.support.KafkaHeaders;
|
||||
import org.springframework.kafka.support.KafkaNull;
|
||||
import org.springframework.kafka.support.KafkaUtils;
|
||||
import org.springframework.kafka.support.converter.MessagingMessageConverter;
|
||||
import org.springframework.kafka.support.converter.RecordMessageConverter;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -83,6 +85,11 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
|
||||
private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");
|
||||
|
||||
/**
|
||||
* Message used when no conversion is needed.
|
||||
*/
|
||||
protected static final Message<KafkaNull> NULL_MESSAGE = new GenericMessage<>(KafkaNull.INSTANCE); // NOSONAR
|
||||
|
||||
private final Object bean;
|
||||
|
||||
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); //NOSONAR
|
||||
@@ -99,6 +106,8 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
|
||||
private boolean isMessageList;
|
||||
|
||||
private boolean conversionNeeded = true;
|
||||
|
||||
private RecordMessageConverter messageConverter = new MessagingMessageConverter();
|
||||
|
||||
private Type fallbackType = Object.class;
|
||||
@@ -174,6 +183,10 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
return this.isConsumerRecords;
|
||||
}
|
||||
|
||||
public boolean isConversionNeeded() {
|
||||
return this.conversionNeeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the topic to which to send any result from the method invocation.
|
||||
* May be a SpEL expression {@code !{...}} evaluated at runtime.
|
||||
@@ -481,14 +494,26 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
|
||||
Type genericParameterType = null;
|
||||
int allowedBatchParameters = 1;
|
||||
int notConvertibleParameters = 0;
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
MethodParameter methodParameter = new MethodParameter(method, i);
|
||||
/*
|
||||
* We're looking for a single non-annotated parameter, or one annotated with @Payload.
|
||||
* We ignore parameters with type Message because they are not involved with conversion.
|
||||
* We ignore parameters with type Message, Consumer, Ack, ConsumerRecord because they
|
||||
* are not involved with conversion.
|
||||
*/
|
||||
if (eligibleParameter(methodParameter)
|
||||
Type parameterType = methodParameter.getGenericParameterType();
|
||||
boolean isNotConvertible = parameterIsType(parameterType, ConsumerRecord.class);
|
||||
boolean isAck = parameterIsType(parameterType, Acknowledgment.class);
|
||||
this.hasAckParameter |= isAck;
|
||||
isNotConvertible |= isAck;
|
||||
boolean isConsumer = parameterIsType(parameterType, Consumer.class);
|
||||
isNotConvertible |= isConsumer;
|
||||
if (isNotConvertible) {
|
||||
notConvertibleParameters++;
|
||||
}
|
||||
if (!isNotConvertible && !isMessageWithNoTypeInfo(parameterType)
|
||||
&& (methodParameter.getParameterAnnotations().length == 0
|
||||
|| methodParameter.hasParameterAnnotation(Payload.class))) {
|
||||
if (genericParameterType == null) {
|
||||
@@ -500,8 +525,7 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (methodParameter.getGenericParameterType().equals(Acknowledgment.class)) {
|
||||
this.hasAckParameter = true;
|
||||
else if (isAck) {
|
||||
allowedBatchParameters++;
|
||||
}
|
||||
else if (methodParameter.hasParameterAnnotation(Header.class)) {
|
||||
@@ -511,11 +535,10 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (methodParameter.getGenericParameterType().equals(Consumer.class)) {
|
||||
if (isConsumer) {
|
||||
allowedBatchParameters++;
|
||||
}
|
||||
else {
|
||||
Type parameterType = methodParameter.getGenericParameterType();
|
||||
if (parameterType instanceof ParameterizedType
|
||||
&& ((ParameterizedType) parameterType).getRawType().equals(Consumer.class)) {
|
||||
allowedBatchParameters++;
|
||||
@@ -524,6 +547,9 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
}
|
||||
}
|
||||
|
||||
if (notConvertibleParameters == method.getParameterCount()) {
|
||||
this.conversionNeeded = false;
|
||||
}
|
||||
boolean validParametersForBatch = method.getGenericParameterTypes().length <= allowedBatchParameters;
|
||||
|
||||
if (!validParametersForBatch) {
|
||||
@@ -587,27 +613,26 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
|
||||
&& ((WildcardType) paramType).getUpperBounds().length > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't consider parameter types that are available after conversion.
|
||||
* Acknowledgment, ConsumerRecord, Consumer, ConsumerRecord<...>, Consumer<...>, and Message<?>.
|
||||
*/
|
||||
private boolean eligibleParameter(MethodParameter methodParameter) {
|
||||
Type parameterType = methodParameter.getGenericParameterType();
|
||||
if (parameterType.equals(Acknowledgment.class) || parameterType.equals(ConsumerRecord.class)
|
||||
|| parameterType.equals(Consumer.class)) {
|
||||
return false;
|
||||
}
|
||||
private boolean isMessageWithNoTypeInfo(Type parameterType) {
|
||||
if (parameterType instanceof ParameterizedType) {
|
||||
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
|
||||
Type rawType = parameterizedType.getRawType();
|
||||
if (rawType.equals(ConsumerRecord.class) || rawType.equals(Consumer.class)) {
|
||||
return false;
|
||||
}
|
||||
else if (rawType.equals(Message.class)) {
|
||||
return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType);
|
||||
if (rawType.equals(Message.class)) {
|
||||
return parameterizedType.getActualTypeArguments()[0] instanceof WildcardType;
|
||||
}
|
||||
}
|
||||
return !parameterType.equals(Message.class); // could be Message without a generic type
|
||||
return parameterType.equals(Message.class); // could be Message without a generic type
|
||||
}
|
||||
|
||||
private boolean parameterIsType(Type parameterType, Type type) {
|
||||
if (parameterType instanceof ParameterizedType) {
|
||||
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
|
||||
Type rawType = parameterizedType.getRawType();
|
||||
if (rawType.equals(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return parameterType.equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.kafka.listener.KafkaListenerErrorHandler;
|
||||
import org.springframework.kafka.listener.ListenerExecutionFailedException;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
|
||||
/**
|
||||
@@ -71,8 +72,16 @@ public class RecordMessagingMessageListenerAdapter<K, V> extends MessagingMessag
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
|
||||
Message<?> message = toMessagingMessage(record, acknowledgment, consumer);
|
||||
logger.debug(() -> "Processing [" + message + "]");
|
||||
Message<?> message;
|
||||
if (isConversionNeeded()) {
|
||||
message = toMessagingMessage(record, acknowledgment, consumer);
|
||||
}
|
||||
else {
|
||||
message = NULL_MESSAGE;
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Processing [" + message + "]");
|
||||
}
|
||||
try {
|
||||
Object result = invokeHandler(record, acknowledgment, message, consumer);
|
||||
if (result != null) {
|
||||
@@ -82,6 +91,9 @@ public class RecordMessagingMessageListenerAdapter<K, V> extends MessagingMessag
|
||||
catch (ListenerExecutionFailedException e) { // NOSONAR ex flow control
|
||||
if (this.errorHandler != null) {
|
||||
try {
|
||||
if (message.equals(NULL_MESSAGE)) {
|
||||
message = new GenericMessage<>(record);
|
||||
}
|
||||
Object result = this.errorHandler.handleError(message, e, consumer);
|
||||
if (result != null) {
|
||||
handleResult(result, record, message);
|
||||
|
||||
@@ -27,6 +27,7 @@ import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -53,6 +54,7 @@ import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.clients.consumer.OffsetCommitCallback;
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
|
||||
import org.apache.kafka.common.serialization.ByteArraySerializer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -102,6 +104,7 @@ import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper;
|
||||
import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper.TypePrecedence;
|
||||
import org.springframework.kafka.support.converter.JsonMessageConverter;
|
||||
import org.springframework.kafka.support.converter.ProjectingMessageConverter;
|
||||
import org.springframework.kafka.support.converter.RecordMessageConverter;
|
||||
import org.springframework.kafka.support.converter.StringJsonMessageConverter;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
@@ -246,13 +249,13 @@ public class EnableKafkaIntegrationTests {
|
||||
template.send("annotated3", 0, "foo");
|
||||
template.flush();
|
||||
assertThat(this.listener.latch3.await(60, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.listener.record.value()).isEqualTo("foo");
|
||||
assertThat(this.listener.capturedRecord.value()).isEqualTo("foo");
|
||||
assertThat(this.config.listen3Exception).isNotNull();
|
||||
|
||||
template.send("annotated4", 0, "foo");
|
||||
template.flush();
|
||||
assertThat(this.listener.latch4.await(60, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.listener.record.value()).isEqualTo("foo");
|
||||
assertThat(this.listener.capturedRecord.value()).isEqualTo("foo");
|
||||
assertThat(this.listener.ack).isNotNull();
|
||||
assertThat(this.listener.eventLatch.await(60, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.listener.event.getListenerId().startsWith("qux-"));
|
||||
@@ -847,6 +850,37 @@ public class EnableKafkaIntegrationTests {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
|
||||
factoryWithBadConverter() {
|
||||
|
||||
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setRecordFilterStrategy(recordFilter());
|
||||
factory.setReplyTemplate(partitionZeroReplyingTemplate());
|
||||
factory.setErrorHandler((ConsumerAwareErrorHandler) (t, d, c) -> {
|
||||
this.globalErrorThrowable = t;
|
||||
c.seek(new org.apache.kafka.common.TopicPartition(d.topic(), d.partition()), d.offset());
|
||||
});
|
||||
factory.getContainerProperties().setMicrometerTags(Collections.singletonMap("extraTag", "foo"));
|
||||
factory.setMessageConverter(new RecordMessageConverter() {
|
||||
|
||||
@Override
|
||||
public Message<?> toMessage(ConsumerRecord<?, ?> record, Acknowledgment acknowledgment,
|
||||
Consumer<?, ?> consumer, Type payloadType) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProducerRecord<?, ?> fromMessage(Message<?> message, String defaultTopic) {
|
||||
throw new UnsupportedOperationException(); }
|
||||
|
||||
});
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
|
||||
withNoReplyTemplateContainerFactory() {
|
||||
@@ -1425,7 +1459,7 @@ public class EnableKafkaIntegrationTests {
|
||||
|
||||
volatile Integer partition;
|
||||
|
||||
volatile ConsumerRecord<?, ?> record;
|
||||
volatile ConsumerRecord<?, ?> capturedRecord;
|
||||
|
||||
volatile Acknowledgment ack;
|
||||
|
||||
@@ -1502,12 +1536,13 @@ public class EnableKafkaIntegrationTests {
|
||||
private final AtomicBoolean reposition3 = new AtomicBoolean();
|
||||
|
||||
@KafkaListener(id = "baz", topicPartitions = @TopicPartition(topic = "${topicThree:annotated3}",
|
||||
partitions = "${zero:0}"), errorHandler = "listen3ErrorHandler")
|
||||
partitions = "${zero:0}"), errorHandler = "listen3ErrorHandler",
|
||||
containerFactory = "factoryWithBadConverter")
|
||||
public void listen3(ConsumerRecord<?, ?> record) {
|
||||
if (this.reposition3.compareAndSet(false, true)) {
|
||||
throw new RuntimeException("reposition");
|
||||
}
|
||||
this.record = record;
|
||||
this.capturedRecord = record;
|
||||
this.latch3.countDown();
|
||||
}
|
||||
|
||||
@@ -1537,7 +1572,7 @@ public class EnableKafkaIntegrationTests {
|
||||
relativeToCurrent = "${zzz:true}"))
|
||||
}, clientIdPrefix = "${foo.xxx:clientIdViaAnnotation}")
|
||||
public void listen5(ConsumerRecord<?, ?> record) {
|
||||
this.record = record;
|
||||
this.capturedRecord = record;
|
||||
this.latch5.countDown();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user