Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
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.
