基础知识

Spring Modulith 支持开发人员在 Spring Boot 应用中实现逻辑模块。它允许他们应用结构验证,记录模块排列,运行单个模块的集成测试,观察模块在运行时的交互,以及通常以松耦合的方式实现模块交互。本节将讨论开发人员在深入了解技术支持之前需要理解的基本概念。

应用模块

在 Spring Boot 应用中,应用模块是一个功能单元,由以下部分组成:

  • 向其他模块公开的 API,由 Spring bean 实例和模块发布的应用事件实现,通常称为提供的接口

  • 不应被其他模块访问的内部实现组件。

  • 以 Spring bean 依赖项、监听的应用事件和公开的配置属性的形式引用其他模块公开的 API,通常称为所需的接口

Spring Moduliths 提供了在 Spring Boot 应用中表达模块的不同方式,主要区别在于整体排列的复杂程度。这允许开发人员从简单开始,并在需要时自然地转向更复杂的方法。

ApplicationModules 类型

Spring Moduliths 允许检查代码库以根据给定的排列和可选配置推导出应用模块模型。spring-modulith-core 工件包含可以指向 Spring Boot 应用类的 ApplicationModules

创建应用模块模型
  • Java

  • Kotlin

var modules = ApplicationModules.of(Application.class);
val modules = ApplicationModules.of(Application::class.java)

modules 将包含从代码库派生的应用模块排列的内存表示。哪些部分将被检测为模块取决于指向的类所在的包下方的 Java 包结构。在简单应用模块中了解更多关于默认期望的排列的信息。高级排列和自定义选项在高级应用模块

为了了解分析后的排列是什么样子,我们可以简单地将包含在整体模型中的各个模块写入控制台

将应用模块排列写入控制台
  • Java

  • Kotlin

modules.forEach(System.out::println);
modules.forEach { println(it) }
应用模块排列的控制台输出
## example.inventory ##
> Logical name: inventory
> Base package: example.inventory
> Spring beans:
  + ….InventoryManagement
  o ….SomeInternalComponent

## example.order ##
> Logical name: order
> Base package: example.order
> Spring beans:
  + ….OrderManagement
  + ….internal.SomeInternalComponent

请注意,如何列出每个模块,识别包含的 Spring 组件,以及呈现相应的可见性。

排除包

如果您想从应用模块检查中排除某些 Java 类或完整包,可以使用:

  • Java

  • Kotlin

ApplicationModules.of(Application.class, JavaClass.Predicates.resideInAPackage("com.example.db")).verify();
ApplicationModules.of(Application::class.java, JavaClass.Predicates.resideInAPackage("com.example.db")).verify()

排除的其他示例

  • com.example.db — 匹配给定包com.example.db中的所有文件。

  • com.example.db.. — 匹配给定包 (com.example.db) 和所有子包 (com.example.db.acom.example.db.b.c) 中的所有文件。

  • ..example.. — 匹配a.examplea.example.ba.b.example.c.d,但不匹配a.exam.b

有关可能匹配器的完整详细信息,请参阅 ArchUnit 的 JavaDoc PackageMatcher

简单应用模块

应用的主包是包含主应用类所在的包。即使用@SpringBootApplication注释的类,并且通常包含用于运行它的main(…)方法。默认情况下,主包的每个直接子包都被视为应用模块包

如果此包不包含任何子包,则认为它是一个简单的包。它允许通过使用 Java 的包作用域隐藏类型以防止被驻留在其他包中的代码引用,从而隐藏其中的代码,因此不受注入到这些代码中的依赖项的影响。因此,自然地,模块的 API 由包中的所有公共类型组成。

让我们来看一个示例排列( 表示公共类型, 表示包私有类型)。

单个库存应用模块
 Example
└─  src/main/java
   ├─  example                        (1)
   |  └─  Application.java
   └─  example.inventory              (2)
      ├─  InventoryManagement.java
      └─  SomethingInventoryInternal.java
1 应用的主包example
2 应用模块包inventory

高级应用模块

如果应用模块包包含子包,则可能需要公开这些子包中的类型,以便可以从同一个模块的代码中引用它。

库存和订单应用模块
 Example
└─  src/main/java
   ├─  example
   |  └─  Application.java
   ├─  example.inventory
   |  ├─  InventoryManagement.java
   |  └─  SomethingInventoryInternal.java
   ├─  example.order
   |  └─  OrderManagement.java
   └─  example.order.internal
      └─  SomethingOrderInternal.java

在这种排列中,order 包被视为 API 包。允许来自其他应用模块的代码引用其中的类型。order.internal,就像应用模块基本包的任何其他子包一样,被视为内部包。其中的代码不得被其他模块引用。请注意,SomethingOrderInternal 是一个公共类型,可能是因为OrderManagement 依赖于它。不幸的是,这也意味着可以从其他包(如inventory)中引用它。在这种情况下,Java 编译器在防止这些非法引用方面没有多大用处。

开放应用模块

上述描述的安排被认为是封闭的,因为它们仅向其他模块公开主动选择公开的类型。当将 Spring Modulith 应用于遗留应用程序时,隐藏嵌套包中位于其他模块的所有类型可能不充分,或者也需要将所有这些包标记为公开。

要将应用程序模块转换为开放模块,请在package-info.java类型上使用@ApplicationModule注解。

声明应用程序模块为开放
  • Java

  • Kotlin

