1. Overview
We saw in the previous installments of the series how we can leverage Spring Remoting and the related technologies to enable synchronous Remote Procedure Calls on top of an HTTP channel between a server and a client.
In this article, we’ll explore Spring Remoting on top of AMQP, which makes it possible to execute synchronous RPC while leveraging a medium that is inherently asynchronous.
2. Installing RabbitMQ
There are various messaging systems that are compatible with AMQP that we could use, and we choose RabbitMQ because it’s a proven platform and it’s fully supported in Spring – both products are managed by the same company (Pivotal).
If you’re not acquainted with AMQP or RabbitMQ you can read our quick introduction.
So, the first step is to install and start RabbitMQ. There are various ways to install it – just choose your preferred method following the steps mentioned in the official guide.
3. Maven Dependencies
We are going to set up server and client Spring Boot applications to show how AMQP Remoting works. As is often the case with Spring Boot, we just have to choose and import the correct starter dependencies, as explained here:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>
We explicitly excluded the spring-boot-starter-tomcat because we don’t need any embedded HTTP server – that would be automatically started instead if we allowed Maven to import all the transitive dependencies in the classpath.
4. Server Application
4.1. Expose the Service
As we showed in the previous articles, we’ll expose a CabBookingService that simulates a likely remote service.
Let’s start by declaring a bean that implements the interface of the service we want to make remotely callable. This is the bean that will actually execute the service call at server-side:
@Bean CabBookingService bookingService() { return new CabBookingServiceImpl(); }
Let’s then define the queue from which the server will retrieve invocations. It’s enough in this case to specify a name for it, providing it in the constructor:
@Bean Queue queue() { return new Queue("remotingQueue"); }
As we already know from the previous articles, one of the main concepts of Spring Remoting is the Service Exporter, the component that actually collects the invocation requests from some source ─ in this case a RabbitMQ queue ─ and invokes the desired method on the service implementation.
In this case, we define an AmqpInvokerServiceExporter that ─ as you can see ─ needs a reference to an AmqpTemplate. The AmqpTemplate class is provided by the Spring Framework and eases the handling of AMQP-compatible messaging systems the same way the JdbcTemplate makes easier to deal with databases.
We won’t explicitly define such AmqpTemplate bean because it will be automatically provided by Spring Boot‘s auto-configuration module:
@Bean AmqpInvokerServiceExporter exporter( CabBookingService implementation, AmqpTemplate template) { AmqpInvokerServiceExporter exporter = new AmqpInvokerServiceExporter(); exporter.setServiceInterface(CabBookingService.class); exporter.setService(implementation); exporter.setAmqpTemplate(template); return exporter; }
Finally, we need to define a container that has the responsibility to consume messages from the queue and forward them to some specified listener.
We’ll then connect this container to the service exporter, we created in the previous step, to allow it to receive the queued messages. Here the ConnectionFactory is automatically provided by Spring Boot the same way the AmqpTemplate is:
@Bean SimpleMessageListenerContainer listener( ConnectionFactory facotry, AmqpInvokerServiceExporter exporter, Queue queue) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(facotry); container.setMessageListener(exporter); container.setQueueNames(queue.getName()); return container; }
4.2. Configuration
Let’s remember to set up the application.properties file to allow Spring Boot to configure the basic objects. Obviously, the values of the parameters will also depend on the way RabbitMQ has been installed.
For instance, the following one could be a reasonable configuration when RabbitMQ runs it the same machine where this example runs:
spring.rabbitmq.dynamic=true spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.host=localhost
5. Client Application
5.1. Invoke the Remote Service
Let’s tackle the client now. Again, we need to define the queue where invocation messages will be written to. We need to double-check that both client and server use the same name.
@Bean Queue queue() { return new Queue("remotingQueue"); }
At client-side, we need a slightly more complex setup than on the server side. In fact, we need to define an Exchange with the related Binding:
@Bean Exchange directExchange(Queue someQueue) { DirectExchange exchange = new DirectExchange("remoting.exchange"); BindingBuilder .bind(someQueue) .to(exchange) .with("remoting.binding"); return exchange; }
A good intro on the main concepts of RabbitMQ as Exchanges and Bindings is available here.
Since Spring Boot does not auto-configure the AmqpTemplate, we must set one up ourselves, specifying a routing key. In doing so, we need to double-check that the routing key and the exchange match with the one used to define the Exchange in the previous step:
@Bean RabbitTemplate amqpTemplate(ConnectionFactory factory) { RabbitTemplate template = new RabbitTemplate(factory); template.setRoutingKey("remoting.binding"); template.setExchange("remoting.exchange"); return template; }
Then, as we did with other Spring Remoting implementations, we define a FactoryBean that will produce local proxies of the service that is remotely exposed. Nothing too fancy here, we just need to provide the interface of the remote service:
@Bean AmqpProxyFactoryBean amqpFactoryBean(AmqpTemplate amqpTemplate) { AmqpProxyFactoryBean factoryBean = new AmqpProxyFactoryBean(); factoryBean.setServiceInterface(CabBookingService.class); factoryBean.setAmqpTemplate(amqpTemplate); return factoryBean; }
We can now use the remote service as if it was declared as a local bean:
CabBookingService service = context.getBean(CabBookingService.class); out.println(service.bookRide("13 Seagate Blvd, Key Largo, FL 33037"));
5.2. Setup
Also for the client application, we have to properly choose the values in the application.properties file. In a common setup, those would exactly match the ones used on the server side.
5.3. Run the Example
This should be enough to demonstrate the remote invocation through RabbitMQ. Let’s then start RabbitMQ, the server application, and the client application that invokes the remote service.
What happens behind the scenes is that the AmqpProxyFactoryBean will build a proxy that implements the CabBookingService.
When a method is invoked on that proxy, it queues a message on the RabbitMQ, specifying in it all the parameters of the invocation and a name of a queue to be used to send back the result.
The message is consumed from the AmqpInvokerServiceExporter that invokes the actual implementation. It then collects the result in a message and places it on the queue which name was specified in the incoming message.
The AmqpProxyFactoryBean receives back the result and, finally, returns the value that has been originally produced at the server side.
6. Conclusion
In this article, we saw how we can use Spring Remoting to provide RPC on top of a messaging system.
It’s probably not the way to go for the main scenarios where we probably prefer to leverage the asynchronicity of RabbitMQ, but in some selected and limited scenarios, a synchronous call can be easier to understand and quicker and simpler to develop.
As usual, you’ll find the sources on over on GitHub.