org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.java

Source

/*
 * Copyright 2016-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.kafka.listener.adapter;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.common.TopicPartition;

import org.springframework.context.expression.MapAccessor;
import org.springframework.core.MethodParameter;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.kafka.core.KafkaTemplate;
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.KafkaUtils;
import org.springframework.kafka.support.converter.MessagingMessageConverter;
import org.springframework.kafka.support.converter.RecordMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
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.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * An abstract {@link org.springframework.kafka.listener.MessageListener} adapter
 * providing the necessary infrastructure to extract the payload of a
 * {@link org.springframework.messaging.Message}.
 *
 * @param <K> the key type.
 * @param <V> the value type.
 *
 * @author Stephane Nicoll
 * @author Gary Russell
 * @author Artem Bilan
 * @author Venil Noronha
 */
public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerSeekAware {

    private static final SpelExpressionParser PARSER = new SpelExpressionParser();

    private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");

    private final Object bean;

    protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); //NOSONAR

    private final Type inferredType;

    private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();

    private HandlerAdapter handlerMethod;

    private boolean isConsumerRecordList;

    private boolean isConsumerRecords;

    private boolean isMessageList;

    private RecordMessageConverter messageConverter = new MessagingMessageConverter();

    private Type fallbackType = Object.class;

    private Expression replyTopicExpression;

    @SuppressWarnings("rawtypes")
    private KafkaTemplate replyTemplate;

    private boolean hasAckParameter;

    private boolean messageReturnType;

    private ReplyHeadersConfigurer replyHeadersConfigurer;

    public MessagingMessageListenerAdapter(Object bean, Method method) {
        this.bean = bean;
        this.inferredType = determineInferredType(method); // NOSONAR = intentionally not final
    }

    /**
     * Set the MessageConverter.
     * @param messageConverter the converter.
     */
    public void setMessageConverter(RecordMessageConverter messageConverter) {
        this.messageConverter = messageConverter;
    }

    /**
     * Return the {@link MessagingMessageConverter} for this listener,
     * being able to convert {@link org.springframework.messaging.Message}.
     * @return the {@link MessagingMessageConverter} for this listener,
     * being able to convert {@link org.springframework.messaging.Message}.
     */
    protected final RecordMessageConverter getMessageConverter() {
        return this.messageConverter;
    }

    /**
     * Returns the inferred type for conversion or, if null, the
     * {@link #setFallbackType(Class) fallbackType}.
     * @return the type.
     */
    protected Type getType() {
        return this.inferredType == null ? this.fallbackType : this.inferredType;
    }

    /**
     * Set a fallback type to use when using a type-aware message converter and this
     * adapter cannot determine the inferred type from the method. An example of a
     * type-aware message converter is the {@code StringJsonMessageConverter}. Defaults to
     * {@link Object}.
     * @param fallbackType the type.
     */
    public void setFallbackType(Class<?> fallbackType) {
        this.fallbackType = fallbackType;
    }

    /**
     * Set the {@link HandlerAdapter} to use to invoke the method
     * processing an incoming {@link ConsumerRecord}.
     * @param handlerMethod {@link HandlerAdapter} instance.
     */
    public void setHandlerMethod(HandlerAdapter handlerMethod) {
        this.handlerMethod = handlerMethod;
    }

    protected boolean isConsumerRecordList() {
        return this.isConsumerRecordList;
    }

    public boolean isConsumerRecords() {
        return this.isConsumerRecords;
    }

    /**
     * Set the topic to which to send any result from the method invocation.
     * May be a SpEL expression {@code !{...}} evaluated at runtime.
     * @param replyTopicParam the topic or expression.
     * @since 2.0
     */
    public void setReplyTopic(String replyTopicParam) {
        String replyTopic = replyTopicParam;
        if (!StringUtils.hasText(replyTopic)) {
            replyTopic = PARSER_CONTEXT.getExpressionPrefix() + "source.headers['" + KafkaHeaders.REPLY_TOPIC + "']"
                    + PARSER_CONTEXT.getExpressionSuffix();
        }
        if (replyTopic.contains(PARSER_CONTEXT.getExpressionPrefix())) {
            this.replyTopicExpression = PARSER.parseExpression(replyTopic, PARSER_CONTEXT);
        } else {
            this.replyTopicExpression = new LiteralExpression(replyTopic);
        }

    }

    /**
     * Set the template to use to send any result from the method invocation.
     * @param replyTemplate the template.
     * @since 2.0
     */
    public void setReplyTemplate(KafkaTemplate<?, ?> replyTemplate) {
        this.replyTemplate = replyTemplate;
    }

    /**
     * Set a bean resolver for runtime SpEL expressions. Also configures the evaluation
     * context with a standard type converter and map accessor.
     * @param beanResolver the resolver.
     * @since 2.0
     */
    public void setBeanResolver(BeanResolver beanResolver) {
        this.evaluationContext.setBeanResolver(beanResolver);
        this.evaluationContext.setTypeConverter(new StandardTypeConverter());
        this.evaluationContext.addPropertyAccessor(new MapAccessor());
    }

    protected boolean isMessageList() {
        return this.isMessageList;
    }

    /**
     * Return the reply configurer.
     * @return the configurer.
     * @since 2.2
     * @see #setReplyHeadersConfigurer(ReplyHeadersConfigurer)
     */
    protected ReplyHeadersConfigurer getReplyHeadersConfigurer() {
        return this.replyHeadersConfigurer;
    }

    /**
     * Set a configurer which will be invoked when creating a reply message.
     * @param replyHeadersConfigurer the configurer.
     * @since 2.2
     */
    public void setReplyHeadersConfigurer(ReplyHeadersConfigurer replyHeadersConfigurer) {
        this.replyHeadersConfigurer = replyHeadersConfigurer;
    }

    @Override
    public void registerSeekCallback(ConsumerSeekCallback callback) {
        if (this.bean instanceof ConsumerSeekAware) {
            ((ConsumerSeekAware) this.bean).registerSeekCallback(callback);
        }
    }

    @Override
    public void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
        if (this.bean instanceof ConsumerSeekAware) {
            ((ConsumerSeekAware) this.bean).onPartitionsAssigned(assignments, callback);
        }
    }

    @Override
    public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
        if (this.bean instanceof ConsumerSeekAware) {
            ((ConsumerSeekAware) this.bean).onPartitionsRevoked(partitions);
        }
    }

    @Override
    public void onIdleContainer(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
        if (this.bean instanceof ConsumerSeekAware) {
            ((ConsumerSeekAware) this.bean).onIdleContainer(assignments, callback);
        }
    }

    protected Message<?> toMessagingMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment,
            Consumer<?, ?> consumer) {
        return getMessageConverter().toMessage(record, acknowledgment, consumer, getType());
    }

    /**
     * Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException}
     * with a dedicated error message.
     * @param data the data to process during invocation.
     * @param acknowledgment the acknowledgment to use if any.
     * @param message the message to process.
     * @param consumer the consumer.
     * @return the result of invocation.
     */
    protected final Object invokeHandler(Object data, Acknowledgment acknowledgment, Message<?> message,
            Consumer<?, ?> consumer) {
        try {
            if (data instanceof List && !this.isConsumerRecordList) {
                return this.handlerMethod.invoke(message, acknowledgment, consumer);
            } else {
                return this.handlerMethod.invoke(message, data, acknowledgment, consumer);
            }
        } catch (org.springframework.messaging.converter.MessageConversionException ex) {
            if (this.hasAckParameter && acknowledgment == null) {
                throw new ListenerExecutionFailedException("invokeHandler Failed",
                        new IllegalStateException("No Acknowledgment available as an argument, "
                                + "the listener container must have a MANUAL AckMode to populate the Acknowledgment.",
                                ex));
            }
            throw new ListenerExecutionFailedException(createMessagingErrorMessage(
                    "Listener method could not " + "be invoked with the incoming message", message.getPayload()),
                    new MessageConversionException("Cannot handle message", ex));
        } catch (MessagingException ex) {
            throw new ListenerExecutionFailedException(createMessagingErrorMessage(
                    "Listener method could not " + "be invoked with the incoming message", message.getPayload()),
                    ex);
        } catch (Exception ex) {
            throw new ListenerExecutionFailedException("Listener method '"
                    + this.handlerMethod.getMethodAsString(message.getPayload()) + "' threw exception", ex);
        }
    }

    /**
     * Handle the given result object returned from the listener method, sending a
     * response message to the SendTo topic.
     * @param resultArg the result object to handle (never <code>null</code>)
     * @param request the original request message
     * @param source the source data for the method invocation - e.g.
     * {@code o.s.messaging.Message<?>}; may be null
     */
    protected void handleResult(Object resultArg, Object request, Object source) {
        this.logger.debug(
                () -> "Listener method returned result [" + resultArg + "] - generating response message for it");
        boolean isInvocationResult = resultArg instanceof InvocationResult;
        Object result = isInvocationResult ? ((InvocationResult) resultArg).getResult() : resultArg;
        String replyTopic = evaluateReplyTopic(request, source, resultArg);
        Assert.state(replyTopic == null || this.replyTemplate != null,
                "a KafkaTemplate is required to support replies");
        sendResponse(result, replyTopic, source,
                isInvocationResult ? ((InvocationResult) resultArg).isMessageReturnType() : this.messageReturnType);
    }

    private String evaluateReplyTopic(Object request, Object source, Object result) {
        String replyTo = null;
        if (result instanceof InvocationResult) {
            replyTo = evaluateTopic(request, source, result, ((InvocationResult) result).getSendTo());
        } else if (this.replyTopicExpression != null) {
            replyTo = evaluateTopic(request, source, result, this.replyTopicExpression);
        }
        return replyTo;
    }

    private String evaluateTopic(Object request, Object source, Object result, Expression sendTo) {
        if (sendTo instanceof LiteralExpression) {
            return sendTo.getValue(String.class);
        } else {
            Object value = sendTo == null ? null
                    : sendTo.getValue(this.evaluationContext, new ReplyExpressionRoot(request, source, result));
            boolean isByteArray = value instanceof byte[];
            if (!(value == null || value instanceof String || isByteArray)) {
                throw new IllegalStateException("replyTopic expression must evaluate to a String or byte[], it is: "
                        + value.getClass().getName());
            }
            if (isByteArray) {
                return new String((byte[]) value, StandardCharsets.UTF_8);
            }
            return (String) value;
        }
    }

    /**
     * Send the result to the topic.
     *
     * @param result the result.
     * @param topic the topic.
     * @deprecated in favor of {@link #sendResponse(Object, String, Object, boolean)}.
     */
    @Deprecated
    protected void sendResponse(Object result, String topic) {
        sendResponse(result, topic, null, false);
    }

    /**
     * Send the result to the topic.
     *
     * @param result the result.
     * @param topic the topic.
     * @param source the source (input).
     * @param returnTypeMessage true if we are returning message(s).
     * @since 2.1.3
     */
    @SuppressWarnings("unchecked")
    protected void sendResponse(Object result, String topic, @Nullable Object source, boolean returnTypeMessage) {
        if (!returnTypeMessage && topic == null) {
            this.logger.debug(() -> "No replyTopic to handle the reply: " + result);
        } else if (result instanceof Message) {
            this.replyTemplate.send((Message<?>) result);
        } else {
            if (result instanceof Collection) {
                ((Collection<V>) result).forEach(v -> {
                    if (v instanceof Message) {
                        this.replyTemplate.send((Message<?>) v);
                    } else {
                        this.replyTemplate.send(topic, v);
                    }
                });
            } else {
                sendSingleResult(result, topic, source);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void sendSingleResult(Object result, String topic, @Nullable Object source) {
        byte[] correlationId = null;
        boolean sourceIsMessage = source instanceof Message;
        if (sourceIsMessage && ((Message<?>) source).getHeaders().get(KafkaHeaders.CORRELATION_ID) != null) {
            correlationId = ((Message<?>) source).getHeaders().get(KafkaHeaders.CORRELATION_ID, byte[].class);
        }
        if (sourceIsMessage) {
            sendReplyForMessageSource(result, topic, source, correlationId);
        } else {
            this.replyTemplate.send(topic, result);
        }
    }

    @SuppressWarnings("unchecked")
    private void sendReplyForMessageSource(Object result, String topic, Object source, byte[] correlationId) {
        MessageBuilder<Object> builder = MessageBuilder.withPayload(result).setHeader(KafkaHeaders.TOPIC, topic);
        if (this.replyHeadersConfigurer != null) {
            Map<String, Object> headersToCopy = ((Message<?>) source).getHeaders().entrySet().stream().filter(e -> {
                String key = e.getKey();
                return !key.equals(MessageHeaders.ID) && !key.equals(MessageHeaders.TIMESTAMP)
                        && !key.equals(KafkaHeaders.CORRELATION_ID) && !key.startsWith(KafkaHeaders.RECEIVED);
            }).filter(e -> this.replyHeadersConfigurer.shouldCopy(e.getKey(), e.getValue()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
            if (headersToCopy.size() > 0) {
                builder.copyHeaders(headersToCopy);
            }
            headersToCopy = this.replyHeadersConfigurer.additionalHeaders();
            if (!ObjectUtils.isEmpty(headersToCopy)) {
                builder.copyHeaders(headersToCopy);
            }
        }
        if (correlationId != null) {
            builder.setHeader(KafkaHeaders.CORRELATION_ID, correlationId);
        }
        setPartition(builder, ((Message<?>) source));
        this.replyTemplate.send(builder.build());
    }

    private void setPartition(MessageBuilder<Object> builder, Message<?> source) {
        byte[] partitionBytes = source.getHeaders().get(KafkaHeaders.REPLY_PARTITION, byte[].class);
        if (partitionBytes != null) {
            builder.setHeader(KafkaHeaders.PARTITION_ID, ByteBuffer.wrap(partitionBytes).getInt());
        }
    }

    protected final String createMessagingErrorMessage(String description, Object payload) {
        return description + "\n" + "Endpoint handler details:\n" + "Method ["
                + this.handlerMethod.getMethodAsString(payload) + "]\n" + "Bean [" + this.handlerMethod.getBean()
                + "]";
    }

    /**
     * Subclasses can override this method to use a different mechanism to determine
     * the target type of the payload conversion.
     * @param method the method.
     * @return the type.
     */
    protected Type determineInferredType(Method method) { // NOSONAR complexity
        if (method == null) {
            return null;
        }

        Type genericParameterType = null;
        int allowedBatchParameters = 1;

        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.
             */
            if (eligibleParameter(methodParameter) && (methodParameter.getParameterAnnotations().length == 0
                    || methodParameter.hasParameterAnnotation(Payload.class))) {
                if (genericParameterType == null) {
                    genericParameterType = extractGenericParameterTypFromMethodParameter(methodParameter);
                } else {
                    this.logger.debug(() -> "Ambiguous parameters for target payload for method " + method
                            + "; no inferred type available");
                    break;
                }
            } else if (methodParameter.getGenericParameterType().equals(Acknowledgment.class)) {
                this.hasAckParameter = true;
                allowedBatchParameters++;
            } else if (methodParameter.hasParameterAnnotation(Header.class)) {
                Header header = methodParameter.getParameterAnnotation(Header.class);
                if (header != null && KafkaHeaders.GROUP_ID.equals(header.value())) {
                    allowedBatchParameters++;
                }
            } else {
                if (methodParameter.getGenericParameterType().equals(Consumer.class)) {
                    allowedBatchParameters++;
                } else {
                    Type parameterType = methodParameter.getGenericParameterType();
                    if (parameterType instanceof ParameterizedType
                            && ((ParameterizedType) parameterType).getRawType().equals(Consumer.class)) {
                        allowedBatchParameters++;
                    }
                }
            }
        }

        boolean validParametersForBatch = method.getGenericParameterTypes().length <= allowedBatchParameters;

        if (!validParametersForBatch) {
            String stateMessage = "A parameter of type '%s' must be the only parameter "
                    + "(except for an optional 'Acknowledgment' and/or 'Consumer' "
                    + "and/or '@Header(KafkaHeaders.GROUP_ID) String groupId'";
            Assert.state(!this.isConsumerRecords, () -> String.format(stateMessage, "ConsumerRecords"));
            Assert.state(!this.isConsumerRecordList, () -> String.format(stateMessage, "List<ConsumerRecord>"));
            Assert.state(!this.isMessageList, () -> String.format(stateMessage, "List<Message<?>>"));
        }
        this.messageReturnType = KafkaUtils.returnTypeMessageOrCollectionOf(method);
        return genericParameterType;
    }

    private Type extractGenericParameterTypFromMethodParameter(MethodParameter methodParameter) {
        Type genericParameterType = methodParameter.getGenericParameterType();
        if (genericParameterType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
            if (parameterizedType.getRawType().equals(Message.class)) {
                genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0];
            } else if (parameterizedType.getRawType().equals(List.class)
                    && parameterizedType.getActualTypeArguments().length == 1) {

                Type paramType = parameterizedType.getActualTypeArguments()[0];
                this.isConsumerRecordList = paramType.equals(ConsumerRecord.class)
                        || (isSimpleListOfConsumerRecord(paramType)
                                || isListOfConsumerRecordUpperBounded(paramType));
                boolean messageHasGeneric = paramType instanceof ParameterizedType
                        && ((ParameterizedType) paramType).getRawType().equals(Message.class);
                this.isMessageList = paramType.equals(Message.class) || messageHasGeneric;
                if (messageHasGeneric) {
                    genericParameterType = ((ParameterizedType) paramType).getActualTypeArguments()[0];
                }
            } else {
                this.isConsumerRecords = parameterizedType.getRawType().equals(ConsumerRecords.class);
            }
        }
        return genericParameterType;
    }

    private boolean isSimpleListOfConsumerRecord(Type paramType) {
        return paramType instanceof ParameterizedType
                && ((ParameterizedType) paramType).getRawType().equals(ConsumerRecord.class);
    }

    private boolean isListOfConsumerRecordUpperBounded(Type paramType) {
        return isWildCardWithUpperBound(paramType)
                && ((WildcardType) paramType).getUpperBounds()[0] instanceof ParameterizedType
                && ((ParameterizedType) ((WildcardType) paramType).getUpperBounds()[0]).getRawType()
                        .equals(ConsumerRecord.class);
    }

    private boolean isWildCardWithUpperBound(Type paramType) {
        return paramType instanceof WildcardType && ((WildcardType) paramType).getUpperBounds() != null
                && ((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;
        }
        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);
            }
        }
        return !parameterType.equals(Message.class); // could be Message without a generic type
    }

    /**
     * Root object for reply expression evaluation.
     * @since 2.0
     */
    public static final class ReplyExpressionRoot {

        private final Object request;

        private final Object source;

        private final Object result;

        public ReplyExpressionRoot(Object request, Object source, Object result) {
            this.request = request;
            this.source = source;
            this.result = result;
        }

        public Object getRequest() {
            return this.request;
        }

        public Object getSource() {
            return this.source;
        }

        public Object getResult() {
            return this.result;
        }

    }

}