Spring Modulith: Kiến trúc Monolith Mô-đun Giải thích

Học Spring Modulith để xây dựng monolith mô-đun trong Java. Kiến trúc, mô-đun, sự kiện bất đồng bộ và testing với ví dụ Spring Boot 3.

Spring Modulith: kiến trúc monolith mô-đun với Spring Boot

Spring Modulith mang đến cách tiếp cận thực dụng để cấu trúc các ứng dụng Spring Boot thành những mô-đun nghiệp vụ gắn kết. Kiến trúc này đặt monolith mô-đun nằm giữa monolith truyền thống và microservice, cung cấp tính mô-đun mạnh mẽ mà không kéo theo độ phức tạp vận hành của hệ thống phân tán.

Điểm Cốt lõi

Spring Modulith chính thức hóa các thực hành tốt của kiến trúc hexagonal và Domain-Driven Design ngay trong Spring Boot, kèm xác minh tự động phụ thuộc giữa các mô-đun.

Tại sao chọn Monolith Mô-đun?

Vấn đề của Monolith Cổ điển

Các monolith truyền thống chịu hậu quả từ sự gắn kết quá mức giữa các thành phần. Theo thời gian, các phụ thuộc chéo tích tụ và biến ứng dụng thành một "big ball of mud" không thể bảo trì. Một thay đổi trong mô-đun thanh toán tác động đến mô-đun người dùng, rồi đến mô-đun thông báo, sinh ra các tác dụng phụ khó lường.

AntiPattern.javajava
// Direct coupling between modules - AVOID THIS
@Service
public class OrderService {

    // Direct dependencies to other modules
    // Creates tight coupling and dependency cycles
    private final UserRepository userRepository;
    private final InventoryService inventoryService;
    private final PaymentProcessor paymentProcessor;
    private final NotificationService notificationService;
    private final ShippingCalculator shippingCalculator;

    public OrderService(UserRepository userRepository,
                        InventoryService inventoryService,
                        PaymentProcessor paymentProcessor,
                        NotificationService notificationService,
                        ShippingCalculator shippingCalculator) {
        this.userRepository = userRepository;
        this.inventoryService = inventoryService;
        this.paymentProcessor = paymentProcessor;
        this.notificationService = notificationService;
        this.shippingCalculator = shippingCalculator;
    }

    public Order createOrder(OrderRequest request) {
        // This service knows too many implementation details
        User user = userRepository.findById(request.userId()).orElseThrow();
        inventoryService.reserveItems(request.items());
        BigDecimal shipping = shippingCalculator.calculate(user.getAddress());
        paymentProcessor.charge(user, request.total().add(shipping));
        notificationService.sendOrderConfirmation(user, request);
        // ...
        return null;
    }
}

Mẫu hình này tạo ra những vấn đề cụ thể: kiểm thử tích hợp dễ vỡ, khó suy luận về tác động của thay đổi và không thể triển khai hay phát triển một mô-đun độc lập.

Microservice Không phải Lúc nào cũng là Câu trả lời

Microservice giải quyết bài toán gắn kết, nhưng đem đến độ phức tạp vận hành đáng kể: giao tiếp qua mạng, eventual consistency, triển khai phân tán, observability đa dịch vụ. Với nhiều đội ngũ, độ phức tạp này không tương xứng với lợi ích thu được.

Monolith mô-đun mang đến một lựa chọn thay thế: một đơn vị triển khai duy nhất với ranh giới mô-đun được định nghĩa và thực thi rõ ràng. Spring Modulith tự động hóa việc kiểm tra các ranh giới đó.

Bắt đầu với Spring Modulith

Cấu hình Dự án

Tích hợp Spring Modulith vào dự án Spring Boot 3 yêu cầu một số phụ thuộc Maven. Starter chính kích hoạt phát hiện mô-đun tự động.

xml
<!-- pom.xml -->
<!-- Spring Modulith dependencies for Spring Boot 3.2+ -->
<dependencies>
    <!-- Core Spring Modulith -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-core</artifactId>
    </dependency>

    <!-- Async event support with persistence -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-jpa</artifactId>
    </dependency>

    <!-- Module structure tests -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Automatic documentation generation -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-docs</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.modulith</groupId>
            <artifactId>spring-modulith-bom</artifactId>
            <version>1.3.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Cấu trúc Mô-đun

