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

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

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

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

相关推荐

一条命令搞定pip国内镜像源设置(pip install 指定镜像)

玩python的同学想必没有不用pip的吧,pip是python包管理工具,和Nodejs的npm、Java的maven类似,这些依靠开源力量建立起的庞大软件库极大提高了开发的效率,不过默认pytho...

Cadence Allegro在PCB中手动或者自动添加差分对属性

设计PCB过程中,若设计中有差分对信号,则需要将是差分的2个信号设置为差分对,设置差分对有2种方式:手动添加及自动添加一、手动添加差分对:1、点击Setup-Constraints-Constrain...

合亿 Gutab 三防|车载工业平板功能介绍,车载工业平板厂家推荐

在商用车队管理迈向智能化、特种车辆作业追求高效化的今天,车载工业平板早已突破传统“车载导航”的单一功能,成为连接车辆、司机与云端管理的核心枢纽。从物流运输的实时调度中枢,到矿山开采的无人驾驶控制器,再...

「探长分享-黑匣子」本田冠道 2020款

【品牌】探长360汽车黑匣子【产品型号】2TPro【安装车型】本田冠道2020款【功能特点】360全景安全辅助,行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,震动监控,一步标...

「探长分享-黑匣子」奥迪A6L 2019款

【品牌】探长360汽车黑匣子【产品型号】2TPro【安装车型】奥迪A6L2019款【功能特点】360全景安全辅助,行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,震动监控,一步...

探长360全景案例分享:奥迪Q7 2011款360全景效果展示

【品牌】DCT360汽车黑匣子【产品型号】3TPro【安装车型】奥迪Q72011款【功能特点】360全景安全辅助,四路行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,24小时停...

「探长分享-黑匣子」保时捷Cayenne 2015款

【品牌】探长360汽车黑匣子【产品型号】4TPro【安装车型】保时捷Cayenne2015款【功能特点】360全景安全辅助,四路行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,...

苍蝇再小也是肉,变态电路的大阳巧客S2差点难死大神

这台大阳巧客S2电动四轮车是我家第二台四轮俱全的篷车!哈哈!大阳巧客S2配置4.5Kw永磁同步电机,SVPWM矢量控制正弦波系统,车辆在加速、爬坡上性能有提升,效率高,操控灵敏。这台车前段时间刚更换了...

「探长分享-黑匣子」奥迪Q5L 2020款

360汽车黑匣子【产品型号】4TPro【安装车型】奥迪Q5L2020款【功能特点】360全景安全辅助,四路行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,24小时停车监控,一秒一...

「探长分享-黑匣子」丰田兰德酷路泽 2016款

【品牌】探长360汽车黑匣子【产品型号】3TPro【安装车型】丰田兰德酷路泽2016款【功能特点】360全景安全辅助,四路行车录像,极致高清摄像头,模拟/数字高清/AHD多种信号格式输出,24小...

驾驶室盲区是酿成重卡事故主因?后视系统:这个锅我不背

小时候家中长辈常常提醒:离大货车远一点!司机根本看不到你!早期的货车可能真的存在驾驶盲区,比如车辆正下方,因驾驶座过高,恰好是司机看不到的视野盲区。而如今的重卡在环视系统上已经非常完善,是否还存在驾驶...

前后双录,360 G580行车记录仪(360行车记录仪g580s)

相信每一位车主都会为爱车安装行车记录仪,行车记录仪的作用不仅能为交通事故还原证据,还能防止碰瓷。传统的单镜头行车记录仪只能拍摄车头方向的行车画面,如果遇到后方车辆故意碰瓷的事故时,没有监控和后摄画面则...

海康威视同轴录像机怎么使用,海康XVR配置说明

海康威视同轴录像机支持模拟、同轴以及数字IP摄像机接入,因此在使用多种类型摄像机、老久监控项目改造等场景广泛使用。首先,新录像机第一次使用需要设置管理密码激活,密码需由8-16位数字、小写字母、大写...

亿道三防2代工业级车载平板电脑震撼登场,农机矿车专用

亿道三防近日推出2024年全新2代车载平板电脑V12R,引领多项技术创新和升级,为农机、矿车等车载领域带来了超越期待与想象的震撼体验。V12R是一款从里到外,性能、功能全线拉满的工业级车载平板电脑!拥...

分析神州十八号返回舱内的摄像机最有可能是什么类型的摄像头

有没有发现,神州十八号返回舱内摄像机的图像虽然清晰度不是很高,但是画面非常干净,没有一点干扰,几乎看不到噪点。图像清晰度不高不太可能是镜头原因,很可能是图像传感器的分辨率比较低的原因,图像传感器分辨率...