├── .gitignore ├── src ├── main │ ├── resources │ │ └── application.properties │ └── java │ │ └── com │ │ └── cbwleft │ │ ├── Application.java │ │ └── rabbit │ │ ├── sync │ │ ├── RPCServer.java │ │ └── RPCClient.java │ │ ├── async │ │ ├── AsyncTask.java │ │ ├── AsyncRPCClient.java │ │ └── AsyncRPCServer.java │ │ └── RabbitConfig.java └── test │ └── java │ └── com │ └── cbwleft │ └── rabbit │ ├── sync │ └── RPCClientTest.java │ └── async │ └── AsyncRPCClientTest.java ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.classpath 3 | /.project 4 | /.settings -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.rabbitmq.host=localhost 2 | spring.rabbitmq.port=5672 3 | spring.rabbitmq.username=guest 4 | spring.rabbitmq.password=guest -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/Application.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/sync/RPCServer.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.sync; 2 | 3 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 4 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 5 | import org.springframework.stereotype.Component; 6 | 7 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_SYNC_RPC; 8 | 9 | @Component 10 | @RabbitListener(queues = QUEUE_SYNC_RPC) 11 | public class RPCServer { 12 | 13 | @RabbitHandler 14 | public String process(String message) { 15 | int millis = (int) (Math.random() * 2 * 1000); 16 | try { 17 | Thread.sleep(millis); 18 | } catch (InterruptedException e) { 19 | } 20 | return message + " sleep for " + millis + " ms"; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/async/AsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.async; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.scheduling.annotation.AsyncResult; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.concurrent.ListenableFuture; 9 | 10 | @Component 11 | public class AsyncTask { 12 | 13 | Logger logger = LoggerFactory.getLogger(getClass()); 14 | 15 | @Async 16 | public ListenableFuture expensiveOperation(String message) { 17 | int millis = (int) (Math.random() * 5 * 1000); 18 | try { 19 | Thread.sleep(millis); 20 | } catch (InterruptedException e) { 21 | } 22 | String result = message + " executed by " + Thread.currentThread().getName() + " for " + millis + " ms"; 23 | return new AsyncResult(result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/sync/RPCClient.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.sync; 2 | 3 | import org.springframework.amqp.core.AmqpTemplate; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.scheduling.annotation.AsyncResult; 7 | import org.springframework.stereotype.Component; 8 | 9 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_SYNC_RPC; 10 | 11 | import java.util.concurrent.Future; 12 | 13 | @Component 14 | public class RPCClient { 15 | 16 | @Autowired 17 | AmqpTemplate amqpTemplate; 18 | 19 | public String send(String message) { 20 | String result = (String) amqpTemplate.convertSendAndReceive(QUEUE_SYNC_RPC, message); 21 | return result; 22 | } 23 | 24 | @Async 25 | public Future sendAsync(String message) { 26 | return new AsyncResult(send(message)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-amqp-rpc 2 | 使用Spring Boot(1.5.9)+Spring AMQP+RabbitMQ实现RPC的demo 3 | 4 | ## 为何要使用MQ实现RPC 5 | 1. 相比于http接口实现的RPC,MQ实现的RPC客户端不需要知道RPC服务端的存在。 6 | 2. MQ实现的RPC服务端高可用,只需要简单地启动多个RPC服务即可,不需要额外的服务注册发现以及负载均衡。 7 | 3. 如果原有的MQ的普通消息需要知道执行结果,可以很方便地切换到RPC模式。 8 | 4. RabbitMQ RPC的工作方式非常擅长处理异步回调式的任务 9 | 10 | ## 为何要写这个demo 11 | Spring AMQP的官方文档提供了一个RPC的demo,但是RPC服务端是同步返回结果的,同步的RPC服务会顺序执行RPC队列中的请求, 12 | 如果某一个请求执行较慢,会阻塞后面的请求并造成严重的性能问题。解决这种问题的方法是设置并发消费者(concurrentConsumers属性)或者启动多个RPC服务。 13 | 这在大多数情况下是有效的,但是如果这个任务是异步的,或者甚至是事件驱动的(比如NIO中的readable事件),那么同步阻塞消费者线程的方式就不太合适了。 14 | 该Demo中提供了3个服务端示例: 15 | * 同步执行的RPC服务;多执行几次测试用例,客户端可能会超时(convertSendAndReceive默认5秒)。 16 | * 异步执行的RPC服务,客户端为每个请求创建一个临时的回复队列(或者使用Direct reply-to)。 17 | * 异步执行的RPC服务,客户端使用固定回复队列,需要提供额外的correlationId以关联请求和响应。 18 | 19 | ## 参考资料 20 | * RabbitMQ官方文档,介绍RPC的工作方式: 21 | * Spring AMQP官方文档: 22 | * Direct reply-to介绍: 23 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/async/AsyncRPCClient.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.async; 2 | 3 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_ASYNC_RPC; 4 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_ASYNC_RPC_WITH_FIXED_REPLY; 5 | 6 | import java.util.concurrent.Future; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.amqp.core.AmqpTemplate; 11 | import org.springframework.amqp.rabbit.AsyncRabbitTemplate; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.scheduling.annotation.Async; 14 | import org.springframework.scheduling.annotation.AsyncResult; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.util.concurrent.ListenableFuture; 17 | 18 | @Component 19 | public class AsyncRPCClient { 20 | 21 | @Autowired 22 | AsyncRabbitTemplate asyncRabbitTemplate; 23 | 24 | @Autowired 25 | AmqpTemplate amqpTemplate; 26 | 27 | Logger logger = LoggerFactory.getLogger(getClass()); 28 | 29 | @Async 30 | public Future sendAsync(String message) { 31 | String result = (String) amqpTemplate.convertSendAndReceive(QUEUE_ASYNC_RPC, message); 32 | return new AsyncResult(result); 33 | } 34 | 35 | public Future sendWithFixedReplay(String message) { 36 | ListenableFuture future = asyncRabbitTemplate.convertSendAndReceive(QUEUE_ASYNC_RPC_WITH_FIXED_REPLY, message); 37 | return future; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/cbwleft/rabbit/sync/RPCClientTest.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.sync; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.Future; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | 17 | import com.cbwleft.rabbit.sync.RPCClient; 18 | 19 | @SpringBootTest 20 | @RunWith(SpringRunner.class) 21 | public class RPCClientTest { 22 | 23 | Logger logger = LoggerFactory.getLogger(getClass()); 24 | 25 | @Autowired 26 | RPCClient rpcClient; 27 | 28 | @Test 29 | public void testSend() { 30 | String result = rpcClient.send("hello world"); 31 | logger.info(result); 32 | } 33 | 34 | @Test 35 | public void testSendAsync() throws InterruptedException, ExecutionException { 36 | String[] messages = { "hello", "my", "name", "is", "leijun" }; 37 | List> results = new ArrayList<>(); 38 | for (String message : messages) { 39 | Future result = rpcClient.sendAsync(message); 40 | results.add(result); 41 | } 42 | for (Future future : results) { 43 | String result = future.get(); 44 | if (result == null) { 45 | logger.info("message timeout after 5 seconds"); 46 | } else { 47 | logger.info(result); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.cbwleft 6 | spring-amqp-rpc 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | spring-amqp-rpc 11 | http://maven.apache.org 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 1.5.9.RELEASE 17 | 18 | 19 | 20 | UTF-8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-amqp 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-devtools 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | true 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/test/java/com/cbwleft/rabbit/async/AsyncRPCClientTest.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.async; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Future; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | 17 | @SpringBootTest 18 | @RunWith(SpringRunner.class) 19 | public class AsyncRPCClientTest { 20 | 21 | Logger logger = LoggerFactory.getLogger(getClass()); 22 | 23 | @Autowired 24 | AsyncRPCClient asyncRPCClient; 25 | 26 | @Test 27 | public void testSendAsync() throws InterruptedException, ExecutionException{ 28 | String[] messages = { "hello", "my", "name", "is", "leijun" }; 29 | List> results = new ArrayList<>(); 30 | for (String message : messages) { 31 | Future result = asyncRPCClient.sendAsync(message); 32 | results.add(result); 33 | } 34 | for (Future future : results) { 35 | String result = future.get(); 36 | if (result == null) { 37 | Assert.fail("message will not timeout"); 38 | } else { 39 | logger.info(result); 40 | } 41 | } 42 | } 43 | 44 | @Test 45 | public void testSendWithFixedReplay() throws InterruptedException, ExecutionException{ 46 | String[] messages = { "hello", "my", "name", "is", "leijun" }; 47 | List> results = new ArrayList<>(); 48 | for (String message : messages) { 49 | Future result = asyncRPCClient.sendWithFixedReplay(message); 50 | results.add(result); 51 | } 52 | for (Future future : results) { 53 | String result = future.get(); 54 | if (result == null) { 55 | Assert.fail("message will not timeout"); 56 | } else { 57 | logger.info(result); 58 | } 59 | } 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit; 2 | 3 | import org.springframework.amqp.core.AnonymousQueue; 4 | import org.springframework.amqp.core.Queue; 5 | import org.springframework.amqp.rabbit.AsyncRabbitTemplate; 6 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 7 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 8 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Primary; 12 | import org.springframework.scheduling.annotation.EnableAsync; 13 | 14 | @Configuration 15 | @EnableAsync 16 | public class RabbitConfig { 17 | 18 | /** 19 | * 同步RPC队列 20 | */ 21 | public static final String QUEUE_SYNC_RPC = "rpc.sync"; 22 | 23 | /** 24 | * 异步RPC队列,使用临时回复队列,或者使用“Direct reply-to”特性 25 | */ 26 | public static final String QUEUE_ASYNC_RPC = "rpc.async"; 27 | 28 | /** 29 | * 异步RPC队列,每个客户端使用不同的固定回复队列,需要额外提供correlationId以关联请求和响应 30 | */ 31 | public static final String QUEUE_ASYNC_RPC_WITH_FIXED_REPLY = "rpc.with.fixed.reply"; 32 | 33 | @Bean 34 | public Queue syncRPCQueue() { 35 | return new Queue(QUEUE_SYNC_RPC); 36 | } 37 | 38 | @Bean 39 | public Queue asyncRPCQueue() { 40 | return new Queue(QUEUE_ASYNC_RPC); 41 | } 42 | 43 | @Bean 44 | public Queue fixedReplyRPCQueue() { 45 | return new Queue(QUEUE_ASYNC_RPC_WITH_FIXED_REPLY); 46 | } 47 | 48 | @Bean 49 | public Queue repliesQueue() { 50 | return new AnonymousQueue(); 51 | } 52 | 53 | @Bean 54 | @Primary 55 | public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) { 56 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); 57 | container.setQueueNames(repliesQueue().getName()); 58 | return container; 59 | } 60 | 61 | @Bean 62 | public AsyncRabbitTemplate asyncRabbitTemplate(RabbitTemplate template, SimpleMessageListenerContainer container) { 63 | return new AsyncRabbitTemplate(template, container); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/cbwleft/rabbit/async/AsyncRPCServer.java: -------------------------------------------------------------------------------- 1 | package com.cbwleft.rabbit.async; 2 | 3 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_ASYNC_RPC; 4 | import static com.cbwleft.rabbit.RabbitConfig.QUEUE_ASYNC_RPC_WITH_FIXED_REPLY; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.amqp.AmqpException; 9 | import org.springframework.amqp.core.AmqpTemplate; 10 | import org.springframework.amqp.core.Message; 11 | import org.springframework.amqp.core.MessagePostProcessor; 12 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 13 | import org.springframework.amqp.support.AmqpHeaders; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.messaging.handler.annotation.Header; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.util.concurrent.ListenableFuture; 18 | import org.springframework.util.concurrent.ListenableFutureCallback; 19 | 20 | @Component 21 | public class AsyncRPCServer { 22 | 23 | @Autowired 24 | AmqpTemplate amqpTemplate; 25 | 26 | @Autowired 27 | AsyncTask asyncTask; 28 | 29 | Logger logger = LoggerFactory.getLogger(getClass()); 30 | 31 | @RabbitListener(queues = QUEUE_ASYNC_RPC) 32 | public void process(String message, @Header(AmqpHeaders.REPLY_TO) String replyTo) { 33 | logger.info("recevie message {} and reply to {}", message, replyTo); 34 | if(replyTo.startsWith("amq.rabbitmq.reply-to")) { 35 | logger.debug("starting with version 3.4.0, the RabbitMQ server now supports Direct reply-to"); 36 | }else { 37 | logger.info("fall back to using a temporary reply queue"); 38 | } 39 | ListenableFuture asyncResult = asyncTask.expensiveOperation(message); 40 | asyncResult.addCallback(new ListenableFutureCallback() { 41 | @Override 42 | public void onSuccess(String result) { 43 | amqpTemplate.convertAndSend(replyTo, result); 44 | } 45 | 46 | @Override 47 | public void onFailure(Throwable ex) { 48 | 49 | }; 50 | }); 51 | } 52 | 53 | @RabbitListener(queues = QUEUE_ASYNC_RPC_WITH_FIXED_REPLY) 54 | public void process(String message, @Header(AmqpHeaders.REPLY_TO) String replyTo, 55 | @Header(AmqpHeaders.CORRELATION_ID) byte[] correlationId) { 56 | logger.info("use a fixed reply queue {}, it is necessary to provide correlation data {} so that replies can be correlated to requests", replyTo, new String(correlationId)); 57 | ListenableFuture asyncResult = asyncTask.expensiveOperation(message); 58 | asyncResult.addCallback(new ListenableFutureCallback() { 59 | @Override 60 | public void onSuccess(String result) { 61 | /*Message resultMessage = MessageBuilder.withBody(result.getBytes()) 62 | .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) 63 | .setCorrelationId(correlationId) 64 | .build(); 65 | amqpTemplate.send(replyTo, resultMessage);*/ 66 | amqpTemplate.convertAndSend(replyTo, (Object)result, new MessagePostProcessor() { 67 | @Override 68 | public Message postProcessMessage(Message message) throws AmqpException { 69 | //https://stackoverflow.com/questions/42382307/messageproperties-setcorrelationidstring-is-not-working 70 | message.getMessageProperties().setCorrelationId(correlationId); 71 | return message; 72 | } 73 | }); 74 | } 75 | 76 | @Override 77 | public void onFailure(Throwable ex) { 78 | 79 | }; 80 | }); 81 | } 82 | 83 | } 84 | --------------------------------------------------------------------------------