In this 2 part tutorial we will touch base all the important concepts of java based microservices applications using Spring Boot.
Defining our microservices :
So before moving further in this microservices tutorial series lets first define what are the microservices we would be working on. For the sake of simplicity we would be giving the name as microservice-1 and microservice-2. And yes, microservice-2 is calling microservice-1 and microservice-1 is talking to database for anything.
Lets create the microservice-1 first.
Create a Spring Boot application by adding below dependencies:
- Spring Web
- Lombok
- Spring Boot DevTools
Add below in application.properties:
spring.application.name=microservice-1 server.port=8000
Run the application, it will run fine.
Similarly create the microservice-2 application.
Lets finalize the Request and Response Structure :
Below is the Request and Response structure for microservice-1.
Request:
http://localhost:8000/microservice-1/from/USD/to/INR
Response:
{ "id":10001, "from":"USD", "to":"INR", "conversionMultiple":65.00, "environment":"8000 instance-id" }
And below is the Request and Response structure for microservice-2.
Request:
http://localhost:8100/microservice-2/from/USD/to/INR/quantity/10
Response:
{ "id": 10001, "from": "USD", "to": "INR", "conversionMultiple": 65.00, "quantity": 10, "totalCalculatedAmount": 650.00, "environment": "8000 instance-id" }
Setting dynamic port in response :
In the returning bean class have a String variable and give it some name – environment. Add setter and getter for this variable.
Then in the controller class inject an Environment reference like below and in some business method fetch the “local.server.port” property and assign it to the returning bean:
import org.springframework.core.env.Environment; // @Autowired private Environment environment;
String port=environment.getProperty("local.server.port"); responseBean.setEnvironment(port);
Running another instance of application on different port :
Right click on the application in eclipse -> Run Configurations -> Right click on the java application -> Duplicate
Arguments -> VM arguments
-Dserver.port=8001
Java Microservices Communication :
How a microservice invoke another microservice in Spring Boot ?
Let us invoke microservice-1 from microservice-2. For that one of the way is to use RestTemplate like below:
HashMap uriVariables=new HashMap(); uriVariables.put("from",from); uriVariables.put("to",to); ResponseEntity<Microservice2> responseEntity=new RestTemplate().getForEntity("http://localhost:8000/microservice-1/from/USD/to/INR", Microservice2.class, uriVariables); Microservice2 microservice2 = responseEntity.getBody();
But this is not the right approach. If there are multiple microservices which are calling each other, the code would be too cumbersome.
The alternate to this is – Feign.
In the above example we can replace it with Feign.
Lets follow the steps to enable Feign in microservice-2 (the calling service), you don’t have to do anything in the microservice-1.
Steps to add Feign:
- Add below Feign dependency. Feign dependencies are not available in Spring Initializer. You need to manually add it.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2. In main class add this: @EnableFeignClients
3. Create a proxy interface and add @FeignClient on the top. There the “name“ attribute is the name of the application you are supposed to call. Add the methods that you want to call with the same signature that is present in the calling method.
@FeignClient(name="microservice-1", url="localhost:8000") public interface MyProxy { @GetMapping("microservice-1/from/{from}/to/{to}") public Microservice2 getValue(@PathVariable String from, @PathVariable String to); }
4. In the controller class you just have to call the above proxy method like below:
proxy.getValue(from, to);
The full controller class:
@Autowired private Environment environment; @Autowired MyProxy proxy; @GetMapping("microservice-2-feign/from/{from}/to/{to}/quantity/{quantity}") public Microservice2 calculateFeign(@PathVariable String from, @PathVariable String to, @PathVariable BigDecimal quantity) { Microservice2 microservice2 = proxy.getValue(from, to); return new Microservice2(microservice2.getId(), from, to, microservice2.getConversionMultiple(),microservice2.getEnvironment(), quantity, quantity.multiply(microservice2.getConversionMultiple()) ); }
Thats it….
You can test by calling the URL:
http://localhost:8100/microservice-2-feign/from/USD/to/INR/quantity/10
The GitHub URL for both microservices are here:
https://github.com/heapsteep/microservice-1.git
https://github.com/heapsteep/microservice-2.git
Eureka Naming Server & Client :
In the above tutorial we had hardcoded the port in the microservices-2 while setting the Feign:
@FeignClient(name="microservice-1", url="localhost:8000")
If there are multiple instances of microservice-1 then we will not be able to call other instances.
To solve this issue, we will go with Naming Server or Service Registry, where all the instances of the microservices will be registered. So this Naming Server will also be responsible for Load Balancing at server side .
FYI, Feign is used as client side load balancing.
Lets create a new spring boot application from Spring Initializr. We will call it – naming-server. Add below dependency to it:
* Eureka Server
In pom.xml we can see these:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
Add @EnableEurekaServer to the main class.
In application.properties:
spring.application.name=naming-server server.port=8761 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false
That all…. Just start the application and call the below url in a browser so that you can see the UI console of Naming Server:
http://localhost:8761/
But you may not see any applications (microservices) registered with it. For that you have to follow some steps mentioned in the next section – Eureka Discovery Client.
GitHub source code of the above code: https://github.com/heapsteep/Naming-Server.git
Eureka Discovery Client:
Now lets add this dependency to all microservices in pom.xml: Eureka Discovery Client
In pom.xml one can see this:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
Thats all… Start the microservices. You will see its been registered with the Naming Server.
So, you only need the Eureka Discovery Client as the dependency in microservice.
P.S. : Whenever you are adding any “Cloud” specific dependency, don’t forget to add below items as well in pom.xml:
<spring-cloud.version>2020.0.2</spring-cloud.version> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Load Balancing In Spring Boot Microservices :
Prior to Springboot 2.4 the load balancing was happened through Ribbon. But now its been taken care by the Load Balancer jar present in Eureka Naming Client.
So to perform load balancing, first we need to remove any particular port from the below code- since we have to load balance between multiple ports:
@FeignClient(name="microservice-1", url="localhost:8000")
and re-write it like below:
@FeignClient(name="microservice-1")
You will see the below URL would be still working fine:
http://localhost:8100/microservice-2-feign/from/USD/to/INR/quantity/10
Lets launch the application in different port as well, say port: 8001.
We had already discussed here how to start another instance of the application in a different port.
Once that is done, you will see 2 instances of Microservice-1 registered with Eureka.
Then call the above feign URL multiple times and you can see the application is getting load balanced between 2 different ports, i.e., both the ports will be called one by one.
P.S.: If you don't see the application is load balanced, just give some time, may be 1 minute, then try again.
This magic is happening due to spring-cloud-starter-load-balancer which is present in spring-cloud-starter-netflix-eureka-client. If you open pom.xml -> Dependency Hierarchy -> Type “load” in the Filter. You will see below screenshot. You can see the spring-cloud-starter-loadbalancer present in spring-cloud-starter-netflix-eureka-client.
API Gateway :
Before Springboot 2.4 we used to use Zuul API Gateway. But now we will use Spring Cloud API Gateway.
Create a spring boot application from Spring Initializr. Add this dependency- Gateway.
Add Eureka Client dependency as well since this has to be registered with a Naming Server.
In application.properties add the below things:
spring.application.name=api-gateway server.port=8765 spring.cloud.gateway.discovery.locator.enabled=true
Once you start this application, you can see the api-gateway is listed in the Naming server like below:
Now lets call our microservices through api-gateway.
One of our endpoint in microservice-1 is something like below :
http://localhost:8000/microservice-1/from/USD/to/INR
Now, to access the above URL through api-gateway you have to call below URL:
http://localhost:8765/MICROSERVICE-1/microservice-1/from/USD/to/INR
That means, using API Gateway you were calling the visible enlisted services on the API Gateway dashboard, and those services will call the respective microservices.
If the above API Gateway URL doesn’t work, add below property in the respective application.properties of microservices:
eureka.instance.hostname=localhost
P.S.: Whatever you do changes for API Gateway, give some time (may be around 60 seconds) before you hit the URLs. It takes time to reflect things on Naming Server.
Similarly you can re-write the URL for the microservice-2 through the API Gateway.
Now you can add authentication logic on the api gateway and allow only the authenticated/authorized user to pass through.
Now the only problem we see in the URL is the CAPS version of some text. How to fix that out ? Add below line in application.properties file:
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
If the above one doesn’t work add the below one:
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
So instead of this URL: http://localhost:8765/MICROSERVICE-1/microservice-1/from/USD/to/INR
You have to call this URL: http://localhost:8765/microservice-1/microservice-1/from/USD/to/INR
P.S.: When you start the applications always start in the below sequence:
Naming Server -> API Gateway -> Other Microservices.
So, to run the above example you need 4 applications: one Naming Server, one API Gateway and two microservices (microservice-1 and microservice-2).
The above source code can be found at: https://github.com/heapsteep/api-gateway.git
There is one more observation:
If you give the name of book-service as BOOK-SERVICE-2.4, means some numeric at the end, you will not be able to access the URL. Don’t know, it seems like a bug.
So, for me below URL did not worked, even if I had given the application name as same in application.properties file:
http://localhost:8765/BOOK-SERVICE-2.4/api/getBookDetail/1
So, don’t give application name as shown in the above diagram as BOOK-SERVICE-2.4.
Lets continue further.
If you see there is a little problem in the below invoked URL :
http://localhost:8765/microservice-1/microservice-1/from/USD/to/INR
The application names are mentioned 2 times. To fix that we have to see something called custom routes in the part 2 of this blog series.