Added support for Spring Kafka

fixes gh-877
This commit is contained in:
Marcin Grzejszczak
2019-09-16 22:37:08 +02:00
parent 582503a8c9
commit 4d45bdf31d
24 changed files with 1639 additions and 19 deletions

View File

@@ -75,14 +75,7 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
Assert.isTrue(
mockingDetails(rabbitTemplate).isSpy()
|| mockingDetails(rabbitTemplate).isMock(),
"StubRunner AMQP will work only if RabbiTemplate is a spy"); // we get
// send
// messages
// by
// capturing
// arguments
// on the
// spy
"StubRunner AMQP will work only if RabbiTemplate is a spy");
this.rabbitTemplate = rabbitTemplate;
this.messageListenerAccessor = messageListenerAccessor;
}
@@ -95,14 +88,7 @@ public class SpringAmqpStubMessages implements MessageVerifier<Message> {
Assert.isTrue(
mockingDetails(rabbitTemplate).isSpy()
|| mockingDetails(rabbitTemplate).isMock(),
"StubRunner AMQP will work only if RabbiTemplate is a spy"); // we get
// send
// messages
// by
// capturing
// arguments
// on the
// spy
"StubRunner AMQP will work only if RabbiTemplate is a spy");
this.rabbitTemplate = rabbitTemplate;
this.messageListenerAccessor = messageListenerAccessor;
this.rabbitProperties = rabbitProperties;

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-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.cloud.contract.verifier.messaging.kafka;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.messaging.Message;
/**
* @author Marcin Grzejszczak
*/
@Configuration
@ConditionalOnClass({ KafkaTemplate.class, EmbeddedKafkaBroker.class })
@ConditionalOnProperty(name = "stubrunner.kafka.enabled", havingValue = "true",
matchIfMissing = true)
@AutoConfigureBefore({ ContractVerifierIntegrationConfiguration.class,
NoOpContractVerifierAutoConfiguration.class })
@ConditionalOnBean(EmbeddedKafkaBroker.class)
public class ContractVerifierKafkaConfiguration {
@Bean
@ConditionalOnMissingBean
MessageVerifier<Message<?>> contractVerifierKafkaMessageExchange(
KafkaTemplate kafkaTemplate, EmbeddedKafkaBroker broker,
KafkaProperties kafkaProperties, KafkaStubMessagesInitializer initializer) {
return new KafkaStubMessages(kafkaTemplate, broker, kafkaProperties, initializer);
}
@Bean
@ConditionalOnMissingBean
KafkaStubMessagesInitializer contractVerifierKafkaStubMessagesInitializer() {
return new ContractVerifierKafkaStubMessagesInitializer();
}
@Bean
@ConditionalOnMissingBean
ContractVerifierMessaging<Message<?>> contractVerifierKafkaMessaging(
MessageVerifier<Message<?>> exchange) {
return new ContractVerifierKafkaHelper(exchange);
}
}
class ContractVerifierKafkaHelper extends ContractVerifierMessaging<Message<?>> {
ContractVerifierKafkaHelper(MessageVerifier<Message<?>> exchange) {
super(exchange);
}
@Override
protected ContractVerifierMessage convert(Message<?> message) {
return new ContractVerifierMessage(message.getPayload(), message.getHeaders());
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013-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.cloud.contract.verifier.messaging.kafka;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.utils.KafkaTestUtils;
class ContractVerifierKafkaStubMessagesInitializer
implements KafkaStubMessagesInitializer {
private static final Log log = LogFactory
.getLog(ContractVerifierKafkaStubMessagesInitializer.class);
@Override
public Map<String, Consumer> initialize(EmbeddedKafkaBroker broker,
KafkaProperties kafkaProperties) {
Map<String, Consumer> map = new HashMap<>();
for (String topic : broker.getTopics()) {
map.put(topic, prepareListener(broker, topic, kafkaProperties));
}
return map;
}
private Consumer prepareListener(EmbeddedKafkaBroker broker, String destination,
KafkaProperties kafkaProperties) {
Map<String, Object> consumerProperties = KafkaTestUtils.consumerProps(
kafkaProperties.getConsumer().getGroupId(), "false", broker);
consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
DefaultKafkaConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<>(
consumerProperties);
Consumer<String, String> consumer = consumerFactory.createConsumer();
broker.consumeFromAnEmbeddedTopic(consumer, destination);
if (log.isDebugEnabled()) {
log.debug("Prepared consumer for destination [" + destination + "]");
}
return consumer;
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2013-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.cloud.contract.verifier.messaging.kafka;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import net.minidev.json.JSONObject;
import net.minidev.json.parser.JSONParser;
import net.minidev.json.parser.ParseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.header.Headers;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
class KafkaStubMessages implements MessageVerifier<Message<?>> {
private static final Log log = LogFactory.getLog(KafkaStubMessages.class);
private final KafkaTemplate kafkaTemplate;
private final Receiver receiver;
KafkaStubMessages(KafkaTemplate kafkaTemplate, EmbeddedKafkaBroker broker,
KafkaProperties kafkaProperties, KafkaStubMessagesInitializer initializer) {
this.kafkaTemplate = kafkaTemplate;
Map<String, Consumer> topicToConsumer = initializer.initialize(broker,
kafkaProperties);
this.receiver = new Receiver(topicToConsumer);
}
@Override
public void send(Message<?> message, String destination) {
String defaultTopic = this.kafkaTemplate.getDefaultTopic();
try {
this.kafkaTemplate.setDefaultTopic(destination);
if (log.isDebugEnabled()) {
log.debug("Will send a message [" + message + "] to destination ["
+ destination + "]");
}
this.kafkaTemplate.send(message).get(5, TimeUnit.SECONDS);
this.kafkaTemplate.flush();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
finally {
this.kafkaTemplate.setDefaultTopic(defaultTopic);
}
}
@Override
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
return this.receiver.receive(destination, timeout, timeUnit);
}
@Override
public Message receive(String destination) {
return receive(destination, 5, TimeUnit.SECONDS);
}
@Override
public void send(Object payload, Map headers, String destination) {
Message<?> message = MessageBuilder.createMessage(payload,
new MessageHeaders(headers));
send(message, destination);
}
}
class Receiver {
private final Map<String, Consumer> consumers;
private static final Log log = LogFactory.getLog(Receiver.class);
Receiver(Map<String, Consumer> consumers) {
this.consumers = consumers;
}
Message receive(String topic, long timeout, TimeUnit timeUnit) {
Consumer consumer = this.consumers.get(topic);
if (consumer == null) {
throw new IllegalStateException(
"No consumer set up for topic [" + topic + "]");
}
ConsumerRecord<String, String> record = KafkaTestUtils.getSingleRecord(consumer,
topic, timeUnit.toMillis(timeout));
if (log.isDebugEnabled()) {
log.debug("Got a single record for destination [" + topic + "]");
}
return new Record(record).toMessage();
}
}
class Record {
private final ConsumerRecord record;
Record(ConsumerRecord record) {
this.record = record;
}
private Map<String, Object> toMap(Headers headers) {
Map<String, Object> map = new HashMap<>();
for (Header header : headers) {
map.put(header.key(), header.value());
}
return map;
}
Message toMessage() {
Object textPayload = record.value();
// sometimes it's a message sometimes just payload
MessageHeaders headers = new MessageHeaders(toMap(record.headers()));
if (textPayload instanceof String && ((String) textPayload).contains("payload")
&& ((String) textPayload).contains("headers")) {
try {
Object object = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE)
.parse((String) textPayload);
JSONObject jo = (JSONObject) object;
String payload = (String) jo.get("payload");
JSONObject headersInJson = (JSONObject) jo.get("headers");
Map newHeaders = new HashMap(headers);
newHeaders.putAll(headersInJson);
return MessageBuilder.createMessage(unquoted(payload),
new MessageHeaders(newHeaders));
}
catch (ParseException ex) {
throw new IllegalStateException(ex);
}
}
return MessageBuilder.createMessage(unquoted(textPayload), headers);
}
private Object unquoted(Object value) {
String textPayload = value instanceof byte[] ? new String((byte[]) value)
: value.toString();
if (textPayload.startsWith("\"") && textPayload.endsWith("\"")) {
return textPayload.substring(1, textPayload.length() - 1).replace("\\\"",
"\"");
}
return textPayload;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-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.cloud.contract.verifier.messaging.kafka;
import java.util.Map;
import org.apache.kafka.clients.consumer.Consumer;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
/**
* Logic used to initialize {@link KafkaStubMessages}. This interface might have a
* different implementation for the producer side and for the consumer side. That's
* because you can't poll for a single message by different consumers.
*
* @author Marcin Grzejszczak
* @since 2.2.0
*/
public interface KafkaStubMessagesInitializer {
/**
* @param broker - embedded Kafka broker
* @param kafkaProperties - kafka properties
* @return topic to initialized consumer mapping
*/
Map<String, Consumer> initialize(EmbeddedKafkaBroker broker,
KafkaProperties kafkaProperties);
}

View File

@@ -6,4 +6,5 @@ org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpA
org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration,\
org.springframework.cloud.contract.verifier.messaging.jms.ContractVerifierJmsConfiguration,\
org.springframework.cloud.contract.verifier.messaging.kafka.ContractVerifierKafkaConfiguration,\
org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration