Part 3- D'une architecture monolithique vers une architecture micro-services : USE CASE
Part 3 : Architecture Micro-services avec Spring Cloud
Bonnes
pratiques des implémentations d’une Architecture basées sur les
Micro-services : USE CASE
Par
Mohamed YOUSSFI
Pour montrer comment mettre en
œuvre les bases d’une architecture micro-services, ce paragraphe présente un
exemple d’application qui se compose de deux micro-services. Un micro-services
qui permet gérer des clients « Customer-service » et un micro-service
qui permet de gérer des factures appartenant aux clients.
-
Architecture technique
du projet :
-
Composants principaux de
l’architecture :
A. Customer Service
a) Structure du Projet
- spring-boot-starter-data-jpa, pour l'utilisation de JPA, Hibernate et Spring Data
- spring-boot-starter-web, pour l'utilisation de Spring Web (RestController)
- spring-cloud-starter-netflix-eureka-client, pour permettre au micro-service de s'enregistrer dans Eureka Discovery Server
- h2 : In Memory Data base
- lombok : pour les Getters, Setters et Constructeurs
- mapstruct : pour le mapping des objets (Entités JPA <=> DTO)
- springdoc-open-api : pour générer la documentation des API Restful (Swagger)
- une configuration de plugin maven compiler pour compiler avec les processeurs lombok et mapstruct
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final
</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
a) Data Access Layer
- Entité JPA Customer
@Entity
@Data @NoArgsConstructor @AllArgsConstructor
public class Customer {
@Id
private String id;
private String name;
private String email;
}
- Interface CustomerRepository basée sur Spring Data
public interface CustomerRepository extends JpaRepository<Customer, String> {
}
a) DTO et Mappers Layer
- Les DTOs
@Data
@NoArgsConstructor @AllArgsConstructor
public class CustomerRequestDTO {
private String
id;
private String
name;
private String
email;
}
@Data
public class CustomerResponseDTO {
private String id;
private String name;
private String email;
}
- Interface de mapping mapstruct : Entité JPA <=> DTOs
@Mapper(componentModel
= "spring")
public interface CustomerMapper {
CustomerResponseDTO
customerToCustomerDTO(Customer
customer);
Customer customerRequestDtoToCustomer(CustomerRequestDTO
custReqDTO);
}
a)
Business Access
Layer
public interface CustomerService {
CustomerResponseDTO getCustomer(String id);
List<CustomerResponseDTO> getCustomers();
CustomerResponseDTO save(CustomerRequestDTO
custReqDTO);
CustomerResponseDTO update(CustomerRequestDTO
custReqDTO);
void deleteCustomer(String id);
}
@Service
@Transactional
public class CustomerServiceImpl implements
CustomerService {
private CustomerRepository
customerRepository;
private CustomerMapper
customerMapper;
public CustomerServiceImpl(CustomerRepository
customerRepository, CustomerMapper customerMapper)
{
this.customerRepository
= customerRepository;
this.customerMapper
= customerMapper;
}
@Override
public CustomerResponseDTO
getCustomer(String id)
{
Customer customer=customerRepository.findById(id).get();
return customerMapper.customerToCustomerDTO(customer);
}
@Override
public List<CustomerResponseDTO>
getCustomers() {
List<Customer>
customers=customerRepository.findAll();
return customers.stream().map((customer)->
customerMapper.customerToCustomerDTO(customer)).collect(Collectors.toList());
}
@Override
public CustomerResponseDTO
save(CustomerRequestDTO
customerRequestDTO) {
Customer customer=customerMapper.customerRequestDtoToCustomer(customerRequestDTO);
Customer
savedCustomer=customerRepository.save(customer);
return customerMapper.customerToCustomerDTO(savedCustomer);
}
@Override
public CustomerResponseDTO
update(CustomerRequestDTO
customerRequestDTO) {
Customer customer=customerMapper.customerRequestDtoToCustomer(customerRequestDTO);
Customer
savedCustomer=customerRepository.save(customer);
return customerMapper.customerToCustomerDTO(savedCustomer);
}
@Override
public void deleteCustomer(String
id) {
customerRepository.deleteById(id);
}
}
a)
Web API Layer
@RestController
@RequestMapping(path = "/api")
public class CustomerRestController {
private CustomerService
customerService;
public CustomerRestController(CustomerService
customerService) {
this.customerService
= customerService;
}
@GetMapping(path
= "/customers")
public List<CustomerResponseDTO>
customers(){
return customerService.getCustomers();
}
@GetMapping(path
= "/customers/{id}")
public CustomerResponseDTO
customerById(@PathVariable String
id){
return customerService.getCustomer(id);
}
@PostMapping(path = "/customers")
public CustomerResponseDTO
save(@RequestBody CustomerRequestDTO
customerRequestDTO){
return customerService.save(customerRequestDTO);
}
@PutMapping(path
= "/customers/{id}")
public CustomerResponseDTO
update(@RequestBody CustomerRequestDTO
custReqDTO, @PathVariable String
id){
customerRequestDTO.setId(id);
return customerService.save(customerRequestDTO);
}
@DeleteMapping(path
= "/customers/{id}")
public void delete(@PathVariable
String id){
customerService.deleteCustomer(id);
}
}
a)
Application Spring
Boot
@SpringBootApplication
public class CustomerServiceApplication {
public static
void main(String[]
args) {
SpringApplication.run(CustomerServiceApplication.class,
args);
}
@Bean
CommandLineRunner start(CustomerRepository
customerRepository){
return args
-> {
customerRepository.save(new Customer("001","Adria","med@adria.com"));
customerRepository.save(new Customer("002","LinkedIn","linked@adria.com"));
};
}
}
application.properties
spring.cloud.discovery.enabled=false
eureka.instance.prefer-ip-address=true
server.port=8082
spring.application.name=customer-service
spring.datasource.url=jdbc:h2:mem:customers-db
spring.h2.console.enabled=true
a)
Tests
A. Billing-Service
a) Structure du Projet
<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.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final
</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.2</version>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<!-- depending on your project -->
<target>1.8</target>
<!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final
</version>
</path>
<!-- other
annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
a) Data Acces Layer
@Entity
@Data @NoArgsConstructor
@AllArgsConstructor
public class Invoice {
@Id
private String
id;
private Date date;
private BigDecimal
amount;
private String
customerID;
@Transient
private Customer
customer;
}
public
interface InvoiceRepository extends JpaRepository<Invoice, String>
{
}
a) Open Feign Layer
@Data
public class Customer {
private String
id;
private String
name;
private String
email;
}
@FeignClient(name
= "CUSTOMER-SERVICE")
public interface CustomerServiceRestClient
{
@GetMapping(path="/api/customers/{id}")
Customer customerById(@PathVariable
String id);
@GetMapping(path="/api/customers")
List<Customer>
customers();
}
a) DTO et Mappers
@Data
@NoArgsConstructor @AllArgsConstructor
public class InvoiceRequestDTO {
private BigDecimal
amount;
private String
customerID;
}
@Data
@NoArgsConstructor @AllArgsConstructor
public class InvoiceResponseDTO {
private String
id;
private Date date;
private BigDecimal
amount;
private Customer
customer;
}
@Mapper(componentModel
= "spring")
public interface InvoiceMapper {
InvoiceResponseDTO
invoiceToInvoiceDTO(Invoice
invoice);
Invoice invoiceDTOtoInvoice(InvoiceRequestDTO
invoiceRequestDTO);
}
a) Business Layer
public
interface InvoiceService {
InvoiceResponseDTO
getInvoice(String id);
List<InvoiceResponseDTO>
listInvoices();
InvoiceResponseDTO
newInvoice(InvoiceRequestDTO invoiceRequestDTO);
}
@Service
@Transactional
public class InvoiceServiceImpl implements
InvoiceService {
private final InvoiceRepository
invoiceRepository;
private final CustomerServiceRestClient
customerServiceRestClient;
private final InvoiceMapper
invoiceMapper;
public InvoiceServiceImpl(InvoiceRepository
invoiceRepository, CustomerServiceRestClient
customerServiceRestClient, InvoiceMapper invoiceMapper)
{
this.invoiceRepository
= invoiceRepository;
this.customerServiceRestClient
= customerServiceRestClient;
this.invoiceMapper
= invoiceMapper;
}
@Override
public InvoiceResponseDTO
getInvoice(String id)
{
Invoice invoice=invoiceRepository.findById(id).orElse(null);
if(invoice==null) throw
new RuntimeException("Invoice
Not found");
Customer customer=customerServiceRestClient.customerById(invoice.getCustomerID());
invoice.setCustomer(customer);
return invoiceMapper.invoiceToInvoiceDTO(invoice);
}
@Override
public List<InvoiceResponseDTO>
listInvoices() {
List<Invoice>
invoices=invoiceRepository.findAll();
for(Invoice
invoice:invoices){
Customer customer=customerServiceRestClient.customerById(invoice.getCustomerID());
invoice.setCustomer(customer);
}
return invoices.stream().map((inv)->invoiceMapper.invoiceToInvoiceDTO(inv)).collect(Collectors.toList());
}
@Override
public InvoiceResponseDTO
newInvoice(InvoiceRequestDTO invoiceRequestDTO)
{
Customer customer;
try {
customer=customerServiceRestClient.customerById(invoiceRequestDTO.getCustomerID());
} catch (Exception
e){
throw new RuntimeException(e.getMessage());
}
Invoice invoice=invoiceMapper.invoiceDTOtoInvoice(invoiceRequestDTO);
invoice.setCustomer(customer);
invoice.setId(UUID.randomUUID().toString());
invoice.setDate(new Date());
Invoice
savedInvoice=invoiceRepository.save(invoice);
savedInvoice.setCustomer(customerServiceRestClient.customerById(savedInvoice.getCustomerID()));
return invoiceMapper.invoiceToInvoiceDTO(savedInvoice);
}
}
a) Web API Layer
@RestController
@RequestMapping(path = "/api")
public class InvoiceRestAPI {
private InvoiceService
invoiceService;
public InvoiceRestAPI(InvoiceService
invoiceService) {
this.invoiceService
= invoiceService;
}
@GetMapping(path
= "/invoices")
public List<InvoiceResponseDTO>
invoices(){
return invoiceService.listInvoices();
}
@GetMapping(path
= "/invoices/{id}")
public InvoiceResponseDTO
invoice(@PathVariable String
id){
return invoiceService.getInvoice(id);
}
@PostMapping(path="/invoices")
public InvoiceResponseDTO
save(@RequestBody InvoiceRequestDTO
invoiceRequestDTO){
return invoiceService.newInvoice(invoiceRequestDTO);
}
}
a) Application Spring Boot
@SpringBootApplication
@EnableFeignClients
public class BillingServiceApplication {
public static
void main(String[]
args) {
SpringApplication.run(BillingServiceApplication.class,
args);
}
@Bean
CommandLineRunner commandLineRunner(InvoiceService
invoiceService){
return args
-> {
invoiceService.newInvoice(new InvoiceRequestDTO(new BigDecimal(8700),"001"));
invoiceService.newInvoice(new InvoiceRequestDTO(new BigDecimal(5400),"001"));
};
}
}
Application.properties
spring.cloud.discovery.enabled=true
eureka.instance.prefer-ip-address=true
server.port=8083
spring.application.name=billing-service
spring.datasource.url=jdbc:h2:mem:billing-db
spring.h2.console.enabled=true
a) Tests
A. Eureka Discovery Service
@SpringBootApplication
@EnableEurekaServer
public class EurekaDiscoveryApplication {
public static
void main(String[]
args) {
SpringApplication.run(EurekaDiscoveryApplication.class,
args);
}
}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
server.port=8761
# dont register server itself as a client
eureka.client.fetch-registry=false
# Does not register itself in the service registry
eureka.client.register-with-eureka=false
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@SpringBootApplication
public class GatewayApplication
{
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
@Configuration
public class GatewayConfig {
@Bean
DiscoveryClientRouteDefinitionLocator
dcrdl(
ReactiveDiscoveryClient
rdc, DiscoveryLocatorProperties dlp) {
return new DiscoveryClientRouteDefinitionLocator(rdc,dlp);
}
}
spring.application.name=gateway
spring.cloud.discovery.enabled=true
server.port=8888
eureka.instance.prefer-ip-address=true
Commentaires
Enregistrer un commentaire