Spring Modulith tự động phát hiện các mô-đun dựa trên các package trực tiếp dưới package chính của ứng dụng. Mỗi sub-package đại diện cho một mô-đun riêng biệt với trách nhiệm của riêng nó.

text
com.example.shop/
├── ShopApplication.java        # Spring Boot entry point
├── order/                       # Order Module
│   ├── Order.java              # Public entity (module API)
│   ├── OrderService.java       # Public service
│   ├── internal/               # Module-internal package
│   │   ├── OrderRepository.java
│   │   └── OrderValidator.java
│   └── OrderCreatedEvent.java  # Published event
├── inventory/                   # Inventory Module
│   ├── InventoryService.java
│   ├── Product.java
│   └── internal/
│       └── StockRepository.java
├── customer/                    # Customer Module
│   ├── Customer.java
│   ├── CustomerService.java
│   └── internal/
│       └── CustomerRepository.java
└── notification/                # Notification Module
    ├── NotificationService.java
    └── internal/
        └── EmailSender.java

Quy ước này thiết lập một quy tắc cốt lõi: chỉ những class nằm trong package gốc của mô-đun (không phải trong internal/) mới tạo thành API công khai mà các mô-đun khác có thể truy cập.

Order.javajava
// Public entity of Order module - accessible from other modules
package com.example.shop.order;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "orders")
public class Order {

    @Id
    private UUID id;

    // Reference by ID rather than entity
    // Avoids direct coupling with Customer module
    private UUID customerId;

    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    private LocalDateTime createdAt;

    protected Order() {
        // JPA constructor
    }

    public Order(UUID customerId, BigDecimal totalAmount) {
        this.id = UUID.randomUUID();
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }

    // Public getters - part of module API
    public UUID getId() { return id; }
    public UUID getCustomerId() { return customerId; }
    public BigDecimal getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
    public LocalDateTime getCreatedAt() { return createdAt; }

    // Encapsulated business methods
    void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}
OrderRepository.javajava
// Internal repository - NOT accessible from other modules
package com.example.shop.order.internal;

import com.example.shop.order.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

// This repository is in the internal package
// Spring Modulith prohibits access from other modules
interface OrderRepository extends JpaRepository<Order, UUID> {

    // Methods specific to the Order module
    List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}

Repository giữ ở mức nội bộ vì truy cập dữ liệu phải đi qua service công khai, đảm bảo việc đóng gói logic nghiệp vụ.

Quy ước Đặt tên

Package internal không có gì đặc biệt với Java. Đó là quy ước được Spring Modulith nhận biết và xác minh tự động trong các kiểm thử. Mọi vi phạm đều phát sinh lỗi tường minh.

Giao tiếp Giữa các Mô-đun

Sự kiện Domain

Giao tiếp giữa các mô-đun diễn ra qua sự kiện domain thay vì các lệnh gọi trực tiếp. Mẫu hình này tách rời mô-đun phát đi khỏi mô-đun nhận sự kiện.

OrderCreatedEvent.javajava
// Event published by Order module
package com.example.shop.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

// Immutable record representing the event
// Contains only information needed by consumers
public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    BigDecimal totalAmount,
    LocalDateTime createdAt
) {
    // Factory method to create event from entity
    public static OrderCreatedEvent from(Order order) {
        return new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            order.getCreatedAt()
        );
    }
}
OrderService.javajava
// Public service that publishes events
package com.example.shop.order;

import com.example.shop.order.internal.OrderRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.UUID;

@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(OrderRepository orderRepository,
                        ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    public Order createOrder(UUID customerId, BigDecimal amount) {
        // Create the order
        Order order = new Order(customerId, amount);
        order = orderRepository.save(order);

        // Publish the event
        // Interested modules will react asynchronously
        eventPublisher.publishEvent(OrderCreatedEvent.from(order));

        return order;
    }

    public Order confirmOrder(UUID orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        order.confirm();

        // Confirmation event
        eventPublisher.publishEvent(new OrderConfirmedEvent(
            order.getId(),
            order.getCustomerId()
        ));

        return order;
    }
}

