1. 概述
模块化单体架构是一种基于模块概念组织源代码的架构风格。对于许多组织来说,模块化单体是绝佳选择。它能在保持一定独立性的同时,为未来迁移到微服务架构提供可能。
Spring Modulith 是 Spring 官方推出的项目,专为模块化单体应用设计。它能帮助开发者识别和管理应用模块,同时支持构建结构良好、领域对齐的 Spring Boot 应用。
本文将介绍 Spring Modulith 的核心概念,并通过实战示例展示其使用方法。
2. 模块化单体架构
应用代码组织方式多种多样。传统设计通常围绕基础设施展开,但围绕业务领域设计应用能带来更好的可理解性和可维护性。模块化单体架构正是这种设计思想的体现。
凭借简洁性和可维护性,模块化单体架构正受到架构师和开发者的青睐。将领域驱动设计(DDD)应用于现有单体应用,可以重构为模块化单体架构:
通过识别应用领域并定义限界上下文,我们可以将单体核心拆分为多个模块。
下面看看如何在 Spring Boot 框架中实现模块化单体应用。Spring Modulith 提供了一组库,帮助开发者构建模块化的 Spring Boot 应用。
3. Spring Modulith 基础
Spring Modulith 帮助开发者构建领域驱动的应用模块,并支持对模块化结构进行验证和文档化。
3.1. Maven 依赖
首先在 pom.xml
的 <dependencyManagement>
中导入 BOM 依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>1.2.2</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
然后添加核心依赖:
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</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
模块调用 notification
模块的服务。
首先创建两个应用模块:product
和 notification
。在主包下创建两个直接子包:
查看 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
}
定义 ProductService
Bean:
@Service
public class ProductService {
private final NotificationService notificationService;
public ProductService(ApplicationEventPublisher events, NotificationService notificationService) { this.events = events;
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
、NotificationType
和 NotificationService
类:
@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()
方法中调用 ProductService
的 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
能访问 Notification
类,而这是 notification
模块的内部实现。
模块化设计要求隐藏内部实现并控制访问。Spring Modulith 通过应用模块基包的子包实现模块封装:
- 模块可访问其他模块的内容
- 但不能访问其他模块的子包
创建 internal
子包并移动内部实现:
此时 notification
包作为 API 包,其他模块可引用其类型。但 notification.internal
包中的类不能被外部引用。
3.5. 验证模块结构
上述设计仍有问题:Notification
类位于 notification.internal
包,但被 product
模块引用:
public void create(Product product) {
notificationService.createNotification(new Notification(new Date(), NotificationType.SMS, product.getName()));
}
这违反了模块访问规则。Spring Modulith 通过单元测试验证而非编译时检查:
@Test
void verifiesModularStructure() {
ApplicationModules modules = ApplicationModules.of(Application.class);
modules.verify();
}
使用 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
}
在 product
模块中使用 NotificationDTO
替代 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 图表生成在 target/modulith-docs
目录的 puml
文件中。
使用 在线 PlantUML 服务器 渲染:
图表显示 product
模块使用了 notification
模块的 API。
4. 基于事件的模块交互
模块交互有两种方式:直接依赖其他模块的 Spring Bean,或使用事件。
前文展示了在 product
模块注入 notification
模块 API。但Spring Modulith 推荐使用 Spring 应用事件 实现模块通信,以最大限度解耦。
4.1. 发布事件
使用 Spring 的 ApplicationEventPublisher
发布领域事件:
@Service
public class ProductService {
private final ApplicationEventPublisher events;
public ProductService(ApplicationEventPublisher events, NotificationService notificationService) {
this.events = events; this.notificationService = notificationService;
}
public void create(Product product) {
events.publishEvent(new NotificationDTO(new Date(), "SMS", product.getName()));
}
}
注入 ApplicationEventPublisher
并调用 publishEvent()
API。
4.2. 应用模块监听器
使用 @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());
}
在方法级别使用该注解。示例中消费 NotificationDTO
事件并记录日志。
4.3. 异步事件处理
@ApplicationModuleListener
已整合 @Async
、@Transactional
和 @TransactionalEventListener
功能,无需单独添加 @Async
:
@ApplicationModuleListener
public void notificationEvent(NotificationDTO event) {
// ...
}
异步行为需通过 @EnableAsync
开启:
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// ...
}
}
5. 总结
本文介绍了 Spring Modulith 的核心概念:
- 模块化单体架构的优势
- 应用模块的设计与封装
- 模块结构的验证方法
- 基于事件的模块交互
Spring Modulith 为构建结构清晰、领域驱动的单体应用提供了强大支持,是微服务转型前的理想中间态。