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


 
a)      Dépendances Maven :

Les dépendances principales du projets sont :
  • 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

<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.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>
       
<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


Dépendances Maven


<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




Spring Cloud Gateway

<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

Posts les plus consultés de ce blog

Part 1- D’une Architecture Monolithique Vers une Architecture micro-services

Part 2 : D'une architecture monolithique vers une architecture micro-services

Prise en main de Spring Boot : Premier Service backend