Các mô-đun khác tiêu thụ những sự kiện này mà không cần biết chi tiết triển khai của mô-đun Order.

NotificationEventListener.javajava
// Notification module consuming Order events
package com.example.shop.notification.internal;

import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderConfirmedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
class NotificationEventListener {

    private final EmailSender emailSender;
    private final CustomerLookup customerLookup;

    NotificationEventListener(EmailSender emailSender,
                               CustomerLookup customerLookup) {
        this.emailSender = emailSender;
        this.customerLookup = customerLookup;
    }

    // @ApplicationModuleListener guarantees async processing
    // and event persistence for retry on failure
    @ApplicationModuleListener
    void onOrderCreated(OrderCreatedEvent event) {
        // Retrieve email via local interface
        // Avoids direct dependency on Customer module
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Order Received",
            "Your order #%s has been received.".formatted(event.orderId())
        );
    }

    @ApplicationModuleListener
    void onOrderConfirmed(OrderConfirmedEvent event) {
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Order Confirmed",
            "Your order #%s is confirmed and being prepared."
                .formatted(event.orderId())
        );
    }
}

Sẵn sàng chinh phục phỏng vấn Spring Boot?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Sự kiện Bền vững và Bất đồng bộ

Spring Modulith cung cấp một tính năng mạnh mẽ: lưu trữ sự kiện. Sự kiện được ghi vào cơ sở dữ liệu trước khi công bố, đảm bảo việc xử lý ngay cả khi ứng dụng gặp sự cố.

EventPublicationConfig.javajava
// Persisted events configuration
package com.example.shop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.config.EnablePersistentDomainEvents;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
@EnablePersistentDomainEvents  // Enables event persistence
public class EventPublicationConfig {

    // Spring Modulith automatically creates required tables
    // EVENT_PUBLICATION stores pending events
    // Processed events are marked as completed
}
InventoryEventListener.javajava
// Listener with transactional event handling
package com.example.shop.inventory.internal;

import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderCancelledEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
class InventoryEventListener {

    private final StockRepository stockRepository;

    InventoryEventListener(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    // Transactional processing - if exception thrown,
    // event will be retried automatically
    @ApplicationModuleListener
    @Transactional
    void onOrderCreated(OrderCreatedEvent event) {
        // Reserve stock for this order
        // On failure, event remains in EVENT_PUBLICATION
        // and will be reprocessed in next cycle
        reserveStockForOrder(event.orderId(), event.items());
    }

    @ApplicationModuleListener
    @Transactional
    void onOrderCancelled(OrderCancelledEvent event) {
        // Release reserved stock
        releaseStockForOrder(event.orderId());
    }

    private void reserveStockForOrder(UUID orderId, List<OrderItem> items) {
        for (OrderItem item : items) {
            Stock stock = stockRepository.findByProductId(item.productId())
                .orElseThrow(() -> new StockNotFoundException(item.productId()));

            stock.reserve(item.quantity());
            stockRepository.save(stock);
        }
    }

    private void releaseStockForOrder(UUID orderId) {
        // Stock release implementation
    }
}

Bảng EVENT_PUBLICATION được Spring Modulith tự động tạo:

sql
-- EVENT_PUBLICATION table structure (PostgreSQL)
CREATE TABLE event_publication (
    id UUID PRIMARY KEY,
    listener_id VARCHAR(512) NOT NULL,
    event_type VARCHAR(512) NOT NULL,
    serialized_event TEXT NOT NULL,
    publication_date TIMESTAMP NOT NULL,
    completion_date TIMESTAMP
);

-- Index for retry queries
CREATE INDEX idx_event_publication_incomplete
ON event_publication (completion_date)
WHERE completion_date IS NULL;

Giao diện Mở giữa các Mô-đun

Khi một mô-đun cần thông tin từ mô-đun khác mà không dùng sự kiện, một interface công khai trong mô-đun nguồn cho phép giữ mức gắn kết tối thiểu.

CustomerLookup.javajava
// Public interface of Customer module
package com.example.shop.customer;

import java.util.Optional;
import java.util.UUID;

// Interface exposed to other modules
// Defines contract without exposing implementation details
public interface CustomerLookup {

    Optional<String> findEmailById(UUID customerId);

