Spring 模塊介紹
一、簡介
Modular Monolith 是一種架構風格,我們的源代碼是根據模塊的概念構建的。對於許多組織而言,模塊化單體可能是一個很好的選擇。它有助於保持一定程度的獨立性,這有助於我們在需要時過渡到微服務架構。
Spring Modulith是 Spring 的一個實驗項目,可用於模塊化單體應用程序。此外,它還支持開發人員構建結構良好、領域對齊的 Spring Boot 應用程序。
在本教程中,我們將討論 Spring Modulith 項目的基礎知識,並展示如何在實踐中使用它的示例。
2. 模塊化單體架構
我們有不同的選擇來構建我們的應用程序代碼。傳統上,我們圍繞基礎架構設計軟件解決方案。但是,當我們圍繞業務設計應用程序時,它會導致更好地理解和維護系統。模塊化單體架構就是這樣一種設計。
由於其簡單性和可維護性,模塊化單體架構在架構師和開發人員中越來越受歡迎。如果我們將領域驅動設計 (DDD) 應用於我們現有的單體應用程序,我們可以將其重構為模塊化單體架構:
我們可以通過識別應用程序的領域和定義有界上下文,將單體的核心拆分為模塊。
讓我們看看如何在 Spring Boot 框架內實現模塊化單體應用程序。 Spring Modulith 由一組庫組成,可幫助開發人員構建模塊化的 Spring Boot 應用程序。
3. Spring 模塊基礎
Spring Modulith 幫助開發人員使用域驅動的應用程序模塊。此外,它還支持對此類模塊化安排的驗證和記錄。
3.1. Maven 依賴項
讓我們首先在pom.xml
的<dependencyManagement>
部分中將spring-modulith-bom
依賴項作為物料清單 (BOM) 導入:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>0.5.1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
此外,我們還需要一些核心的 Spring Modulith 依賴項:
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
3.2.應用模塊
Spring Modulith 的主要概念是應用程序模塊。應用程序模塊是向其他模塊公開 API 的功能單元。此外,它還有一些不應該被其他模塊訪問的內部實現。當我們設計我們的應用程序時,我們會為每個域考慮一個應用程序模塊。
Spring Modulith 提供了不同的模塊表達方式。我們可以將應用程序的領域或業務模塊視為應用程序主包的直接子包。換句話說,應用程序模塊是與 Spring Boot 主類位於同一級別的包(使用@SpringBootApplication
註釋):
├───pom.xml
├───src
├───main
│ ├───java
│ │ └───main-package
│ │ └───module A
│ │ └───module B
│ │ ├───sub-module B
│ │ └───module C
│ │ ├───sub-module C
│ │ │ MainApplication.java
現在,讓我們看一個包含product
和notification
域的簡單應用程序。在此示例中,我們從product
模塊調用服務,然後product
模塊從notification
模塊調用服務。
首先,我們將創建兩個應用程序模塊: product
和notification
。為此,我們需要在主包中創建兩個直接子包:
我們來看看這個例子的product
模塊。我們在product
模塊中有一個簡單的Product
類:
public class Product {
private String name;
private String description;
private int price;
public Product(String name, String description, int price) {
this.name = name;
this.description = description;
this.price = price;
}
// getters and setters
}
然後,讓我們在product
模塊中定義ProductService
bean:
@Service
public class ProductService {
private final NotificationService notificationService;
public ProductService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void create(Product product) {
notificationService.createNotification(new Notification(new Date(), NotificationType.SMS, product.getName()));
}
}
在此類中, create()
方法從notification
模塊調用公開的NotificationService
API,並創建Notification
類的實例。
讓我們看一下notification
模塊。 notification
模塊包括Notification
、 NotificationType
和NotificationService
類。
讓我們看看NotificationService
bean:
@Service
public class NotificationService {
private static final Logger LOG = LoggerFactory.getLogger(NotificationService.class);
public void createNotification(Notification notification) {
LOG.info("Received notification by module dependency for product {} in date {} by {}.",
notification.getProductName(),
notification.getDate(),
notification.getFormat());
}
}
在此服務中,我們只記錄創建的產品。
最後,在main()
方法中,我們從product
模塊調用ProductService
API 的create()
方法:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args)
.getBean(ProductService.class)
.create(new Product("baeldung", "course", 10));
}
}
目錄結構如下圖所示:
3.3.應用模塊模型
我們可以分析我們的代碼庫,根據排列派生出一個應用程序模塊模型。 ApplicationModules
類提供創建應用程序模塊排列的功能。
讓我們創建一個應用程序模塊模型:
@Test
void createApplicationModuleModel() {
ApplicationModules modules = ApplicationModules.of(Application.class);
modules.forEach(System.out::println);
}
如果我們看一下控制台輸出,我們可以看到我們的應用程序模塊安排:
# Notification
> Logical name: notification
> Base package: com.baeldung.ecommerce.notification
> Spring beans:
+ ….NotificationService
# Product
> Logical name: product
> Base package: com.baeldung.ecommerce.product
> Spring beans:
+ ….ProductService
如我們所見,它檢測到我們的兩個模塊: notification
和product
。此外,它還列出了每個模塊的 Spring 組件。
3.4.模塊封裝
值得注意的是,當前的設計存在問題。 ProductService
API 可以訪問Notification
類,這是notification
模塊的內部功能。
在模塊化設計中,我們必須保護和隱藏特定信息並控制對內部實現的訪問。 Spring Modulith 使用應用模塊基礎包的子包提供模塊封裝。
此外,它還隱藏了類型,使其不被駐留在其他包中的代碼引用。一個模塊可以訪問任何其他模塊的內容,但不能訪問其他模塊的子包。
現在,讓我們在每個模塊中創建一個internal
子包並將內部實現移至其中:
在這樣的安排中, notification
包被認為是一個 API 包。來自其他應用程序模塊的源代碼可以引用其中的類型。但是不得從其他模塊引用notification.internal
包中的源代碼。
3.5.驗證模塊化結構
這種設計還有另一個問題。在上面的示例中, Notification
類位於notification.internal
包中。但是,我們從其他包中引用了Notification
類,例如product
一:
public void create(Product product) {
notificationService.createNotification(new Notification(new Date(), NotificationType.SMS, product.getName()));
}
不幸的是,這意味著它違反了模塊訪問規則。在這種情況下,Spring Modulith 無法使Java 編譯失敗來阻止這些非法引用。它改用單元測試:
@Test
void verifiesModularStructure() {
ApplicationModules modules = ApplicationModules.of(Application.class);
modules.verify();
}
我們在ApplicationModules
實例上使用verify()
方法來確定我們的代碼安排是否符合預期的約束。 Spring Modulith 使用 ArchUnit 項目來實現此功能。
對於上面的示例,我們的驗證測試失敗並拋出org.springframework.modulith.core.Violations
異常:
org.springframework.modulith.core.Violations:
- Module 'product' depends on non-exposed type com.baeldung.modulith.notification.internal.Notification within module 'notification'!
Method <com.baeldung.modulith.product.ProductService.create(com.baeldung.modulith.product.internal.Product)> calls constructor <com.baeldung.modulith.notification.internal.Notification.<init>(java.util.Date, com.baeldung.modulith.notification.internal.NotificationType, java.lang.String)> in (ProductService.java:25)
測試失敗,因為product
模塊試圖訪問notification
模塊的內部類Notification
。
現在,讓我們通過向notification
模塊添加一個NotificationDTO
類來修復它:
public class NotificationDTO {
private Date date;
private String format;
private String productName;
// getters and setters
}
之後,我們使用NotificationDTO
實例代替product
模塊中的Notification
:
public void create(Product product) {
notificationService.createNotification(new NotificationDTO(new Date(), "SMS", product.getName()));
}
最終的目錄結構如下圖所示:
3.6.記錄模塊
我們可以記錄項目模塊之間的關係。 Spring Modulith 提供基於PlantUML生成圖表的功能,帶有 UML 或C4皮膚。
讓我們將應用程序模塊導出為 C4 組件圖:
@Test
void createModuleDocumentation() {
ApplicationModules modules = ApplicationModules.of(Application.class);
new Documenter(modules)
.writeDocumentation()
.writeIndividualModulesAsPlantUml();
}
C4 圖將作為puml
文件在target/modulith-docs
目錄中創建。
讓我們使用在線 PlantUML 服務器渲染生成的組件圖:
此圖顯示product
模塊使用notification
模塊的 API。
4. 使用事件的模塊間交互
我們有兩種模塊間交互的方法:依賴於其他應用程序模塊的 Spring bean 或使用事件。
在上一節中,我們將notification
模塊 API 注入到product
模塊中。但是,Spring Modulith 鼓勵使用 Spring Framework 應用程序事件進行模塊間通信。為了使應用程序模塊盡可能相互解耦,我們使用事件發布和消費作為交互的主要方式。
4.1.發布事件
現在,讓我們使用 Spring 的ApplicationEventPublisher
來發布一個領域事件:
@Service
public class ProductService {
private final ApplicationEventPublisher events;
public ProductService(ApplicationEventPublisher events) {
this.events = events;
}
public void create(Product product) {
events.publishEvent(new NotificationDTO(new Date(), "SMS", product.getName()));
}
}
我們只是注入了ApplicationEventPublisher
並使用了publishEvent()
API。
4.2.應用程序模塊偵聽器
為了註冊一個監聽器,Spring Modulith 提供了@ApplicationModuleListener
註解:
@Service
public class NotificationService {
@ApplicationModuleListener
public void notificationEvent(NotificationDTO event) {
Notification notification = toEntity(event);
LOG.info("Received notification by event for product {} in date {} by {}.",
notification.getProductName(),
notification.getDate(),
notification.getFormat());
}
我們可以在方法級別使用@ApplicationModuleListener
註釋。在上面的示例中,我們使用了NotificationDTO
事件並記錄了詳細信息。
4.3.異步事件處理
對於異步事件處理,我們需要在監聽器中添加@Async
註解:
@Async
@ApplicationModuleListener
public void notificationEvent(NotificationDTO event) {
// ...
}
此外,需要使用@EnableAsync
註釋在 Spring 上下文中啟用異步行為。它可以添加到主應用程序類中:
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// ...
}
}
5.結論
在本指南中,我們重點介紹了 Spring Modulith 項目的基礎知識。我們首先討論什麼是模塊化單體設計。
接下來,我們談到了應用程序模塊。我們還詳細介紹了應用程序模塊模型的創建及其結構的驗證。
最後,我們解釋了使用事件的模塊間交互。
一如既往,本文的完整源代碼可在 GitHub 上獲得。