1、介绍
模块化单体是一种架构风格,代码是根据模块的概念构成的。 对于许多组织而言,模块化单体可能是一个很好的选择。 它有助于保持一定程度的独立性,这有助于我们在需要的时候轻松过渡到微服务架构。
Spring Modulith是Spring的一个实验项目,可用于构建模块化单体应用程序。 此外,它还支持开发人员构建结构良好且业务领域对齐的Spring Boot应用程序。
在本文中,我们将讨论Spring Modulith项目的基础知识,并演示如何使用它。
2、模块化单体架构
我们有不同的选项来构建我们的程序代码。 传统上,我们会围绕基础设施来设计软件解决方案。 但当我们围绕业务设计程序时,它就会促进对系统更好地理解和维护。 模块化单体架构就是这样一种设计。
由于其简单性和可维护性,模块化单体架构在架构师和开发人员中越来越受欢迎。 如果我们将领域驱动设计 (DDD) 应用于我们现有的单体应用程序,我们可以将其重构为模块化单体架构:
我们可以通过识别应用程序的领域(domain)和定义界限上下文(bounded contexts),将单体应用的核心拆分为模块。
让我们看看如何在Spring Boot框架内实现模块化单体应用程序。Spring Modulith由一组库组成,它们可帮助开发人员构建模块化的Spring Boot应用程序。
3、Spring Modulith基础知识
Spring Modulith帮助开发人员使用领域驱动开发应用程序模块。 此外,它还支持对此类模块提供验证和文档化功能。
3.1、Maven依赖
使用Spring Modulith的依赖,首先要在pom.xml 的<dependencyManagement>中导入spring-modulith-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”。为此,我们需要在main 包中创建两个直接的子包:
让我们看一下这个示例的“”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());
}
}
在这个服务里,我们仅仅用log记录了创建的product。
最后,在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 and product。同时,它也列出了每个模块的Spring组件。
3.4、模块封装
值得注意的是,当前的设计是存在问题的。ProductService API可以访问Notification 类,而这是notification模块的内部功能。
在模块化设计中,我们必须保护和隐藏特定的信息,并控制对内部实现的访问。Spring Modulith使用应用模块基包的子包提供模块封装的能力。
此外,它还隐藏了类型,使其不被其他包中的代码引用。 一个模块可以访问任何其他模块的内容,但不能访问其他模块的子包。
现在,让我们在每个模块中创建一个名为internal内部子包并将内部实现移至其中:
在这样的排列下,notification包被认为是一个 API 包。 来自其他应用程序模块的源代码可以引用其中的类型。 但是不得从其他模块引用notification.internal包中的源代码。
验证模块结构
现在的设计还有另外一个问题。在上面的例子中,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。
现在,我们通过添加一个NotificationDTO类到notification模块来修复这个问题:
public class NotificationDTO {
private Date date;
private String format;
private String productName;
// getters and setters
}
之后,我们使用NotificationDTO实例代替product模块中的Notification:
After that, we use the NotificationDTO instance instead of the Notification in the product module:
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或者使用事件。
在上一节中,我们将notification模块API注入到product模块中。 但是,Spring Modulith 鼓励使用Spring Framework应用程序事件(Application Events)进行模块间通信。 为了使应用程序模块尽可能相互解耦,我们使用事件发布和消费作为交互的主要方式。
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注解,在上面的例子中,我们消费事件并用log打印明细。
异步消息处理
对于异步的事件处理,我们需要添加@Async注解到监听器上:
@Async
@ApplicationModuleListener
public void notificationEvent(NotificationDTO event) {
// ...
}
另外,异步行为需要我们在Spring的上下文中通过@EnableAsync开启。它可以添加到Spring Boot的入口类中。
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// ...
}
}
5、结语
在本文中,我们重点介绍了Spring Modulith项目的基础知识。
- 我们首先讨论什么是模块化单体设计。
- 接下来,我们谈到了应用程序模块。
- 我们还详细介绍了应用程序模块模型的创建及其结构的验证。
- 最后,我们解释了使用事件的模块间交互。
源码地址:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-libraries-2
Spring Modulith官网地址:https://spring.io/projects/spring-modulith
Spring Modulith github地址:https://github.com/spring-projects/spring-modulith
原文地址:https://www.baeldung.com/spring-modulith