1. Overview

The Spring Cloud Consul project provides easy integration with Consul for Spring Boot applications.

Consul is a tool that provides components for resolving some of the most common challenges in a micro-services architecture:

  • Service Discovery – to automatically register and unregister the network locations of service instances
  • Health Checking – to detect when a service instance is up and running
  • Distributed Configuration – to ensure all service instances use the same configuration

In this article, we’ll see how we can configure a Spring Boot application to use these features.

2. Prerequisites

To start with, it’s recommended to take a quick look at Consul and all its features.

In this article, we’re going to use a Consul agent running on localhost:8500. For more details about how to install Consul and run an agent, refer to this link.

First, we’ll need to add the spring-cloud-starter-consul-all dependency to our pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-all</artifactId>
    <version>3.1.1</version>
</dependency>

3. Service Discovery

Let’s write our first Spring Boot application and wire up with the running Consul agent:

@SpringBootApplication
public class ServiceDiscoveryApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(ServiceDiscoveryApplication.class)
          .web(true).run(args);
    }
}

By default, Spring Boot will try to connect to the Consul agent at localhost:8500. To use other settings, we need to update the application.yml file:

spring:
  cloud:
    consul:
      host: localhost
      port: 8500

Then, if we visit the Consul agent’s site in the browser at http://localhost:8500, we’ll see that our application was properly registered in Consul with the identifier from “${spring.application.name}:${profiles separated by comma}:${server.port}”.

To customize this identifier, we need to update the property spring.cloud.discovery.instanceId with another expression:

spring:
  application:
    name: myApp
  cloud:
    consul:
      discovery:
        instanceId: ${spring.application.name}:${random.value}

If we run the application again, we’ll see that it was registered using the identifier “MyApp” plus a random value. We need this for running multiple instances of our application on our local machine.

Finally, to disable Service Discovery, we need to set the property spring.cloud.consul.discovery.enabled to false.

3.1. Looking Up Services

We already have our application registered in Consul, but how can clients find the service endpoints? We need a discovery client service to get a running and available service from Consul.

Spring provides a DiscoveryClient API for this, which we can enable with the @EnableDiscoveryClient annotation:

@SpringBootApplication
@EnableDiscoveryClient
public class DiscoveryClientApplication {
    // ...
}

Then, we can inject the DiscoveryClient bean into our controller and access the instances:

@RestController
public class DiscoveryClientController {
 
    @Autowired
    private DiscoveryClient discoveryClient;

    public Optional<URI> serviceUrl() {
        return discoveryClient.getInstances("myApp")
          .stream()
          .findFirst() 
          .map(si -> si.getUri());
    }
}

Finally, we’ll define our application endpoints:

@GetMapping("/discoveryClient")
public String discoveryPing() throws RestClientException, 
  ServiceUnavailableException {
    URI service = serviceUrl()
      .map(s -> s.resolve("/ping"))
      .orElseThrow(ServiceUnavailableException::new);
    return restTemplate.getForEntity(service, String.class)
      .getBody();
}

@GetMapping("/ping")
public String ping() {
    return "pong";
}

The “myApp/ping” path is the Spring application name with the service endpoint. Consul will provide all available applications named “myApp”.

4. Health Checking

Consul checks the health of the service endpoints periodically.

By default, Spring implements the health endpoint to return 200 OK if the app is up. If we want to customize the endpoint we have to update the application.yml:

spring:
  cloud:
    consul:
      discovery:
        healthCheckPath: /my-health-check
        healthCheckInterval: 20s

As a result, Consul will poll the “/my-health-check” endpoint every 20 seconds.

Let’s define our custom health check service to return a FORBIDDEN status:

@GetMapping("/my-health-check")
public ResponseEntity<String> myCustomCheck() {
    String message = "Testing my healh check function";
    return new ResponseEntity<>(message, HttpStatus.FORBIDDEN);
}

If we go to the Consul agent site, we’ll see that our application is failing. To fix this, the “/my-health-check” service should return the HTTP 200 OK status code.

5. Distributed Configuration

This feature allows synchronizing the configuration among all the services. Consul will watch for any configuration changes and then trigger the update of all the services.

First, we need to add the spring-cloud-starter-consul-config dependency to our pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
    <version>3.1.1</version>
</dependency>

We also need to move the settings of Consul and Spring application name from the application.yml file to the bootstrap.yml file which Spring loads first.

Then, we need to enable Spring Cloud Consul Config:

spring:
  application:
    name: myApp
  cloud:
    consul:
      host: localhost
      port: 8500
      config:
        enabled: true

Spring Cloud Consul Config will look for the properties in Consul at “/config/myApp”. So if we have a property called “my.prop”, we would need to create this property in the Consul agent site.

We can create the property by going to the “KEY/VALUE” section, then entering “/config/myApp/my/prop” in the “Create Key” form and “Hello World” as value. Finally, click the “Create” button.

Bear in mind that if we are using Spring profiles, we need to append the profiles next to the Spring application name. For example, if we are using the dev profile, the final path in Consul will be “/config/myApp,dev”.

Now, let’s see what our controller with the injected properties looks like:

@RestController
public class DistributedPropertiesController {

    @Value("${my.prop}")
    String value;

    @Autowired
    private MyProperties properties;

    @GetMapping("/getConfigFromValue")
    public String getConfigFromValue() {
        return value;
    }

    @GetMapping("/getConfigFromProperty")
    public String getConfigFromProperty() {
        return properties.getProp();
    }
}

And the MyProperties class:

@RefreshScope
@Configuration
@ConfigurationProperties("my")
public class MyProperties {
    private String prop;

    // standard getter, setter
}

If we run the application, the field value and properties have the same “Hello World” value from Consul.

5.1. Updating the Configuration

What about updating the configuration without restarting the Spring Boot application?

If we go back to the Consul agent site and we update the property “/config/myApp/my/prop” with another value like “New Hello World”, then the field value will not change and the field properties will have been updated to “New Hello World” as expected.

This is because the field properties is a MyProperties class has the @RefreshScope annotation. All beans annotated with the @RefreshScope annotation will be refreshed after configuration changes.

In real life, we should not have the properties directly in Consul, but we should store them persistently somewhere. We can do this using a Config Server.

6. Conclusion

In this article, we’ve seen how to set up our Spring Boot applications to work with Consul for Service Discovery purposes, customize the health checking rules and share a distributed configuration.

We’ve also introduced a number of approaches for the clients to invoke these registered services.

As usual, sources can be found over on GitHub.