    boolean exists(UUID customerId);

    // Specific DTO for shared information
    Optional<CustomerInfo> findInfoById(UUID customerId);

    record CustomerInfo(
        UUID id,
        String email,
        String fullName,
        String preferredLanguage
    ) {}
}
CustomerLookupImpl.javajava
// Internal implementation
package com.example.shop.customer.internal;

import com.example.shop.customer.CustomerLookup;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;

@Component
class CustomerLookupImpl implements CustomerLookup {

    private final CustomerRepository customerRepository;

    CustomerLookupImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public Optional<String> findEmailById(UUID customerId) {
        return customerRepository.findById(customerId)
            .map(Customer::getEmail);
    }

    @Override
    public boolean exists(UUID customerId) {
        return customerRepository.existsById(customerId);
    }

    @Override
    public Optional<CustomerInfo> findInfoById(UUID customerId) {
        return customerRepository.findById(customerId)
            .map(customer -> new CustomerInfo(
                customer.getId(),
                customer.getEmail(),
                customer.getFullName(),
                customer.getPreferredLanguage()
            ));
    }
}

Cách tiếp cận này cho phép mô-đun Notification truy cập thông tin khách hàng mà không phụ thuộc trực tiếp vào repository hay entity Customer.

Kiểm thử Cấu trúc Mô-đun

Xác minh Phụ thuộc Tự động

Spring Modulith cung cấp công cụ kiểm thử để xác minh các quy tắc kiến trúc được tuân thủ. Các kiểm thử này thất bại khi một mô-đun truy cập class nội bộ của mô-đun khác.

ModularityTests.javajava
// Modular architecture verification tests
package com.example.shop;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;

class ModularityTests {

    // Load application module structure
    private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

    @Test
    void verifyModularStructure() {
        // Verify all modules are correctly structured
        // Fails if a module accesses internal packages of another
        modules.verify();
    }

    @Test
    void printModuleOverview() {
        // Print module structure to console
        // Useful for understanding dependencies
        modules.forEach(System.out::println);
    }

    @Test
    void createModuleDocumentation() {
        // Generate automatic module documentation
        // Includes dependency diagrams
        new Documenter(modules)
            .writeModulesAsPlantUml()
            .writeIndividualModulesAsPlantUml();
    }

    @Test
    void detectCyclicDependencies() {
        // The verify() method also detects cycles
        // Module A → Module B → Module C → Module A = failure
        modules.verify();
    }
}

Việc chạy modules.verify() phân tích bytecode và phát hiện:

  • Truy cập đến package internal từ mô-đun khác
  • Phụ thuộc vòng giữa các mô-đun
  • Vi phạm quy tắc đóng gói

Kiểm thử Tích hợp theo Mô-đun

Spring Modulith cho phép kiểm thử từng mô-đun một cách cô lập, chỉ tải các bean cần thiết.

OrderModuleIntegrationTests.javajava
// Order module integration test in isolation
package com.example.shop.order;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import java.math.BigDecimal;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@ApplicationModuleTest  // Load only Order module and its dependencies
class OrderModuleIntegrationTests {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldCreateOrder() {
        // Given
        UUID customerId = UUID.randomUUID();
        BigDecimal amount = new BigDecimal("99.99");

        // When
        Order order = orderService.createOrder(customerId, amount);

        // Then
        assertThat(order.getId()).isNotNull();
        assertThat(order.getCustomerId()).isEqualTo(customerId);
        assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
    }

    @Test
    void shouldPublishEventOnOrderCreation(Scenario scenario) {
        // Given
        UUID customerId = UUID.randomUUID();

        // When / Then - verify event is published
        scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
            .andWaitForEventOfType(OrderCreatedEvent.class)
            .matching(event -> event.customerId().equals(customerId))
            .toArriveAndVerify(event -> {
                assertThat(event.orderId()).isNotNull();
                assertThat(event.totalAmount()).isEqualTo(BigDecimal.TEN);
            });
    }

    @Test
    void shouldHandleOrderConfirmation(Scenario scenario) {
        // Given - create an order
        Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN);

