基础知识

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

应用模块

在 Spring Boot 应用中,应用模块是功能单元,包含以下部分:

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

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

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

Spring Modulith 提供了在 Spring Boot 应用中表达模块的不同方式,主要区别在于整体安排的复杂性级别。这允许开发者从简单开始,并根据需要自然地转向更复杂的方法。

ApplicationModules 类型

Spring Modulith 允许检查代码库,以根据给定的安排和可选配置派生应用模块模型。spring-modulith-core 工件包含 ApplicationModules,可以指向 Spring Boot 应用程序类

创建应用模块模型
  • 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 PackageMatcher 的 JavaDoc。

简单应用模块

应用程序的 主包 是主应用程序类所在的包。该类是带有 @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 编译器在防止这些非法引用方面作用不大。

嵌套应用模块

从版本 1.3 开始,Spring Modulith 应用模块可以包含嵌套模块。这允许在模块包含需要逻辑分离的部分时管理内部结构。要定义嵌套应用模块,请明确使用 @ApplicationModule 注解包。

 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 注解使其本身成为一个嵌套应用模块。在此安排中,适用以下访问规则

  • Nested 中的代码仅可从 InventoryInventory 内部嵌套的同级应用模块公开的任何类型访问。

  • Nested 模块中的任何代码都可以访问父模块中的代码,甚至是内部代码。即,NestedApiNestedInternal 都可以访问 inventory.internal.SomethingInventoryInternal

  • 来自嵌套模块的代码也可以访问顶级应用模块公开的类型。nested(或任何子包)中的任何代码都可以访问 OrderManagement

开放应用模块

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

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

将应用模块声明为开放
  • Java

  • Kotlin

@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 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 {}

在这种情况下,库存 模块中的代码只允许引用 订单 模块中的代码(以及最初未分配给任何模块的代码)。有关如何监控此情况的信息,请参阅验证应用模块结构

命名接口

默认情况下,如高级应用模块所述,应用模块的基包被视为 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;
package example.inventory

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

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

请注意我们如何通过双冒号 :: 串联命名接口的名称 spi。在此设置中,inventory 中的代码将被允许依赖 SomeSpiInterfaceorder.spi 接口中的其他代码,但不能依赖 OrderManagement 等。对于没有明确描述依赖项的模块,应用程序模块根包 SPI 包都是可访问的。

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

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

  • Kotlin

@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 Modulith 允许配置围绕您通过 @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(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 Modulith 将配置的包视为额外的根应用包。换句话说,这些包也将触发应用模块检测。

自定义模块检测

默认情况下,应用模块预计位于 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

如果您正在实现 ApplicationModuleDetectionStrategy 接口以自定义模块的验证和文档,请将自定义及其注册包含在应用程序的测试源中。但是,如果您正在使用 Spring Modulith 运行时组件(例如 ApplicationModuleInitializer生产就绪功能,如执行器和可观测性支持),您需要显式声明以下为编译时依赖项

  • Maven

  • Gradle

<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-INF 中的 spring.factories 文件中注册。

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

这样的工厂可以返回任意包名以应用 ApplicationModuleDetectionStrategy,也可以明确返回包以创建模块。

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(…) 变体转换为 ApplicationModuleSource

自定义命名接口检测

如果您想以编程方式描述应用模块的命名接口,请注册一个 ApplicationModuleDetectionStrategy,如此处所述,并使用 detectNamedInterfaces(JavaPackage, ApplicationModuleInformation) 来实现自定义发现算法。

使用自定义 ApplicationModuleDetectionStrategy 自定义命名接口检测
  • Java

  • Kotlin

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 公开了其他方法,用于选择包作为命名接口或明确将其排除。请注意,构建器将始终包含未命名的命名接口,其中包含应用模块基包中所有公共方法,因为该接口是应用模块所必需的。

要更手动地设置 NamedInterfaces,请务必查看其工厂方法以及 NamedInterface 公开的方法。

© . This site is unofficial and not affiliated with VMware.