I am doing a POC with Spring Boot & Kafka for a transactional project and I have the following doubt:
Scenario: One microservices MSPUB
One way to do it is to use a Spring Integration BarrierMessageHandler.
Here is an example app. Hopefully, it's self-explanatory. Kafka 0.11 or higher is needed...
@SpringBootApplication
@RestController
public class So48349993Application {
private static final Logger logger = LoggerFactory.getLogger(So48349993Application.class);
private static final String TRANSACTION_TOPIC1 = "TRANSACTION_TOPIC1";
private static final String TRANSACTION_RESULT1 = "TRANSACTION_RESULT1";
public static void main(String[] args) {
SpringApplication.run(So48349993Application.class, args);
}
private final Exchanger exchanger;
private final KafkaTemplate kafkaTemplate;
@Autowired
public So48349993Application(Exchanger exchanger,
KafkaTemplate kafkaTemplate) {
this.exchanger = exchanger;
this.kafkaTemplate = kafkaTemplate;
kafkaTemplate.setDefaultTopic(TRANSACTION_RESULT1);
}
@RequestMapping(path = "/foo/{id}/{other}", method = RequestMethod.GET)
@ResponseBody
public String foo(@PathVariable String id, @PathVariable String other) {
logger.info("Controller received: " + other);
String reply = this.exchanger.exchange(id, other);
// if reply is null, we timed out
logger.info("Controller replying: " + reply);
return reply;
}
// Client side
@MessagingGateway(defaultRequestChannel = "outbound", defaultReplyTimeout = "10000")
public interface Exchanger {
@Gateway
String exchange(@Header(IntegrationMessageHeaderAccessor.CORRELATION_ID) String id,
@Payload String out);
}
@Bean
public IntegrationFlow router() {
return IntegrationFlows.from("outbound")
.routeToRecipients(r -> r
.recipient("toKafka")
.recipient("barrierChannel"))
.get();
}
@Bean
public IntegrationFlow outFlow(KafkaTemplate kafkaTemplate) {
return IntegrationFlows.from("toKafka")
.handle(Kafka.outboundChannelAdapter(kafkaTemplate).topic(TRANSACTION_TOPIC1))
.get();
}
@Bean
public IntegrationFlow barrierFlow(BarrierMessageHandler barrier) {
return IntegrationFlows.from("barrierChannel")
.handle(barrier)
.transform("payload.get(1)") // payload is list with input/reply
.get();
}
@Bean
public BarrierMessageHandler barrier() {
return new BarrierMessageHandler(10_000L);
}
@KafkaListener(id = "clientReply", topics = TRANSACTION_RESULT1)
public void result(Message> reply) {
logger.info("Received reply: " + reply.getPayload() + " for id "
+ reply.getHeaders().get(IntegrationMessageHeaderAccessor.CORRELATION_ID));
barrier().trigger(reply);
}
// Server side
@KafkaListener(id = "server", topics = TRANSACTION_TOPIC1)
public void service(String in,
@Header(IntegrationMessageHeaderAccessor.CORRELATION_ID) String id) throws InterruptedException {
logger.info("Service Received " + in);
Thread.sleep(5_000);
logger.info("Service Replying to " + in);
// with spring-kafka 2.0 (and Boot 2), you can return a String and use @SendTo instead of this.
this.kafkaTemplate.send(new GenericMessage<>("reply for " + in,
Collections.singletonMap(IntegrationMessageHeaderAccessor.CORRELATION_ID, id)));
}
// Provision topics if needed
// provided by Boot in 2.0
@Bean
public KafkaAdmin admin() {
Map config = new HashMap<>();
config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
return new KafkaAdmin(config);
}
@Bean
public NewTopic topic1() {
return new NewTopic(TRANSACTION_TOPIC1, 10, (short) 1);
}
@Bean
public NewTopic result1() {
return new NewTopic(TRANSACTION_RESULT1, 10, (short) 1);
}
}
Result
2018-01-20 17:27:54.668 INFO 98522 --- [ server-1-C-1] com.example.So48349993Application : Service Received foo
2018-01-20 17:27:55.782 INFO 98522 --- [nio-8080-exec-2] com.example.So48349993Application : Controller received: bar
2018-01-20 17:27:55.788 INFO 98522 --- [ server-0-C-1] com.example.So48349993Application : Service Received bar
2018-01-20 17:27:59.673 INFO 98522 --- [ server-1-C-1] com.example.So48349993Application : Service Replying to foo
2018-01-20 17:27:59.702 INFO 98522 --- [ientReply-1-C-1] com.example.So48349993Application : Received reply: reply for foo for id 1
2018-01-20 17:27:59.705 INFO 98522 --- [nio-8080-exec-1] com.example.So48349993Application : Controller replying: reply for foo
2018-01-20 17:28:00.792 INFO 98522 --- [ server-0-C-1] com.example.So48349993Application : Service Replying to bar
2018-01-20 17:28:00.798 INFO 98522 --- [ientReply-0-C-1] com.example.So48349993Application : Received reply: reply for bar for id 2
2018-01-20 17:28:00.800 INFO 98522 --- [nio-8080-exec-2] com.example.So48349993Application : Controller replying: reply for bar
Pom
4.0.0
com.example
so48349993
0.0.1-SNAPSHOT
jar
so48349993
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-integration
org.springframework.integration
spring-integration-kafka
2.3.0.RELEASE
org.springframework.kafka
spring-kafka
1.3.2.RELEASE
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
application.properties
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.listener.concurrency=2
EDIT
And here is a version that uses Spring Integration on the server side, instead of @KafkaListener
...
@SpringBootApplication
@RestController
public class So483499931Application {
private static final Logger logger = LoggerFactory.getLogger(So483499931Application.class);
private static final String TRANSACTION_TOPIC1 = "TRANSACTION_TOPIC3";
private static final String TRANSACTION_RESULT1 = "TRANSACTION_RESULT3";
public static void main(String[] args) {
SpringApplication.run(So483499931Application.class, args);
}
private final Exchanger exchanger;
private final KafkaTemplate kafkaTemplate;
@Autowired
public So483499931Application(Exchanger exchanger,
KafkaTemplate kafkaTemplate) {
this.exchanger = exchanger;
this.kafkaTemplate = kafkaTemplate;
kafkaTemplate.setDefaultTopic(TRANSACTION_RESULT1);
}
@RequestMapping(path = "/foo/{id}/{other}", method = RequestMethod.GET)
@ResponseBody
public String foo(@PathVariable String id, @PathVariable String other) {
logger.info("Controller received: " + other);
String reply = this.exchanger.exchange(id, other);
logger.info("Controller replying: " + reply);
return reply;
}
// Client side
@MessagingGateway(defaultRequestChannel = "outbound", defaultReplyTimeout = "10000")
public interface Exchanger {
@Gateway
String exchange(@Header(IntegrationMessageHeaderAccessor.CORRELATION_ID) String id,
@Payload String out);
}
@Bean
public IntegrationFlow router() {
return IntegrationFlows.from("outbound")
.routeToRecipients(r -> r
.recipient("toKafka")
.recipient("barrierChannel"))
.get();
}
@Bean
public IntegrationFlow outFlow(KafkaTemplate kafkaTemplate) {
return IntegrationFlows.from("toKafka")
.handle(Kafka.outboundChannelAdapter(kafkaTemplate).topic(TRANSACTION_TOPIC1))
.get();
}
@Bean
public IntegrationFlow barrierFlow(BarrierMessageHandler barrier) {
return IntegrationFlows.from("barrierChannel")
.handle(barrier)
.transform("payload.get(1)") // payload is list with input/reply
.get();
}
@Bean
public BarrierMessageHandler barrier() {
return new BarrierMessageHandler(10_000L);
}
@KafkaListener(id = "clientReply", topics = TRANSACTION_RESULT1)
public void result(Message> reply) {
logger.info("Received reply: " + reply.getPayload() + " for id "
+ reply.getHeaders().get(IntegrationMessageHeaderAccessor.CORRELATION_ID));
barrier().trigger(reply);
}
// Server side
@Bean
public IntegrationFlow server(ConsumerFactory consumerFactory,
KafkaTemplate kafkaTemplate) {
return IntegrationFlows.from(Kafka.messageDrivenChannelAdapter(consumerFactory, TRANSACTION_TOPIC1))
.handle("so483499931Application", "service")
.handle(Kafka.outboundChannelAdapter(kafkaTemplate).topic(TRANSACTION_RESULT1))
.get();
}
public String service(String in) throws InterruptedException {
logger.info("Service Received " + in);
Thread.sleep(5_000);
logger.info("Service Replying to " + in);
return "reply for " + in;
}
// Provision topics if needed
// provided by Boot in 2.0
@Bean
public KafkaAdmin admin() {
Map config = new HashMap<>();
config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
return new KafkaAdmin(config);
}
@Bean
public NewTopic topic1() {
return new NewTopic(TRANSACTION_TOPIC1, 10, (short) 1);
}
@Bean
public NewTopic result1() {
return new NewTopic(TRANSACTION_RESULT1, 10, (short) 1);
}
}
and
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.listener.concurrency=2
spring.kafka.consumer.group-id=server