In the post Spring Boot Microservices Introduction we have already got an idea of what are Microservices and basic Microservice architecture. In this post we'll see an example of creating Microservice using Spring Boot.
In this Spring Boot Microservice example we'll have two services CustomerService and AccountService to cater to customer and account related functionality respectively.
When customer information is retrieved it should also get the accounts associated with the specific customer. For getting the associated accounts for a customer we'll have to make a call from CustomerService to AccountService. That way we'll also see communication between Microservices.
Let's go the whole nine yards (well atleast 7 and a half yards!) while creating these microservices. So, we'll have DB table for Customer and Account (using MySQL DB), JPA repository using Spring Data JPA so that we don't have to implement data access layer thus reducing the amount of boilerplate code, Lombok to further avoid boilerplate code for getters and setters and for creating objects.
Technologies used in the example
- STS 4.1.19
- Spring Boot 3.1.1
- Spring 6.x
- Lombok 1.8.28
- MySQL 8.x
Customer Service Microservice
Let's start with Customer Service, we'll first create it without getting any account details and add that part later.
In STS create a new Spring Starter Project and give details as given here.
With that the created pom.xml should look like this-
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.netjstech</groupId> <artifactId>customer-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>customer-service</name> <description>Customer Service</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
DB related changes
In MySQL DB I have created a new schema Customer and connection URL points to that schema. In the resources folder create application.yml file and add following configuration.
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/customer username: DB_USERNAME password: DB_PASSWORD application: name: customer-service jpa: properties: hibernate: sqldialect: org.hibernate.dialect.MySQLDialect showsql: true
Please change the database URL, user name and password as per your DB configuration.
Customer Table
CREATE TABLE `customer`.`customer` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NULL, `age` INT NULL, `city` VARCHAR(45) NULL, PRIMARY KEY (`id`));
Customer Entity
Entity class which maps to this Customer table.
package com.netjstech.customerservice.dao.entity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @AllArgsConstructor @NoArgsConstructor @Table(name="customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int age; private String city; }
As you can see Lombok annotations @Data
(to generate getters, setters, equals and hashcode methods), @AllArgsConstructor
,
@NoArgsConstructor
(no-args constructor is required as per specification) are used to generate the boilerplate code.
Customer Repository
Add an interface that extends JPARepository interface.
package com.netjstech.customerservice.dao.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.netjstech.customerservice.dao.entity.Customer; public interface CustomerRepository extends JpaRepository<Customer, Long>{ }
Custom Exception class
A custom exception class CustomerServiceException class is also created.
public class CustomerServiceException extends RuntimeException{ public CustomerServiceException() { super(); } public CustomerServiceException(String msg){ super(msg); } public CustomerServiceException (String msg, Throwable t) { super(msg, t); } }
Service interface and implementation
package com.netjstech.customerservice.service; import com.netjstech.customerservice.dao.entity.Customer; public interface CustomerService { Customer saveCustomer(Customer customer); Customer getCustomerById(Long id); }
CustomerServiceImpl class
package com.netjstech.customerservice.service; import org.springframework.stereotype.Service; import com.netjstech.customerservice.dao.entity.Customer; import com.netjstech.customerservice.dao.repository.CustomerRepository; import com.netjstech.customerservice.exception.CustomerServiceException; @Service public class CustomerServiceImpl implements CustomerService{ private final CustomerRepository customerRepository; CustomerServiceImpl(CustomerRepository customerRepository){ this.customerRepository = customerRepository; } @Override public Customer saveCustomer(Customer customer) { return customerRepository.save(customer); } @Override public Customer getCustomerById(Long id) { return customerRepository.findById(id) .orElseThrow(()-> new CustomerServiceException("No customer found for the given id - " + id)); } }
CustomerRepository is injected here using constructor injection.
Customer Controller class
package com.netjstech.customerservice.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.netjstech.customerservice.dao.entity.Customer; import com.netjstech.customerservice.service.CustomerService; @RestController @RequestMapping("/customer") public class CustomerController { @Autowired private CustomerService customerService; @PostMapping public ResponseEntity<Customer> saveCustomer(@RequestBody Customer customer) { Customer returnedCustomer = customerService.saveCustomer(customer); return ResponseEntity.ok(returnedCustomer); } @GetMapping("/{id}") public ResponseEntity<Customer> getCustomerById(@PathVariable("id") Long customerId){ Customer customer = customerService.getCustomerById(customerId); return ResponseEntity.ok(customer); } }
Methods to save and find customer are mapped here using post mapping and get mapping. Methods return ResponseEntity instance with the HTTPStatus code as ok. As body of Response instance of customer is returned.
With this we are done with Customer Service, right click on the generated CustomerServiceApplication class and run it as Spring Boot App. If everything is fine then your application should be deployed on Tomcat and server should listen on port 8081 (server.port is given the value 8081 in application.yml file).
@SpringBootApplication public class CustomerServiceApplication { public static void main(String[] args) { SpringApplication.run(CustomerServiceApplication.class, args); } }
Let's have a quick check of our REST API using Postman.
Get with URL as http://localhost:8081/customer/1 should give you the Customer details.
As you can see right now there are no account details that we'll add a little bit later.
Account-Service MicroService
In order to create Account-Service as a Spring Boot microservice, in STS create a new Spring Starter Project and give details as given here.
name: account-service Group id: com.netjstech Artifact id: accountservice
Add the same dependencies as given in Customer Service.
DB related changes
In MySQL DB I have created a new schema Account and connection URL points to that schema. In the resources folder create application.yml file and add following configuration.
server: port: 8082 spring: datasource: url: jdbc:mysql://localhost:3306/account username: DB_USERNAME password: DB_PASSWORD application: name: account-service jpa: properties: hibernate: sqldialect: org.hibernate.dialect.MySQLDialect showsql: true
Entries are very similar to what we did in Customer Service, port is 8082 this time so that both applications can run at the same time without any problem. DB URL and application name are also changed as per Account Service.
Please change the database URL, user name and password as per your DB configuration.
Account Table
SQL for creating Account table.
CREATE TABLE `account` ( `id` bigint NOT NULL AUTO_INCREMENT, `account_num` varchar(255) NOT NULL, `balance` double NOT NULL, `customer_id` bigint DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `UK_20mg37dn97899fquqomy38gjg` (`account_num`) )
Surrogate key id is used as primary key and account number has unique constraint so that value for account number is unique.
Account Entity
Entity class which maps to the Account table.
package com.netjstech.accountservice.dao.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Data; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor @Data @Table(name="account") public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "account_num", nullable = false, unique = true) private String accountNumber; private double balance; private Long customerId; }
Account Repository
package com.netjstech.accountservice.dao.repository; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.netjstech.accountservice.dao.entity.Account; public interface AccountRepository extends JpaRepository<Account, Long>{ List<Account> findAccountByCustomerId(Long id); Optional<Account> findByAccountNumber(String accountNumber); }
Here two methods are added findAccountByCustomerId()
and findByAccountNumber()
. For methods starting with such names as
"find", "count", "remove", "delete", Spring framework can automatically generate the correct query.
AccountServiceException class
A custom exception class AccountServiceException is also created.
package com.netjstech.accountservice.exception; public class AccountServiceException extends RuntimeException{ public AccountServiceException() { super(); } public AccountServiceException(String msg){ super(msg); } public AccountServiceException (String msg, Throwable t) { super(msg, t); } }
Service interface and implementation
package com.netjstech.accountservice.service; import java.util.List; import com.netjstech.accountservice.dao.entity.Account; public interface AccountService { Account saveAccount(Account account); Account getAccountByAccountNumber(String accountNumber); List<Account> getAccountsByCustomerId(Long costomerId); }
AccountServiceImpl class
package com.netjstech.accountservice.service; import java.util.List; import org.springframework.stereotype.Service; import com.netjstech.accountservice.dao.entity.Account; import com.netjstech.accountservice.dao.repository.AccountRepository; import com.netjstech.accountservice.exception.AccountServiceException; @Service public class AccountServiceImpl implements AccountService { private final AccountRepository accountRepository; AccountServiceImpl(AccountRepository accountRepository) { this.accountRepository = accountRepository; } @Override public Account saveAccount(Account account) { return accountRepository.save(account); } @Override public Account getAccountByAccountNumber(String accountNumber) { Account account = accountRepository.findByAccountNumber(accountNumber) .orElseThrow(() -> new AccountServiceException("No Account found for the given account number: " + accountNumber)); return account; } @Override public List<Account> getAccountsByCustomerId(Long costomerId) { List<Account> accounts = accountRepository.findAccountByCustomerId(costomerId); return accounts; } }
AccountController class
package com.netjstech.accountservice.controller; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.netjstech.accountservice.dao.entity.Account; import com.netjstech.accountservice.service.AccountService; @RestController @RequestMapping("/account") public class AccountController { private final AccountService accountService; AccountController(AccountService accountService){ this.accountService = accountService; } @PostMapping public ResponseEntity<Account> saveAccount(@RequestBody Account account) { return ResponseEntity.ok(accountService.saveAccount(account)); } @GetMapping("/{accountNumber}") public ResponseEntity<Account> getAccountByAccountNumber(@PathVariable("accountNumber") String accountNumber){ System.out.println("id .. " + accountNumber); Account account = accountService.getAccountByAccountNumber(accountNumber); return ResponseEntity.ok(account); } @GetMapping("/customer/{id}") public List<Account> getAccountsByCustomerId(@PathVariable("id") Long customerId){ System.out.println("id .. " + customerId); List<Account> accounts = accountService.getAccountsByCustomerId(customerId); return accounts; } }
With this we are done with Account Service, right click on the generated AccountServiceApplication class and run it as Spring Boot App. If everything is fine then your application should be deployed on Tomcat and server should listen on port.
@SpringBootApplication public class AccountServiceApplication { public static void main(String[] args) { SpringApplication.run(AccountServiceApplication.class, args); } }
We'll check AccountService methods using Postman.
With method selected as Post, URL as http://localhost:8082/account and Body (JSON) as
{ "accountNumber": "A001", "balance": 8000.00, "customerId": 1 }
It should insert a record in Account table. Run it again with the following Body so that we have two accounts for customer with id 1.
{ "accountNumber": "A002", "balance": 8000.00, "customerId": 1 }
Now with method as Get and URL as http://localhost:8082/account/customer/1 you should get both accounts.
[ { "id": 1, "accountNumber": "A001", "balance": 6000.0, "customerId": 1 }, { "id": 2, "accountNumber": "A002", "balance": 8000.0, "customerId": 1 } ]
Communication between Microservices
At this point we have two separate Microservices running, catering to the Customer and Account functionality. As per our initial requirement when we get Customer by id, it should also show the associated accounts. For that we need communication between Customer Microservice and Account Microservice.
In this example we'll use RestTemplate
for communicating with Microservice. Note that RestTemplate is going
to be deprecated in coming Spring versions as it is in maintenance mode from Spring 5. WebClient is the suggested way as
per Spring framework going forward.
Configuration for RestTemplate
package com.netjstech.customerservice.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class TemplateConfig { @Bean RestTemplate restTemplate() { return new RestTemplate(); } }
This configuration is done to instantiate RestTemplate so that it can be injected where it is required.
Since we need Account information in Customer Service so AccountDto is created and also a CustomerDto where List of accounts is also added.
AccountDto class
package com.netjstech.customerservice.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class AccountDto { private Long id; private String accountNumber; private double balance; private Long customerId; }
CustomerDto class
package com.netjstech.customerservice.dto; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Builder @Data @NoArgsConstructor @AllArgsConstructor public class CustomerDto { private Long id; private String name; private int age; private String city; private List<AccountDto> accounts; }
Updated CustomerService interface and CustomerServiceImpl
public interface CustomerService { Customer saveCustomer(Customer customer); CustomerDto getCustomerById(Long id); }Now getCustomerById() method returns CustomerDto.
CustomerServiceImpl class
@Service public class CustomerServiceImpl implements CustomerService{ private final CustomerRepository customerRepository; private final RestTemplate restTemplate; CustomerServiceImpl(CustomerRepository customerRepository, RestTemplate restTemplate){ this.customerRepository = customerRepository; this.restTemplate = restTemplate; } @Override public Customer saveCustomer(Customer customer) { return customerRepository.save(customer); } @Override public CustomerDto getCustomerById(Long id) { Customer customer = customerRepository.findById(id) .orElseThrow(()-> new CustomerServiceException("No customer found for the given id - " + id)); ResponseEntity<List<AccountDto>> response = restTemplate.exchange("http://localhost:8082/account/customer/"+id, HttpMethod.GET, null, new ParameterizedTypeReference<List<AccountDto>>(){}); List<AccountDto> accounts = response.getBody(); CustomerDto customerDto = CustomerDto.builder() .id(customer.getId()) .name(customer.getName()) .age(customer.getAge()) .city(customer.getCity()) .accounts(accounts) .build(); return customerDto; } }
RestTemplate is also injected in the Service class. In the method getCustomerById() call is make to the AccountService (http://localhost:8082/account/customer/{id}) using the resttemplate.exchange() method. Here URL is hardcoded which is not the best way to communicate with a Microservice. Once the Response is received from the call (Note that RestTemplate is a Synchronous client to perform HTTP requests so it waits until response is received) CutomerDto object is created using builder() which uses builder pattern and available for use with @Builder annotation provided by Lombok.
Refer this post- Spring Boot Microservice - Service Registration and Discovery With Eureka to see a better way to have inter-service communication using service registry and discovery
Changes in CustomerController
Need to change return type of getCustomerById() method to CustomerDto.
@GetMapping("/{id}") public ResponseEntity<CustomerDto> getCustomerById(@PathVariable("id") Long customerId){ CustomerDto customer = customerService.getCustomerById(customerId); return ResponseEntity.ok(customer); }With these changes you should also get List of associated accounts along with customer information.
That's all for this topic Spring Boot Microservices Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!
>>>Return to Spring Tutorial Page
Related Topics
You may also like-
No comments:
Post a Comment