此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Modulith 2.0.4spring-doc.cadn.net.cn

基础

Spring Modulith 支持开发者在 Spring Boot 应用中实现逻辑模块。 它允许开发者应用结构验证、记录模块布局、为单个模块运行集成测试、在运行时观察模块间的交互,并以松耦合的方式普遍实现模块交互。 本节将讨论开发者在深入技术支撑之前需要理解的基本概念。spring-doc.cadn.net.cn

应用模块

在 Spring Boot 应用程序中,应用模块是一个功能单元,由以下部分组成:spring-doc.cadn.net.cn

  • 由 Spring Bean 实例实现并暴露给其他模块的 API,以及由该模块发布的应用事件,通常称为提供接口spring-doc.cadn.net.cn

  • 不应被其他模块访问的内部实现组件。spring-doc.cadn.net.cn

  • 对其他模块暴露的 API 的引用,形式为 Spring Bean 依赖、监听的应用事件以及暴露的配置属性,通常称为必需接口spring-doc.cadn.net.cn

Spring Modulith 提供了在 Spring Boot 应用程序中表达模块的多种方法,主要区别在于整体安排的复杂程度。 这使得开发人员可以从简单的方案开始,并在需要时自然地过渡到更复杂的方案。spring-doc.cadn.net.cn

ApplicationModules类型

Spring Modulith 允许检查代码库,以基于给定的排列和可选配置推导出应用模块模型。 spring-modulith-core 构件包含可指向 Spring Boot 应用程序类的 ApplicationModulesspring-doc.cadn.net.cn

创建应用程序模块模型
var modules = ApplicationModules.of(Application.class);
val modules = ApplicationModules.of(Application::class.java)

modules 将包含从代码库派生的应用程序模块布局的内存表示。 其中哪些部分会被检测为模块,取决于该类所在包下方的 Java 包结构。 有关默认预期的布局详情,请参阅 简单应用程序模块。 高级布局和自定义选项在 高级应用程序模块 中描述,且spring-doc.cadn.net.cn

为了了解所分析的布局看起来是什么样子,我们可以简单地将整体模型中包含的各个模块写入控制台:spring-doc.cadn.net.cn

将应用程序模块排列写入控制台
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 组件如何被识别,以及各自的可见性如何呈现。spring-doc.cadn.net.cn

排除包

如果您希望从应用程序模块检查中排除某些 Java 类或完整的包,可以通过以下方式实现:spring-doc.cadn.net.cn

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

更多排除示例:spring-doc.cadn.net.cn

  • com.example.db — 匹配给定包 com.example.db 中的所有文件。spring-doc.cadn.net.cn

  • com.example.db.. — 匹配给定包 (com.example.db) 及其所有子包 (com.example.db.acom.example.db.b.c) 中的所有文件。spring-doc.cadn.net.cn

  • ..example.. — 匹配 a.examplea.example.ba.b.example.c.d,但不匹配 a.exam.bspring-doc.cadn.net.cn

关于可能匹配器的完整详情可在 ArchUnit 的 JavaDoc PackageMatcher 中找到。spring-doc.cadn.net.cn

简单应用模块

应用程序的主包是主应用程序类所在的包。 即该类,它使用 @SpringBootApplication 进行注解,并且通常包含用于运行它的 main(…) 方法。 默认情况下,主包的每个直接子包都被视为应用程序模块包spring-doc.cadn.net.cn

如果此包不包含任何子包,则被视为简单包。 它允许利用 Java 的包作用域将代码隐藏在其中,从而防止其他包中的代码引用这些类型,因此这些类型也不会被注入依赖到那些包中。 因此,该模块的 API 自然由包中的所有公共类型组成。spring-doc.cadn.net.cn

让我们来看一个示例安排(表示一个公共类型,一个包私有的)。spring-doc.cadn.net.cn

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

高级应用模块

如果应用程序模块包包含子包,则可能需要将这些子包中的类型设为公共的,以便同一模块的代码可以引用它们。spring-doc.cadn.net.cn

库存与订单应用模块
 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-doc.cadn.net.cn

嵌套应用程序模块

自 1.3 版本起,Spring Modulith 应用模块可以包含嵌套模块。 这使得在某个模块内部需要进一步逻辑划分时,能够对其内部结构进行管理。 要定义嵌套应用模块,请显式地使用 @ApplicationModule 注解应当构成模块的包。spring-doc.cadn.net.cn

 Example
