百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍

yuyutoo 2025-07-06 17:50 4 浏览 0 评论

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, NotificationTypeNotificationService类。

让我们来看看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();
}

我们使用ApplicationModulesverify()方法来识别我们的代码排列是否符合预期的约束。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项目的基础知识。

  1. 我们首先讨论什么是模块化单体设计。
  2. 接下来,我们谈到了应用程序模块。
  3. 我们还详细介绍了应用程序模块模型的创建及其结构的验证。
  4. 最后,我们解释了使用事件的模块间交互。

源码地址:
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

相关推荐

.NET 奇葩问题调试经历之3——使用了grpc通讯类库后,内存一直增长......

...

全局和隐式 using 指令详解(全局命令)

1.什么是全局和隐式using?在.NET6及更高版本中,Microsoft引入了...

请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍

1、介绍模块化单体是一种架构风格,代码是根据模块的概念构成的。对于许多组织而言,模块化单体可能是一个很好的选择。它有助于保持一定程度的独立性,这有助于我们在需要的时候轻松过渡到微服务架构。Spri...

ASP.NET程序集引用之痛:版本冲突、依赖地狱等解析与实战

我是一位多年后端经验的工程师,其中前几年用ASP.NET...

.NET AOT 详解(.net 6 aot)

简介AOT(Ahead-Of-TimeCompilation)是一种将代码直接编译为机器码的技术,与传统的...

一款基于Yii2开发的免费商城系统(一款基于yii2开发的免费商城系统是什么)

哈喽,我是老鱼,一名致力于在技术道路上的终身学习者、实践者、分享者!...

asar归档解包(游戏arc文件解包)

要学习Electron逆向,首先要有一个Electron开发的程序的发布的包,这里就以其官方的electron-quick-start作为例子来进行一下逆向的过程。...

在PyCharm 中免费集成Amazon CodeWhisperer

CodeWhisperer是Amazon发布的一款免费的AI编程辅助小工具,可在你的集成开发环境(IDE)中生成实时单行或全函数代码建议,帮助你快速构建软件。简单来说,AmazonCodeWhi...

2014年最优秀JavaScript编辑器大盘点

1.WebstormWebStorm是一种轻量级的、功能强大的IDE,为Node.js复杂的客户端开发和服务器端开发提供完美的解决方案。WebStorm的智能代码编辑器支持JavaScript,...

基于springboot、tio、oauth2.0前端vuede 超轻量级聊天软件分享

项目简介:基于JS的超轻量级聊天软件。前端:vue、iview、electron实现的PC桌面版聊天程序,主要适用于私有云项目内部聊天,企业内部管理通讯等功能,主要通讯协议websocket。支持...

JetBrains Toolbox推出全新产品订阅授权模式

捷克知名软件开发公司JetBrains最为人所熟知的产品是Java编程语言开发撰写时所用的集成开发环境IntelliJIDEA,相信很多开发者都有所了解。而近期自2015年11月2日起,JetBr...

idea最新激活jetbrains-agent.jar包,亲测有效

这里分享一个2019.3.3版本的jetbrains-agent.jar,亲测有效,在网上找了很多都不能使用,终于找到一个可以使用的了,这里分享一下具体激活步骤,此方法适用于Jebrains家所有产品...

CountDownTimer的理解(countdowntomars)

CountDownTimer是android开发常用的计时类,按照注释中的说明使用方法如下:kotlin:object:CountDownTimer(30000,1000){...

反射为什么性能会很慢?(反射时为什么会越来越长)

1.背景前段时间维护一个5、6年前的项目,项目总是在某些功能使用上不尽人意,性能上总是差一些,仔细过了一下代码发现使用了不少封装好的工具类,工具类里面用了好多的反射,反射会影响到执行效率吗?盲猜了一...

btrace 开源!基于 Systrace 高性能 Trace 工具

介绍btrace(又名RheaTrace)是抖音基础技术团队自研的一款高性能AndroidTrace工具,它基于Systrace实现,并针对Systrace不足之处加以改进,核心改进...

取消回复欢迎 发表评论: