Я ділюсь своїми проектами на GitHub (@zeglami) — будь ласка, підпишись, щоб підтримати мене 😃
_Стаття була написана на основі мого старого репозиторію [spring-microservice_](https://github.com/zeglami/micro-service-spring)
1- Вступ
Архітектура мікросервісів — це техніка розробки програмного забезпечення, що структурує додаток як колекцію слабко зв'язаних сервісів. Кожен сервіс створений для виконання конкретної бізнес-функції і може розроблятися, розгортатися та масштабуватися незалежно. Цей підхід контрастує з традиційною монолітною архітектурою, де всі компоненти тісно інтегровані в один додаток.
1.1 — Слабко зв'язаний vs.
Тісно зв'язані системи
1.2 — Слабко зв'язані
// Приклад слабко зв'язаних систем
// Інтерфейс повідомлення
interface NotificationService {
void sendNotification(String message);
}
// Реалізація служби електронної пошти
class EmailService implements NotificationService {
public void sendNotification(String message) {
System.out.println("Відправляється email: " + message);
}
}
// Реалізація служби SMS
class SMSService implements NotificationService {
public void sendNotification(String message) {
System.out.println("Відправляється SMS: " + message);
}
}
// Служба користувачів, яка залежить від інтерфейсу повідомлень
class UserService {
private NotificationService notificationService;
public UserService(NotificationService notificationService) {
this.notificationService = notificationService; // Впровадження залежності
}
public void notifyUser(String message) {
notificationService.sendNotification(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService emailService = new EmailService();
UserService userService = new UserService(emailService);
userService.notifyUser("Ласкаво просимо до нашої служби!");
// Перехід до SMS служби
NotificationService smsService = new SMSService();
UserService userServiceSMS = new UserService(smsService);
userServiceSMS.notifyUser("Ласкаво просимо до нашої служби!");
}
}
Характеристики
Впровадження залежності: UserService приймає інтерфейс NotificationService як параметр конструктора, що дозволяє використовувати різні реалізації.
Легкість змін: Щоб змінити метод повідомлень, можна просто передати іншу реалізацію (наприклад, SMSService), не змінюючи UserService.
Полегшене тестування: UserService можна тестувати ізольовано, передавши мок-реалізацію NotificationService.
1.3 — Тісно зв'язаний
// Приклад тісно зв'язаних систем
class EmailService {
public void sendEmail(String message) {
System.out.println("Відправляється email: " + message);
}
}
class NotificationService {
private EmailService emailService = new EmailService(); // Пряма залежність
public void notifyUser(String message) {
emailService.sendEmail(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
notificationService.notifyUser("Ласкаво просимо до нашої служби!");
}
}
Характеристики
Пряма залежність: NotificationService безпосередньо створює екземпляр EmailService.
Складність змін: Якщо ми хочемо змінити метод повідомлень (наприклад, на SMS), потрібно змінювати клас NotificationService.
Проблеми з тестуванням: Тестування NotificationService ізольовано складне, оскільки він безпосередньо залежить від EmailService.
1.4 — Традиційний моноліт і мікросервіси
2 — Огляд проекту та діаграма архітектури
- API Gateway (Spring Cloud Gateway) надає єдину точку входу.
- Eureka Server виконує роль реєстру сервісів.
- Служба клієнтів (керує інформацією про клієнтів).
- Служба інвентаризації (керує продуктами на складі).
- Служба виставлення рахунків (керує замовленнями та оплатами).
Кожен сервіс має власну базу даних, що дозволяє їм працювати та масштабуватися незалежно.
3 — Основні мікросервіси в прикладі додатку
Джерело коду проекту: https://github.com/zeglami/micro-service-spring
Зверніть увагу, що це лише приклад і може не відповідати найкращим практикам; основна мета — продемонструвати архітектуру мікросервісів.
3.1 — Служба клієнтів
Керує всіма операціями, що стосуються клієнтів, такими як реєстрація та управління профілем.
// Використовує Spring Data JPA для зберігання даних.
// Відкриває REST API через Spring Data REST.
package me.zegit.customerservice;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/*
https://fr.wikipedia.org/wiki/HATEOAS
За замовчуванням ми отримуємо формат HATEOAS
Щоб використовувати API:
http://localhost:8081/customers?page=0&size=2
http://localhost:8081/customers
http://localhost:8081/customers/1
Щоб отримати доступ до бази даних (див. application.properties):
http://localhost:8081/h2-console/
*/
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String phone;
private String address;
}
@RepositoryRestResource
interface CustomerRepository extends JpaRepository {
// Користувацький метод для пошуку за ім'ям (часткове співпадіння)
// Наприклад
// GET /customers/search/findByNameContains?kw=l
// із "l" поверне клієнтів з "l" в їхньому імені
java.util.List findByNameContains(String kw);
}
// http://localhost:8081/customers/2?projection=p1
@Projection(name = "p1", types = Customer.class)
interface CustomerProjection {
Long getId();
String getName();
}
// Нова проекція з більше полями
// http://localhost:8081/customers/1?projection=p2
@Projection(name = "p2", types = Customer.class)
interface CustomerProjection2 {
Long getId();
String getName();
String getEmail();
String getPhone();
String getAddress();
}
@SpringBootApplication
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
@Bean
CommandLineRunner start(CustomerRepository customerRepository,
RepositoryRestConfiguration repositoryRestConfiguration) {
return args -> {
repositoryRestConfiguration.exposeIdsFor(Customer.class);
customerRepository.save(new Customer(null, "Abdel", "[email protected]", "111-111-1111", "123 Main St"));
customerRepository.save(new Customer(null, "Ben", "[email protected]", "222-222-2222", "456 Elm St"));
customerRepository.save(new Customer(null, "Antho", "[email protected]", "333-333-3333", "789 Oak Ave"));
customerRepository.save(new Customer(null, "Linda", "[email protected]", "444-444-4444", "321 Pine Rd"));
customerRepository.findAll().forEach(System.out::println);
};
}
}
```
### application.properties
################################
#База Даних
################################
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:inventory;DB_CLOSE_DELAY=- 1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
################################
#Інше
################################
#spring.cloud.discovery.enabled=false за замовчуванням spring буде шукати eureka, щоб зареєструвати, тому встановіть false, якщо не хочете
spring.cloud.discovery.enabled=true
server.port=8081
spring.application.name=customer-service
#management.endpoints.web.exposure.include=*
################################
#Eureka
################################
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
3.2 — Inventory Service
// Обробляє інформацію про продукти.
// Підтримує каталог продуктів.
// Використовує базу даних H2 в пам'яті
package me.zegit.inventoryservice;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/*
Для тестування API (HATEOAS за замовчуванням):
http://localhost:8082/products
http://localhost:8082/products/1
Для консолі бази даних (див. application.properties):
http://localhost:8082/h2-console/
*/
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// Додаткові опційні поля (можете видалити, якщо не потрібні)
private String description;
private String category;
}
@RepositoryRestResource
interface ProductRepository extends JpaRepository {
// Демонстрація власного методу для пошуку за підрядком у назві (необов'язково)
// напр. GET /products/search/findByNameContains?kw=ball
java.util.List findByNameContains(String kw);
}
// Приклад короткої проекції: p1
// http://localhost:8082/products/1?projection=p1
@Projection(name = "p1", types = Product.class)
interface ProductProjectionShort {
Long getId();
String getName();
double getPrice();
}
// Приклад детальної проекції: p2
// http://localhost:8082/products/1?projection=p2
@Projection(name = "p2", types = Product.class)
interface ProductProjectionDetailed {
Long getId();
String getName();
double getPrice();
String getDescription();
String getCategory();
}
@SpringBootApplication
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
@Bean
CommandLineRunner start(ProductRepository productRepository,
RepositoryRestConfiguration repositoryRestConfiguration) {
return args -> {
// Відкриває ідентифікатори продуктів у REST відповідях
repositoryRestConfiguration.exposeIdsFor(Product.class);
// Створює продукти, пов'язані з тенісом
productRepository.save(new Product(null, "Tennis ball", 2, "High-quality felt ball", "Tennis"));
productRepository.save(new Product(null, "Racket", 120, "Pro-level tennis racket", "Tennis"));
productRepository.save(new Product(null, "Tennis trainer", 25, "Solo training device", "Tennis"));
// Виводить усі збережені продукти в консолі
productRepository.findAll().forEach(System.out::println);
};
}
}
### application.properties
################################
#База даних
################################
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:inventory;DB_CLOSE_DELAY=- 1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.cloud.discovery.enabled=true
server.port=8082
spring.application.name=inventory-service
#management.endpoints.web.exposure.include=*
################################
#Eureka
################################
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
3.3 — Billing Service
// Обробляє інформацію про продукти.
// Підтримує каталог продуктів.
// Використовує клієнтів Feign для комунікації з сервісами Customer та Inventory.
// Формує інформацію про рахунок, отримуючи дані з інших сервісів
/*
Він отримує продукти з оновленого InventoryService з вашими новими тенісними товарами.
Створює рахунок для конкретного клієнта.
Пов'язує кожен отриманий продукт з цим рахунком.
// Використовує базу даних H2 в пам'яті
package me.zegit.billingservice;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import javax.persistence.*;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class Bill {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Date billingDate;
@Transient
private Customer customer;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private long customerID;
@OneToMany(mappedBy = "bill")
private Collection productItems;
}
@RepositoryRestResource
interface BillRepository extends JpaRepository {
}
@Projection(name="full", types=Bill.class)
interface BillProjection {
Long getId();
Date getBillingDate();
Long getCustomerID();
Collection getProductItems();
}
@RepositoryRestResource
interface ProductItemRepository extends JpaRepository {
List findByBillId(Long billID);
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class ProductItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Transient
private Product product;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private long productID;
private double price;
private double quantity;
@ManyToOne
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private Bill bill;
}
@Data
class Customer {
private Long id;
private String name;
private String email;
}
@FeignClient(name = "CUSTOMER-SERVICE")
interface CustomerService {
@GetMapping("/customers/{id}")
Customer findCustomerById(@PathVariable(name="id") Long id);
}
@Data
class Product {
private Long id;
private String name;
private double price;
}
@FeignClient(name = "INVENTORY-SERVICE")
interface InventoryService {
@GetMapping("/products/{id}")
Product findProductById(@PathVariable(name="id") Long id);
@GetMapping("/products")
PagedModel findAllProducts();
}
@SpringBootApplication
@EnableFeignClients
public class BillingServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BillingServiceApplication.class, args);
}
@Bean
CommandLineRunner start(
BillRepository billRepository,
ProductItemRepository productItemRepository,
CustomerService customerService,
InventoryService inventoryService
) {
return args -> {
// Отримуємо клієнта (припускаємо, що є клієнт з ID=1 в CustomerService)
Customer c1 = customerService.findCustomerById(1L);
System.out.println("----------------------------------------");
System.out.println("Отриманий клієнт: " + c1.getName() + " (" + c1.getEmail() + ")");
System.out.println("----------------------------------------");
// Створюємо новий рахунок для цього клієнта
// Формує рахунок, отримуючи дані з інших сервісів
/*
Отримує продукти з (оновленого) InventoryService з вашими новими тенісними товарами.
Створює рахунок для вказаного клієнта.
Пов’язує кожен отриманий продукт з цим рахунком.
*/
package me.zegit.billingservice;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.hateoas.PagedModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.persistence.*;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class Bill {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Date billingDate;
@Transient
private Customer customer;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private long customerID;
@OneToMany(mappedBy = "bill")
private Collection productItems;
}
@RepositoryRestResource
interface BillRepository extends JpaRepository {
}
@Projection(name="full", types=Bill.class)
interface BillProjection {
Long getId();
Date getBillingDate();
Long getCustomerID();
Collection getProductItems();
}
@RepositoryRestResource
interface ProductItemRepository extends JpaRepository {
List findByBillId(Long billID);
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class ProductItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Transient
private Product product;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private long productID;
private double price;
private double quantity;
@ManyToOne
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private Bill bill;
}
@Data
class Customer {
private Long id;
private String name;
private String email;
}
@FeignClient(name = "CUSTOMER-SERVICE")
interface CustomerService {
@GetMapping("/customers/{id}")
Customer findCustomerById(@PathVariable(name="id") Long id);
}
@Data
class Product {
private Long id;
private String name;
private double price;
}
@FeignClient(name = "INVENTORY-SERVICE")
interface InventoryService {
@GetMapping("/products/{id}")
Product findProductById(@PathVariable(name="id") Long id);
@GetMapping("/products")
PagedModel findAllProducts();
}
@SpringBootApplication
@EnableFeignClients
public class BillingServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BillingServiceApplication.class, args);
}
@Bean
CommandLineRunner start(
BillRepository billRepository,
ProductItemRepository productItemRepository,
CustomerService customerService,
InventoryService inventoryService
) {
return args -> {
// Отримуємо клієнта (припускаємо, що є клієнт з ID=1 в CustomerService)
Customer c1 = customerService.findCustomerById(1L);
System.out.println("----------------------------------------");
System.out.println("Отриманий клієнт: " + c1.getName() + " (" + c1.getEmail() + ")");
System.out.println("----------------------------------------");
// Створюємо новий рахунок для цього клієнта
## application.properties
################################
#База даних
################################
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:billing;DB_CLOSE_DELAY=- 1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
################################
#Інше
################################
#spring.cloud.discovery.enabled=false за замовчуванням spring буде шукати еureka, щоб бути зареєстрованим, тому false, якщо ви не хочете
spring.cloud.discovery.enabled=true
server.port=8084
spring.application.name=billig-service
#management.endpoints.web.exposure.include=*
################################
#Eureka
################################
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
3.4 — API Gateway
@SpringBootApplication
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
@Bean
RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
// Існуючий маршрут до публічного API для міст
.route(r -> r.path("/publicCities/**")
.filters(f -> f
.addRequestHeader("X-RapidAPI-Key", "YOUR_RAPIDAPI_KEY")
.addRequestHeader("X-RapidAPI-Host", "wft-geo-db.p.rapidapi.com")
.rewritePath("/publicCities/(?.*)", "/${segment}")
)
.uri("https://wft-geo-db.p.rapidapi.com/v1/geo/cities"))
// публічні маршрути API
.route(r -> r.path("/covidStats/**")
.filters(f -> f
.addRequestHeader("X-RapidAPI-Key", "YOUR_RAPIDAPI_KEY")
.addRequestHeader("X-RapidAPI-Host", "covid-193.p.rapidapi.com")
.rewritePath("/covidStats/(?.*)", "/${segment}")
)
.uri("https://covid-193.p.rapidapi.com"))
.route(r -> r.path("/weather/**")
.filters(f -> f
.addRequestHeader("X-RapidAPI-Key", "YOUR_RAPIDAPI_KEY")
.addRequestHeader("X-RapidAPI-Host", "weatherapi-com.p.rapidapi.com")
.rewritePath("/weather/(?.*)", "/${segment}")
)
.uri("https://weatherapi-com.p.rapidapi.com"))
.build();
}
@Bean
DiscoveryClientRouteDefinitionLocator dynamicRoutes(ReactiveDiscoveryClient rdc, DiscoveryLocatorProperties dlp) {
return new DiscoveryClientRouteDefinitionLocator(rdc, dlp);
}
}
//статичне маршрутизування
//lb: балансувальник навантаження
- Що таке балансування навантаження за методом Round-Robin? Балансування навантаження за методом Round-Robin — це один з найпростіших методів розподілу запитів клієнтів між групою серверів.
- Перебираючи список серверів у групі, балансувальник навантаження за методом Round-Robin
- перенаправляє запит клієнта до кожного сервера по черзі.
- //.route(r -> r.path("/products/**").uri("lb://INVENTORY-SERVICE"))
- */
### application.properties
spring.application.name=gateway-service
server.port=8888
spring.cloud.discovery.enabled=true
management.endpoints.web.exposure.include=*
## 3.5 — Виявлення сервісів за допомогою Eureka
/* Сервер Eureka дозволяє кожному мікросервісу реєструвати себе при запуску.
Інші сервіси можуть потім знайти та взаємодіяти з ним без необхідності вказувати URL-адреси в коді.
За замовчуванням працює на порту 8761.
Надає панель управління для перегляду зареєстрованих сервісів та їхнього статусу здоров’я.
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceApplication.class, args);
}
}
```
### application.properties
spring.application.name=eureka-service
server.port=8761
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=true
4 — Розгортання: Поради
Порядок запуску
– Сервер Eureka має запускатися першим.
– Мікросервіси реєструються з Eureka після його запуску.
– API Gateway може запускатися після того, як Eureka буде доступний.
Налаштування
– Кожен сервіс має свій власний файл application.yml/properties.
– Для більш складних налаштувань ви можете централізувати конфігурації за допомогою Spring Cloud Config.
Масштабування
– Ви можете запустити кілька екземплярів сервісів з високим попитом (наприклад, Inventory Service) за допомогою Gateway.
5 — Висновок
Код, представлений тут, є відправною точкою для тих, хто хоче почати роботу з мікросервісами за допомогою Spring Boot і Spring Cloud. Дотримуючись найкращих практик — таких як впровадження API Gateway, використання Eureka Server для виявлення сервісів та використання Feign клієнтів для комунікації — ви зможете створити надійну, модульну систему, яку буде легше підтримувати та розвивати з часом.
Перекладено з: Microservices Architecture : Designing Scalable Java Applications with Spring