╰─  src/main/java
   │
   ├─  example
   │  ╰─  Application.java
   │
   │  -> Inventory
   │
   ├─  example.inventory
   │  ├─  InventoryManagement.java
   │  ╰─  SomethingInventoryInternal.java
   ├─  example.inventory.internal
   │  ╰─  SomethingInventoryInternal.java
   │
   │  -> Inventory > Nested
   │
   ├─  example.inventory.nested
   │  ├─  package-info.java // @ApplicationModule
   │  ╰─  NestedApi.java
   ├─  example.inventory.nested.internal
   │  ╰─  NestedInternal.java
   │
   │  -> Order
   │
   ╰─  example.order
      ├─  OrderManagement.java
      ╰─  SomethingOrderInternal.java

在此示例中,inventory 是一个如上 所述 的应用模块。 位于 nested 包上的 @ApplicationModule 注解使其依次成为一个嵌套的应用模块。 在这种安排下,适用以下访问规则:spring-doc.cadn.net.cn

  • Nested 中的代码仅可从 Inventory 或嵌套在 Inventory 内部的兄弟应用模块所暴露的任何类型中访问。spring-doc.cadn.net.cn

  • Nested模块中的任何代码都可以访问父模块中的代码,即使是内部代码。 即,NestedApiNestedInternal都可以访问inventory.internal.SomethingInventoryInternalspring-doc.cadn.net.cn

  • 嵌套模块中的代码也可以通过顶层应用模块访问暴露的类型。 nested(或其任何子包)中的任何代码都可以访问 OrderManagementspring-doc.cadn.net.cn

开放应用模块

上述描述的排列方式被视为封闭的,因为它们仅向其他主动选择暴露的模块公开类型。 当将 Spring Modulith 应用于遗留应用程序时,将所有嵌套包中的类型对其他模块隐藏可能不够充分,或者需要同样将这些包标记为可暴露。spring-doc.cadn.net.cn

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

声明应用程序模块为开放状态
@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule
import org.springframework.modulith.PackageInfo

@ApplicationModule(
  type = Type.OPEN
)
@PackageInfo
class ModuleMetadata {}

将应用程序模块声明为开放会导致以下验证变更:spring-doc.cadn.net.cn

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

显式应用程序模块依赖

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

库存:显式配置模块依赖项
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order"
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule

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

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

命名接口

默认情况下,如 高级应用模块 中所述,应用模块的根包被视为 API 包,因此是唯一允许其他模块传入依赖的包。 如果您希望向其他模块暴露额外的包,则需要使用命名接口。 您可以通过使用 @NamedInterface 或使用显式标注了 @org.springframework.modulith.PackageInfo 的类型来注解这些包的 package-info.java 文件来实现这一点。spring-doc.cadn.net.cn

用于封装名为 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
package-info.java in example.order.spi
@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 模块正在使用它,它可以像这样引用上述声明的命名接口:spring-doc.cadn.net.cn

定义允许依赖的专用命名接口
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: spi"
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule
import org.springframework.modulith.PackageInfo

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

注意我们如何通过双冒号 :: 拼接命名接口的名称 spi。 在此配置下,inventory 中的代码允许依赖于 SomeSpiInterface 以及位于 order.spi 接口中的其他代码,但不能例如依赖于 OrderManagement。 对于未显式描述依赖关系的模块,应用程序模块的根包 SPI 包均可访问。spring-doc.cadn.net.cn

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

使用星号声明允许依赖所有已命名的接口
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: *"
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule
import org.springframework.modulith.PackageInfo

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

如果您需要对应用程序模块的命名接口进行更通用的控制,请查看自定义部分spring-doc.cadn.net.cn

自定义应用程序模块的排列

Spring Modulith 允许配置围绕您通过 @Modulithic 注解创建的应用程序模块排列的一些核心方面,该注解用于主 Spring Boot 应用程序类。spring-doc.cadn.net.cn

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(MyApplication.class, args);
  }
}
package example

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

@Modulithic
@SpringBootApplication
class MyApplication

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

该注解公开了以下属性以供自定义:spring-doc.cadn.net.cn

注解属性 描述

systemNamespring-doc.cadn.net.cn

将在生成的文档中使用的应用程序的人类可读名称。spring-doc.cadn.net.cn

sharedModulesspring-doc.cadn.net.cn

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