@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package example.inventory

将应用程序模块声明为开放将导致以下更改对验证

  • 通常允许从其他模块访问应用程序模块内部类型。

  • 所有类型,包括驻留在应用程序模块基本包的子包中的类型,都将添加到未命名的命名接口中,除非显式分配给命名接口。

此功能主要用于现有项目的代码库,这些代码库逐渐迁移到 Spring Modulith 推荐的打包结构。在完全模块化的应用程序中,使用开放应用程序模块通常暗示模块化和打包结构不佳。

显式应用程序模块依赖项

模块可以通过在包上使用@ApplicationModule注解来选择声明其允许的依赖项,通过package-info.java文件表示。例如,由于 Kotlin 不支持该文件,因此您也可以在应用程序模块根包中位于单个类型上使用该注解。

清单显式配置模块依赖项
  • Java

  • Kotlin

@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order"
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule

@ApplicationModule(allowedDependencies = "order")
class ModuleMetadata {}

在这种情况下,inventory模块中的代码仅允许引用order模块中的代码(以及最初未分配给任何模块的代码)。在验证应用程序模块结构中了解如何监控它。

命名接口

默认情况下,如高级应用程序模块中所述,应用程序模块的基本包被视为 API 包,因此是允许来自其他模块的传入依赖项的唯一包。如果您想向其他模块公开其他包,则需要使用命名接口。您可以通过使用@NamedInterface注解这些包的package-info.java文件或显式使用@org.springframework.modulith.PackageInfo注解的类型来实现。

封装 SPI 命名接口的包安排
 Example
└─  src/main/java
   ├─  example
   |  └─  Application.java
   ├─ …
   ├─  example.order
   |  └─  OrderManagement.java
   ├─  example.order.spi
   |  ├—  package-info.java
   |  └─  SomeSpiInterface.java
   └─  example.order.internal
      └─  SomethingOrderInternal.java
example.order.spi中的package-info.java
  • Java

  • Kotlin

@org.springframework.modulith.NamedInterface("spi")
package example.order.spi;
package example.order.spi

import org.springframework.modulith.PackageInfo
import org.springframework.modulith.NamedInterface

@PackageInfo
@NamedInterface("spi")
class ModuleMetadata {}

该声明的效果是双重的:首先,其他应用程序模块中的代码被允许引用SomeSpiInterface。应用程序模块能够在显式依赖项声明中引用命名接口。假设inventory模块正在使用它,它可以像这样引用上面声明的命名接口

将允许的依赖项定义为专用的命名接口
  • Java

  • Kotlin

@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: spi"
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: spi"
)
package example.inventory

请注意,我们如何通过双冒号::连接命名接口的名称spi。在此设置中,inventory中的代码将被允许依赖于SomeSpiInterface和驻留在order.spi接口中的其他代码,但不依赖于OrderManagement,例如。对于没有显式描述依赖项的模块,应用程序模块根包SPI包都是可访问的。

如果您想表达应用程序模块被允许引用所有显式声明的命名接口,您可以使用星号(*)如下所示

使用星号声明对所有声明的命名接口的允许依赖项
  • Java

  • Kotlin

@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: *"
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: *"
)
package example.inventory

自定义模块检测

默认情况下,预计应用程序模块将位于 Spring Boot 应用程序类所在的包的直接子包中。可以激活另一种检测策略,仅考虑显式注释的包,无论是通过 Spring Modulith 的@ApplicationModule还是 jMolecules 的@Module注解。可以通过将spring.modulith.detection-strategy配置为explicitly-annotated来激活该策略。

将应用程序模块检测策略切换为仅考虑已注释的包
spring.modulith.detection-strategy=explicitly-annotated

如果默认应用程序模块检测策略和手动注释的策略都不适用于您的应用程序,可以通过提供ApplicationModuleDetectionStrategy的实现来自定义模块的检测。该接口公开了一个单一方法Stream<JavaPackage> getModuleBasePackages(JavaPackage),并将使用 Spring Boot 应用程序类所在的包调用它。然后,您可以检查驻留在其中的包,并根据命名约定等选择要视为应用程序模块基本包的包。

假设您像这样声明一个自定义的ApplicationModuleDetectionStrategy实现

实现自定义的ApplicationModuleDetectionStrategy
  • Java

  • Kotlin

package example;

class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {

  @Override
  public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
    // Your module detection goes here
  }
}
package example

class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {

  override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
    // Your module detection goes here
  }
}

现在可以将此类注册为spring.modulith.detection-strategy,如下所示

spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy

自定义应用程序模块安排

Spring Moduliths 允许配置您通过@Modulithic注解创建的应用程序模块安排周围的一些核心方面,该注解用于 Spring Boot 应用程序的主要类。

  • Java

  • Kotlin

package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.modulith.Modulithic;

@Modulithic
@SpringBootApplication
class MyApplication {

  public static void main(String... args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}
package example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.modulith.Modulithic

@Modulithic
@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
  runApplication<DemoApplication>(*args)
}

该注解公开以下属性以进行自定义

注解属性 描述

systemName

应用程序的可读名称,用于在生成的文档中使用。

sharedModules

将具有给定名称的应用程序模块声明为共享模块,这意味着它们将始终包含在应用程序模块集成测试中。

additionalPackages

指示 Spring Modulith 将配置的包视为其他根应用程序包。换句话说,也将为这些包触发应用程序模块检测。