        // When / Then - confirm and verify event
        scenario.stimulate(() -> orderService.confirmOrder(order.getId()))
            .andWaitForEventOfType(OrderConfirmedEvent.class)
            .toArriveAndVerify(event -> {
                assertThat(event.orderId()).isEqualTo(order.getId());
            });
    }
}

Annotation @ApplicationModuleTest tự động cấu hình:

  • Chỉ tải các bean của mô-đun Order
  • Các mock cho phụ thuộc đến mô-đun khác
  • Hạ tầng kiểm thử cho sự kiện
OrderNotificationIntegrationTest.javajava
// Inter-module integration test
package com.example.shop;

import com.example.shop.order.OrderService;
import com.example.shop.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
import java.math.BigDecimal;
import java.util.UUID;

// DIRECT loads all directly dependent modules
@ApplicationModuleTest(BootstrapMode.DIRECT)
class OrderNotificationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldTriggerNotificationOnOrderCreated(Scenario scenario) {
        // This test verifies Order → Notification integration
        UUID customerId = UUID.randomUUID();

        scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
            .andWaitForEventOfType(OrderCreatedEvent.class)
            .toArriveAndVerify(event -> {
                // Event was processed by NotificationEventListener
                // Test verifies email was sent
            });
    }
}
Cô lập Kiểm thử

Dùng BootstrapMode.STANDALONE (mặc định) cho unit test mô-đun. Dành BootstrapMode.ALL_DEPENDENCIES cho các kiểm thử tích hợp đầu-cuối để tránh các phụ thuộc ẩn.

Cấu hình Mô-đun Nâng cao

Mô-đun Tường minh với @ApplicationModule

Với các trường hợp phức tạp, annotation @ApplicationModule cho phép cấu hình tường minh các quy tắc của mô-đun.

package-info.javajava
// Explicit Order module configuration
@org.springframework.modulith.ApplicationModule(
    // Modules allowed to depend on this one
    allowedDependencies = {"customer", "inventory"},
    // Module type: OPEN (free access) or CLOSED (explicit API)
    type = Type.CLOSED
)
package com.example.shop.order;

import org.springframework.modulith.ApplicationModule.Type;
NamedInterface.javajava
// Named interface definition for finer API control
package com.example.shop.order;

import org.springframework.modulith.NamedInterface;

// Exposes only certain classes as public API
@NamedInterface("order-api")
public class OrderApi {
    // Classes in this package are accessible via "order-api"
}
package-info.javajava
// Module depending on a specific named interface
@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "order::order-api"  // Access limited to named API
)
package com.example.shop.shipping;

Xử lý Phụ thuộc Vòng

Phụ thuộc vòng giữa các mô-đun thường cho thấy vấn đề thiết kế. Spring Modulith phát hiện chúng và làm xác minh thất bại. Giải pháp thường là tách ra một mô-đun mới hoặc dùng sự kiện.

java
// BEFORE - Circular dependency
// Order → Inventory (to check stock)
// Inventory → Order (to know current orders)

// AFTER - Resolution through events
// Order publishes OrderCreatedEvent
// Inventory listens and reserves stock
// Inventory publishes StockReservedEvent
// Order listens and confirms availability
StockReservedEvent.javajava
// Event published by Inventory
package com.example.shop.inventory;

import java.util.UUID;

public record StockReservedEvent(
    UUID orderId,
    UUID productId,
    int quantity,
    boolean success,
    String failureReason
) {}
OrderStockListener.javajava
// Order module listens to Inventory events
package com.example.shop.order.internal;

import com.example.shop.inventory.StockReservedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
class OrderStockListener {

    private final OrderRepository orderRepository;

    OrderStockListener(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @ApplicationModuleListener
    void onStockReserved(StockReservedEvent event) {
        Order order = orderRepository.findById(event.orderId())
            .orElseThrow();

        if (event.success()) {
            order.markStockReserved();
        } else {
            order.markStockUnavailable(event.failureReason());
        }

        orderRepository.save(order);
    }
}

Khả năng Quan sát và Giám sát

Tracing Sự kiện

Spring Modulith tích hợp với Micrometer cho distributed tracing các sự kiện giữa các mô-đun.

ObservabilityConfig.javajava
// Module observability configuration
package com.example.shop.config;

import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModuleEventListener;

@Configuration
public class ObservabilityConfig {

