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

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

bigegpt 2024-09-11 01:11 41 浏览

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

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...