This blog ticket is the first part of an article series aiming at demystifying the process of design and development of microservices with Spring Boot. This is not a Spring Boot documentation or tutorial and I assume the reader has the required exposure to this technology. It is rather a quick-start sample demonstrating some of the Spring Boot basics like messaging and REST services and allowing to jump directly to the code.
The example presented here is taken from a very real use case. It consists of a Spring Boot based microservice, deployed in an embedded Tomcat container. This microservice exposes a REST API which encapsulates a JMS topic. The API allows to its clients to publish/subscribe JMS messages to this topic. Building this projects results in the creation of two Docker containers, one running an ActiveMQ broker, the other one running a Tomcat container having the microservice deployed into it. The two Docker container communicate each other via OpenWire protocol. Let’s look at the code now.
package fr.simplex_software.micro_services.core.controllers;
import fr.simplex_software.micro_services.core.domain.*;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.http.*;
import org.springframework.jms.core.*;
import org.springframework.web.bind.annotation.*;
import javax.jms.*;
import java.util.*;
@RestController
@RequestMapping("/api")
public class HmlRestController
{
public static final Logger logger = LoggerFactory.getLogger(HmlRestController.class);
@Autowired
private JmsTemplate jmsTemplate;
@Autowired
public Topic hmlGlobalTopic;
private static HashMap<String,JmsTopicSubscriberInfo> subscribers = new HashMap<String, JmsTopicSubscriberInfo>();
@RequestMapping(value = "/publish/", method = RequestMethod.POST)
public ResponseEntity<HmlEvent> gtsPublish(@RequestBody HmlEvent hmlEvent)
{
jmsTemplate.convertAndSend(hmlGlobalTopic, hmlEvent);
logger.debug("*** HmlRestController.publish(): Have published a new event {} {}", hmlEvent.getMessageId(), hmlEvent.getPayload());
return ResponseEntity.accepted().body(hmlEvent);
}
@RequestMapping(value = "/subscribe/", method = RequestMethod.POST)
public ResponseEntity gtsSubscribe(@RequestBody SubscriberInfo si) throws JMSException
{
subscribers.put(si.getSubscriptionName(), si.getSubscriberInfo());
logger.debug("HmlRestController.gtsSubscribe(): Have subscribed to events {}, {}, {}",
si.getSubscriptionName(), si.getSubscriberInfo().getClientId(), si.getSubscriberInfo().getMessageSelector());
return ResponseEntity.accepted().build();
}
@RequestMapping(value = "/test/", method = RequestMethod.POST)
public ResponseEntity gtsTest(@RequestBody HmlEvent event)
{
logger.debug("*** HmlRestController.gtsTest: Have received an event {}, {}", event.getMessageId(), event.getPayload());
return ResponseEntity.accepted().build();
}
public static String getCallbackUrl (String subscriptionName)
{
return subscribers.get(subscriptionName).getCallback();
}
}
The code above is showing the REST controller. It is decorated with the @RestController annotation and it declares the /api URI as its root resource. The provided endpoints are as follows:
The Publish Endpoint
This endpoint allows the REST API consumer to publish a JMS message to the ActiveMQ topic. Here is the syntax:
POST /api/publish
{
hmlEvent:
{ … }
}
This endpoint is accessible via POST requests and it takes one parameter, an instance of the HmlEvent class. It is converted to a JMS message and published to the embedded topic. It replies with HTTP 1.1 202 Accepted.
The Subscribe Endpoint
This endpoint allows the REST API consumer to subscribe to messages published on the embedded topic. Here is the syntax:
POST /api/subscribe
{
si:
{ … }
}
This endpoint is accessible via POST requests and it takes one parameter, an instance of the SubscriberInfo class. The endpoint maintains a set of subscribers to the embedded topic, each one with its associated endpoint that has to be called whenever a JMS message, which interest the current subscriber, has been published. The endpoint replies with HTTP 1.1 202 Accepted. Once a message has been published on the topic, a JMS listener is catching it and forwards it to all the subscribers. Here is the listener code:
package fr.simplex_software.micro_services.core.listeners;
import fr.simplex_software.micro_services.core.controllers.*;
import fr.simplex_software.micro_services.core.domain.*;
import org.slf4j.*;
import org.springframework.http.*;
import org.springframework.jms.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.web.client.*;
import javax.jms.*;
@Component
public class HmlMessageListener
{
public static final Logger logger = LoggerFactory.getLogger(HmlMessageListener.class);
private JmsTopicSubscriberInfo jtsi;
public HmlMessageListener()
{}
public HmlMessageListener(JmsTopicSubscriberInfo jtsi)
{
this.jtsi = jtsi;
}
public JmsTopicSubscriberInfo getJtsi()
{
return jtsi;
}
public void setJtsi(JmsTopicSubscriberInfo jtsi)
{
this.jtsi = jtsi;
}
@JmsListener(destination = "#{@hmlConfig.destinationName}")
public void onMessage(Message message) throws JMSException
{
logger.debug("*** HmlMessageListener.onMessage: have received a message {}", message);
HmlEvent event = (HmlEvent)((ObjectMessage)message).getObject();
new RestTemplate().postForObject(HmlRestController.getCallbackUrl(event.getSubscriptionName()),
new HttpEntity<HmlEvent>(event), Message.class);
}
}
The interesting part here is the method onMessage(). It is decorated with the @JmsListener annotation. Please notice how the JMS destination name is injected here. The “#{@ …}” notation means “the property named destinationName of the Spring bean named hmlConfig”.
The JMS infrastructure is configured in the class HmlConfig which code is shown here below.
package fr.simplex_software.micro_services.core.config;
import org.apache.activemq.*;
import org.apache.activemq.command.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.jms.config.*;
import org.springframework.jms.core.*;
import org.springframework.stereotype.Component;
import javax.jms.*;
import java.net.*;
@Component
public class HmlConfig
{
@Value("${jms.destination.name}")
private String destinationName;
@Value("${jms.broker.url}")
private URI jmsBrokerURL;
@Bean
public ConnectionFactory jmsConnectionFactory()
{
ActiveMQConnectionFactory acf = new ActiveMQConnectionFactory(jmsBrokerURL);
acf.setTrustAllPackages(true);
return acf;
}
@Bean
public JmsTemplate jmsTopicTemplate()
{
JmsTemplate jmsTemplate = new JmsTemplate(jmsConnectionFactory());
jmsTemplate.setDefaultDestinationName(destinationName);
jmsTemplate.setPubSubDomain(true);
return jmsTemplate;
}
@Bean
public Topic hmlGlobalTopic()
{
return new ActiveMQTopic(destinationName);
}
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory()
{
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(jmsConnectionFactory());
factory.setConcurrency("1-1");
factory.setPubSubDomain(true);
return factory;
}
public String getDestinationName()
{
return destinationName;
}
public URI getJmsBrokerURL()
{
return jmsBrokerURL;
}
}
Nothing of very special in this code which defines a couple of Spring beans to configure JMS administrable elements like connection factories and destination. Everything is based on the injection from the property file. For example, we inject the property labeled jms.destination.name from the properties file into the destinationName private attribute which, in turn, is used in the jmsTopicTemplate bean to initialize our JMS template.
In order to build the project, please proceed as follows:
cd workspace
git clone https://github.com/nicolasduminil/micro-services.git
git branch core
mvn –DskipTests clean install –pl ms-core docker-compose:up
This command will clone the appropriated branch from the GIT repository and compile and install the artifact into the maven local repository. Then it will switch to the ms-core sub-project and it will perform there the docker-compose:up goal. This emphasizes the docker-compose utility via its maven plugin and it will build two containers out of docker images. By convention, the docker-compose plugin is looking for its configuration in the src/main/resources/docker-compose.yml file. Here is this file:
version: "2"
services:
active-mq:
image: webcenter/activemq:latest
container_name: active-mq
ports:
- "8161:8161"
- "61616:61616"
- "5672:5672"
- "61613:61613"
- "1883:1883"
- "61614:61614"
ms-core:
image: openjdk:8-jdk-alpine
container_name: ms-core
links:
- active-mq:active-mq
volumes:
- ../docker:/usr/local/share/hml
ports:
- "8080:8080"
entrypoint: /usr/local/share/hml/run.sh
environment:
BROKER_PORT: "61616"
What this file is saying is that we are creating here two docker containers, as follows:
- A first docker container, named “active-mq”, based on the webcenter/activemq image, exposing all the TCP ports that the ActiveMQ broker is using.
- A second container, named “ms-core”, based on the alpine image (openjdk:8-jdk-alpine) doing the following:
- It exposes the Tomcat HTTP port (8080).
- It has a link to the active-mq container, meaning that it is able to refer the active-mq host IP address via the “active-mq” alias.
- It creates a volume mounted on /usr/local/share/hml on which it copies the content of the src/main/docker
- It defines and environment variable named BROKER_PORT having the value 61616.
- It executes the bash file /usr/local/share/hml/run.sh.
Here is the file run.sh:
#!/bin/sh
echo "********************************************************"
echo "Waiting for the ActiveMQ broker to start on port $BROKER_PORT"
echo "********************************************************"
while ! `nc -z active-mq $BROKER_PORT `; do sleep 3; done
echo ">>>>>>>>>>>> ActiveMQ Broker has started"
java -jar /usr/local/share/hml/ms-core.jar
The script here above is simply waiting until the broker ActiveMQ has completed started and, then, it runs the micro-services via Spring Boot. Now, you can check that the Docker containers are running by using the commands: docker ps, docker images, etc. You may also run docker inspect to get the IP address of the hosts in the containers. With this IP address you may now fire you preferred browser to 8161 to see the ActiveMQ console, and to 8080/hml to display the microservice home page.
If everything is okay at this point, you may test the whole stuff by doing a mvn test command in a different command window. You should see that the unit test has been successfully fulfilled. Once you finished playing with your microservice, you can stop it, by pressing CTRL-C in the command window and to stop and remove the two containers using the docker stop and docker rm commands.
Congratulations, by running a simple maven command, you got a very complex runtime environment, consisting in two Linux hosts running an ActiveMQ broker and a Tomcat server, with your microservice embedded inside. Enjoy !