additionalPackagesspring-doc.cadn.net.cn

指示 Spring Modulith 将配置的包视为额外的根应用包。换句话说,也会触发对这些包的应用模块检测。spring-doc.cadn.net.cn

自定义模块检测

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

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

如果默认的应用模块检测策略和手动注解的方式都不适用于您的应用程序,可以通过提供 ApplicationModuleDetectionStrategy 的实现来自定义模块检测。 该接口暴露了一个单一方法 Stream<JavaPackage> getModuleBasePackages(JavaPackage),并将使用 Spring Boot 应用程序类所在的包作为参数进行调用。 随后,您可以检查该包下的子包,并根据命名约定或其他规则选择要视为应用模块基础包的包。spring-doc.cadn.net.cn

假设您声明了一个自定义的 ApplicationModuleDetectionStrategy 实现,如下所示:spring-doc.cadn.net.cn

实现自定义 ApplicationModuleDetectionStrategy
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-strategyspring-doc.cadn.net.cn

spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy

如果您正在实现 ApplicationModuleDetectionStrategy 接口以自定义模块的验证和文档,请将自定义内容及其注册包含在应用程序的测试源码中。 然而,如果您使用的是 Spring Modulith 的 运行时组件(例如 ApplicationModuleInitializer,或诸如 Actuator 和可观测性支持等 生产就绪功能),则需要显式声明以下内容作为编译时依赖:spring-doc.cadn.net.cn

<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-core</artifactId>
</dependency>
dependencies {
  implementation 'org.springframework.modulith:spring-modulith-core'
}

从其他包贡献应用模块

虽然 @Modulithic 允许定义 additionalPackages 以触发对注释类所在包之外的其他包的应用程序模块检测,但其使用需要预先了解这些包。 自 1.3 版本起,Spring Modulith 通过 ApplicationModuleSourceApplicationModuleSourceFactory 抽象支持应用程序模块的外部贡献。 后者的实现可以注册到位于 META-INFspring.factories 文件中。spring-doc.cadn.net.cn

org.springframework.modulith.core.ApplicationModuleSourceFactory=example.CustomApplicationModuleSourceFactory

这样的工厂既可以返回任意的包名以应用 ApplicationModuleDetectionStrategy,也可以显式地返回要为其创建模块的包。spring-doc.cadn.net.cn

package example;

public class CustomApplicationModuleSourceFactory implements ApplicationModuleSourceFactory {

  @Override
  public List<String> getRootPackages() {
    return List.of("com.acme.toscan");
  }

  @Override
  public ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
    return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
  }

  @Override
  public List<String> getModuleBasePackages() {
    return List.of("com.acme.module");
  }
}

上述示例将使用 com.acme.toscan 来检测其中 显式声明的模块,并基于 com.acme.module 创建一个应用模块。 从这些返回的包名随后将通过 ApplicationModuleDetectionStrategy 中暴露的相应 getApplicationModuleSource(…) 变体转换为 ApplicationModuleSourcespring-doc.cadn.net.cn

自定义命名接口检测

如果您希望以编程方式描述应用程序模块的命名接口,请按照此处所述注册一个ApplicationModuleDetectionStrategy,并使用detectNamedInterfaces(JavaPackage, ApplicationModuleInformation)来实现自定义发现算法。spring-doc.cadn.net.cn

使用自定义 ApplicationModuleDetectionStrategy 定制命名接口检测
package example;

class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {

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

  @Override
  NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
    return NamedInterfaces.builder()
        .recursive()
        .matching("api")
        .build();
  }
}
package example

class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {

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

  override fun detectNamedInterfaces(basePackage: JavaPackage, information: ApplicationModuleInformation): NamedInterfaces {
    return NamedInterfaces.builder()
        .recursive()
        .matching("api")
        .build()
  }
}

在上方展示的 detectNamedInterfaces(…) 实现中,我们为给定应用模块基础包下所有名为 api 的包构建了一个 NamedInterfaces 实例。 Builder API 提供了额外的方法,用于选择具名接口或显式地将它们排除在外。 请注意,构建器将始终包含未命名的接口(该接口包含位于应用模块基础包中的所有公共方法),因为应用模块需要此接口。spring-doc.cadn.net.cn

若要更手动下载设置 NamedInterfaces,请务必查看其工厂方法以及由 NamedInterface 提供的方法。spring-doc.cadn.net.cn