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

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

ccwgpt 2024-11-18 09:28 151 浏览 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

相关推荐

腾讯开源框架TarsCpp-rpc设计分析-server(二)

2Tars协议2.1是什么借用官方说法:TARS编码协议是一种数据编解码规则,它将整形、枚举值、字符串、序列、字典、自定义结构体等数据类型按照一定的规则编码到二进制数据流中。对端接收到二进制数据流...

微服务调用为什么用RPC框架,http不更简单吗?

简单点,HTTP是协议,RPC是概念!实现RPC可以基于HTTP协议(Feign),TCP协议(Netty),RMI协议(Soap),WebService(XML—RPC)框架。传输过程中,也因为序列...

go-zero:开箱即用的微服务框架(gin框架微服务)

go-zero是一个集成了各种工程实践的Web和rpc框架,它的弹性设计保障了大并发服务端的稳定性,并且已经经过了充分的实战检验。go-zero在设计时遵循了“工具大于约定和文档”的理...

SOFARPC :高性能、高扩展性、生产级的 Java RPC 框架

#暑期创作大赛#SOFARPC是一个高性能、高扩展性、生产级的JavaRPC框架。在蚂蚁金服,SOFARPC已经使用了十多年,已经发展了五代。SOFARPC致力于简化应用程序之间的RPC...

自研分布式高性能RPC框架及服务注册中心ApiRegistry实践笔记

痛点1.bsf底层依赖springcloud,影响bsf更新springboot新版本和整体最新技术版本升级。2.eureka已经闭源,且框架设计较重,同时引入eureka会自行引入较多sprin...

Rust语言从入门到精通系列 - Tonic RPC框架入门实战

Rust语言是一种系统级语言,被誉为“没有丧失性能的安全语言”。Rust语言的优势在于其内存安全机制,在编译时就能保证程序的内存安全。Tonic模块是Rust语言的一个RPC(RemoteProce...

腾讯开源框架TarsCpp-rpc设计分析-client(一)

前言Tars是腾讯开源的微服务平台,包含了一个高性能的rpc框架和服务治理平台,TarsCpp是其C++版本。对于以C++为主要开发语言,同时还想深入了解rpc和微服务框架具体实现的同学来说,Tars...

设计了一款TPS百万级别的分布式、高性能、可扩展的RPC框架

为啥要开发RPC框架事情是这样的,在开发这个RPC框架之前,我花费了不少时间算是对Dubbo框架彻底研究透彻了。冰河在撸透了Dubbo2.x和Dubbo3.x的源码之后,本来想给大家写一个Dubbo源...

rpc框架使用教程,超级稳定好用,大厂都在使用

rpc是什么远程调用协议如何使用导入依赖<dependency><groupId>org.apache.dubbo</groupId><art...

Layui 框架实战:动态加载 Select 与二级联动全解析

在现代Web开发中,下拉选择框(Select)是用户输入数据时不可或缺的组件。很多时候,我们需要的选项并非静态写死在HTML中,而是需要根据业务逻辑从后端动态获取。更有甚者,我们可能需要实现“...

15个能为你节省数百小时的前端设计神器,从UI库到文档生成

无论你是刚开始开发之旅的新手,还是疲于应付生产期限的资深程序员,有一个真理始终不变:正确的工具能彻底改变你的工作流程。多年来,我测试了数百个开发工具——有些实用,大多数平庸。但有一批免费网站经受住了时...

Layui与WinForm通用权限管理系统全解析

嘿,小伙伴们,今天咱们来聊聊Layui和WinForm这两个框架在通用权限管理系统中的应用。别担心,我会尽量用简单易懂的语言来讲解,保证让大家都能跟上节奏!首先说说Layui。Layui是一个前端UI...

纯Python构建精美UI!MonsterUI让前端开发效率飙升

“无需CSS知识,告别类名记忆,11行代码实现专业级卡片组件”在传统Web开发中,构建美观界面需要同时掌握HTML、CSS、JavaScript三剑客,开发者不得不在多种语言间频繁切换。即使使用Boo...

WebTUI:将终端用户界面(TUI)之美带到浏览器的CSS库

在当今Web技术飞速发展的时代,界面设计愈发复杂多样。然而,随着现代化工具的广泛使用,一些开发者开始回归极简风格,追求一种简洁而富有韵味的设计。WebTUI正是这样一款CSS库,它将经典的终...

人教版二年级下册生字描红汇总(拼音+笔顺+描红),可打印!

可定制内容,评论区留言。本次整理的为人教版二年级下册所有生字,共计300个;写字是小学阶段一项重要的基本功训练,把汉字写得正确、工整、美观,可以提高运用汉字这一交际工具的准确性和效率。对小学生进行写字...

取消回复欢迎 发表评论: