Microservices architecture has several advantages over traditional monolithic applications but it presents different challenges as well - service discovery and interaction with other services on-the-fly are very important among them. In this blog I will explain an Easy way to discover containerized Spring Boot Microservices using Consul.
Context
Service registration and discovery are two important aspects of a microservices-based system. Service registration includes registering services and their associated metadata with a service registry, while service discovery includes discovering and connecting to the correct service instances.
Spring Boot applications can use Consul for service discovery by utilizing the Spring Cloud Consul Discovery client. This client allows the application to register itself as a service with Consul and to discover other services registered with Consul. Consul is a powerful service discovery tool that allows developers to easily create, manage and monitor microservices.
We will create two microservices; named 1) clientidgenerator which accepts a REST API GET request to create a hypothetical "Client Id". In this example, the combination of the name of the client (passed as a path parameter in the GET request) and a unique identifier (UUID) forms the "Client Id". To generate a unique Id, clientidgenerator service takes help of another microservice running in parallel named 2) uuidgenerator. Both microservices are discoverable via Consul.
Let’s move to the coding part..
GitHub
uuidgenerator
We will create a microservice uuidgenerator, which exposes only one REST API endpoint (GET http://<hostname>/v1/uuidgenerator). It generates a random unique identifier (UUID) and returns it as a string. uuidgenerator service is discoverable through Consul.
Project dependencies
We use Spring Cloud Consul, which provides an easy integration to Consul for the spring boot applications. For that purpose we require a couple of dependencies; 'spring-cloud-starter-consul-all
' which is a starter dependency for Spring Boot applications that provides support for both service discovery and configuration management using Consul, and 'spring-cloud-starter-bootstrap
' which helps to load the configuration to register with discovery service, just before the main application configurations are loaded.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-consul-all', version: '4.0.0'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-bootstrap', version: '4.0.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Application configurations
uuidgenerator service will be listening to port 8888.
application:
api:
version: v1
server:
port: 8888
servlet:
context-path: /${application.api.version}/uuidgenerator
Bootstrap configuration
In a Spring Boot application that uses Consul for service discovery, the bootstrap.yml
file is used to configure the settings for connecting to the Consul server and registering the application as a service. This file is typically located in the src/main/resources
directory of your project.
The bootstrap.yml
file is read before the application's application.yml
or application.properties
file, and its properties take precedence over those in the other files. This allows to specify the required Consul-specific configurations before application starts up and registers itself as a service with Consul.
By default the applications look for Consul at port 8500, we just keep that value. We auto-enabled (enabled: true) the discovery of our application.
We added some health checks configurations for the service. By default spring boot adds a health check endpoint which is called periodically and returns 200 OK if service is healthy.
spring:
application:
name: uuidgenerator
cloud:
consul:
host: consul
port: 8500
discovery:
enabled: true
prefer-ip-address: true
instance-id: ${spring.application.name}:${random.value}
healthCheckPath: ${management.server.servlet.context-path}/health
healthCheckInterval: 15s
Application and Controller
The UuidgeneratorApplication
class contains a single method generateUuid()
which is exposed as a REST endpoint. It's annotated with @GetMapping
which maps to the HTTP GET method and with produces = MediaType.TEXT_PLAIN_VALUE
, it specifies that the response body will be of plain text format. Inside the method, it generates and returns a random UUID.
We write @EnableDiscoveryClient annotation over Application class, which enables the application to register itself as a service with a service registry (such as Consul) and discover other services that have also registered with the registry.
A @RestController
is used to handle HTTP requests and responses. This simple service receives a HTTP GET request, generates a random unique identifier and returns it as a string.
Upon starting, this application registers itself as a service with Consul and listens for incoming HTTP requests on the root path ("/"). When it receives a GET request, it generates a new UUID and returns it as plain text in the response.
@RestController
@RequestMapping(path = "/")
@EnableDiscoveryClient
@SpringBootApplication
public class UuidgeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(UuidgeneratorApplication.class, args);
}
@GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
public String generateUuid() {
return UUID.randomUUID().toString();
}
}
Now we can build this application using a Gradle wrapper or any build tool of your choice.
./gradlew clean build
Dockerfile
Create a Dockerfile to build an application docker image. If you wonder what are the meanings of the following lines, then please go to this article where I have explained the basics of the containerization process of docker.
FROM eclipse-temurin:19-jdk-alpine
EXPOSE 8080
ADD build/libs/uuidgenerator-0.0.1-SNAPSHOT.jar /app/uuidgenerator.jar
WORKDIR /app/
CMD java -jar uuidgenerator.jar
We can now build the docker image,
docker build -t uuidgenerator .
clientidgenerator
clientidgenerator service exposes an HTTP GET method, and accepts a /{name} path parameters. It discovers uuidgenerator service on the fly, fetches a UUID and concatenates that UUID with the name of the client and returns as a string.
We repeat most of the steps for clientidgenerator as we did for uuidgenerator.
Project dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-consul-all', version: '4.0.0'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-bootstrap', version: '4.0.0'
implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Application configurations
clientidgenerator service listens to port 8889.
application:
api:
version: v1
uuidgenerator:
name: uuidgenerator
uuid-context-path: /v1/uuidgenerator
server:
port: 8889
servlet:
context-path: /${application.api.version}/clientidgenerator
Bootstrap configuration
spring:
application:
name: clientidgenerator
cloud:
consul:
host: consul
port: 8500
discovery:
enabled: true
prefer-ip-address: true
instance-id: ${spring.application.name}:${random.value}
healthCheckPath: ${management.server.servlet.context-path}/health
healthCheckInterval: 15s
Application
@SpringBootApplication
public class ClientIdGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(ClientIdGeneratorApplication.class, args);
}
}
Controller
clientidgenerator service exposes an HTTP GET method and injects ServiceDiscoveryHelper
which is a helper class to discover uuidgenerator service and get unique id.
@RestController
public class ClientIdController {
@Autowired
private ServiceDiscoveryHelper sdHelper;
public ClientIdController(ServiceDiscoveryHelper serviceDiscovery) {
this.sdHelper = serviceDiscovery;
}
@GetMapping(path = "/{name}", produces = MediaType.TEXT_PLAIN_VALUE)
public String getHelloMessageString(
@PathVariable(value = "name") final String name) throws ServiceUnavailableException {
String msgId = sdHelper.getUuidFromUuidGeneratorService();
return "Hello " + name + "-" + msgId;
}
}
ServiceDiscoveryHelper
ServiceDiscoveryHelper is a helper class which 1) resolves the uuidgenerator (the service to discover) application configurations and 2) injects couple of classes into the context,
- DiscoveryClient: provides API to discover other services
- RestTemplate: provides client HTTP access.
@Component
public class ServiceDiscoveryHelper {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@Value("${application.uuidgenerator.name}")
private String uuidGeneratorServiceName;
@Value("${application.uuidgenerator.uuid-context-path}")
private String uuidGeneratorContextPath;
public String getUuidFromUuidGeneratorService() throws ServiceUnavailableException {
// Discover uuid-generator service from consul and read message key (uuid) from it
URI service = uuidGeneratorServerUri()
.map(s -> s.resolve(uuidGeneratorContextPath))
.orElseThrow(ServiceUnavailableException::new);
return restTemplate.getForObject(service, String.class);
}
private Optional<URI> uuidGeneratorServerUri() {
return discoveryClient.getInstances(uuidGeneratorServiceName)
.stream()
.map(si -> si.getUri()).findFirst();
}
}
RestTemplate
RestTemplate is a spring class for client side HTTP access. clientidgenerator service uses RestTemplate
to call HTTP GET method exposed by uuidgenerator.
@Configuration
public class MessageConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
Dockerfile
FROM eclipse-temurin:19-jdk-alpine
EXPOSE 8080
ADD build/libs/clientidgenerator-0.0.1-SNAPSHOT.jar /app/clientidgenerator.jar
WORKDIR /app/
CMD java -jar clientidgenerator.jar
docker-compose
We are ready to run following services, required for this example
- consul
- uuidgenerator
- clientidgenerator
version: '3.9'
services:
consul:
image: consul:latest
ports:
- "8500:8500"
uuidgenerator:
image: uuidgenerator
ports:
- "8888:8888"
depends_on:
- consul
links:
- "consul"
clientidgenerator:
image: clientidgenerator
build:
context: ./
dockerfile: Dockerfile
ports:
- "8889:8889"
Run them using docker-compose,
docker-compose up
We can also check docker images running in containers in Docker Desktop,
We can check all services status on consul UI, http://localhost:8500
Testing
We fire a HTTP GET request "http://localhost:8889/v1/clientidgenerator/bob" - where "bob" is a name path parameter. clientidgenerator service finds the uuidgenerator service to fetch a unique Id and then return "Hello <client-name>-<id>"
Conclusion
Hope this article helps you understand and implement the discoverable spring boot microservices. We see how Spring Cloud Consul makes service discovery easy by providing a set of simple, easy-to-use annotations that developers can use to integrate Consul into their Spring Boot applications. This can greatly simplify the process of creating, managing, and monitoring microservices.