    @Bean
    ModuleEventListener moduleEventListener(ObservationRegistry registry) {
        // Adds spans for each processed event
        return new ModuleEventListener(registry);
    }
}
yaml
# application.yml
# Observability configuration
management:
  tracing:
    sampling:
      probability: 1.0  # Trace all events in dev
  endpoints:
    web:
      exposure:
        include: health,info,metrics,modulith

spring:
  modulith:
    events:
      # Retry interval for failed events
      republish-outstanding-events-on-restart: true
      # Retention duration for completed events
      completion-mode: DELETE  # or ARCHIVE

Actuator Endpoint cho Mô-đun

Spring Modulith mở một Actuator endpoint để hiển thị trạng thái mô-đun trên môi trường production.

ModulithActuatorConfig.javajava
// Actuator endpoint activation
package com.example.shop.config;

import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
import org.springframework.modulith.core.ApplicationModules;

@Configuration
public class ModulithActuatorConfig {

    @Bean
    @ConditionalOnAvailableEndpoint
    ApplicationModulesEndpoint modulesEndpoint(ApplicationModules modules) {
        return new ApplicationModulesEndpoint(modules);
    }
}

Endpoint /actuator/modulith trả về:

json
{
  "modules": [
    {
      "name": "order",
      "basePackage": "com.example.shop.order",
      "dependencies": ["customer"],
      "publishedEvents": [
        "com.example.shop.order.OrderCreatedEvent",
        "com.example.shop.order.OrderConfirmedEvent"
      ],
      "listenedEvents": [
        "com.example.shop.inventory.StockReservedEvent"
      ]
    },
    {
      "name": "inventory",
      "basePackage": "com.example.shop.inventory",
      "dependencies": [],
      "publishedEvents": [
        "com.example.shop.inventory.StockReservedEvent"
      ],
      "listenedEvents": [
        "com.example.shop.order.OrderCreatedEvent"
      ]
    }
  ]
}

Di chuyển sang Microservice

Chuẩn bị cho Việc Tách

Kiến trúc mô-đun giúp việc tách thành microservice trong tương lai trở nên dễ dàng. Mỗi mô-đun trở thành ứng viên tự nhiên để tách.

ExtractionReadinessChecker.javajava
// Extraction readiness verification
package com.example.shop;

import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ApplicationModule;

public class ExtractionReadinessChecker {

    public void checkModule(String moduleName) {
        ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
        ApplicationModule module = modules.getModuleByName(moduleName)
            .orElseThrow();

        System.out.println("Module: " + moduleName);
        System.out.println("Dependencies: " + module.getDependencies());
        System.out.println("Published Events: " + module.getPublishedEvents());
        System.out.println("Listened Events: " + module.getBootstrapDependencies());

        // A module ready for extraction:
        // - Communicates only through events
        // - Has no synchronous dependencies to other modules
        // - Owns its own data tables
    }
}

Các mô-đun chỉ giao tiếp qua sự kiện có thể được tách thành microservice với mức thay đổi tối thiểu: chỉ cần thay event bus cục bộ bằng một message broker (Kafka, RabbitMQ).

Kết luận

Spring Modulith mang đến một giải pháp thực dụng để cấu trúc các ứng dụng Spring Boot dạng monolith:

Cấu trúc theo quy ước: package = mô-đun, internal = đóng gói

Giao tiếp tách rời: sự kiện domain giữa các mô-đun

Xác minh tự động: kiểm thử cấu trúc phát hiện vi phạm

Sự kiện bền vững: đảm bảo xử lý với @ApplicationModuleListener

Kiểm thử cô lập: @ApplicationModuleTest để xác thực từng mô-đun

Tài liệu sinh tự động: sơ đồ PlantUML tự động

Khả năng quan sát: tích hợp Micrometer và Actuator endpoint

Lộ trình tới microservice: tách dễ dàng nhờ giảm gắn kết

Kiến trúc này đặc biệt phù hợp với những đội muốn cấu trúc monolith mà không gánh độ phức tạp vận hành của microservice, đồng thời vẫn giữ tùy chọn tiến tới kiến trúc phân tán khi cần.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#spring modulith
#modular architecture
#spring boot
#java
#modular monolith

Chia sẻ

Bài viết liên quan