基本概念

Spring Modulith 支援開發者在 Spring Boot 應用中實現邏輯模組。它允許對模組結構應用驗證,文件化模組編排,為單個模組執行整合測試,在執行時觀察模組之間的互動,並且通常以鬆散耦合的方式實現模組互動。本節將討論開發者在深入瞭解技術支援之前需要理解的基本概念。

應用模組

在 Spring Boot 應用中,應用模組是一個功能單元,由以下幾個部分組成

  • 向其他模組暴露的 API,由 Spring bean 例項和模組釋出的應用事件實現,通常稱為提供的介面(provided interface)。

  • 內部實現元件,不應被其他模組訪問。

  • 以 Spring bean 依賴、監聽的應用事件和暴露的配置屬性形式引用的其他模組暴露的 API,通常稱為所需介面(required interface)。

Spring Modulith 提供了不同的方式在 Spring Boot 應用中表達模組,主要區別在於整體編排的複雜程度。這允許開發者從簡單開始,並在需要時自然地轉向更復雜的方式。

ApplicationModules 型別

Spring Modulith 允許檢查程式碼庫,根據給定的編排和可選配置來派生應用模組模型。spring-modulith-core Artifact 包含 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 中找到。

簡單的應用模組

應用的主包(main package)是主應用類所在的包。它是被 @SpringBootApplication 註解且通常包含用於執行應用的 main(…) 方法的類。預設情況下,主包的每個直接子包都被視為應用模組包(application module package)。

如果這個包不包含任何子包,它就被認為是一個簡單的包。它允許透過使用 Java 的包作用域來隱藏其中的程式碼,從而使型別不被駐留在其他包中的程式碼引用,因此也不受依賴注入的影響。因此,很自然地,模組的 API 由包中所有公共型別組成。

讓我們看一個示例編排( 表示公共型別, 表示包私有型別)。

一個單獨的庫存應用模組
 Example
╰─  src/main/java
   ├─  example                        (1)
   │  ╰─  Application.java
   ╰─  example.inventory              (2)
      ├─  InventoryManagement.java
      ╰─  SomethingInventoryInternal.java
1 應用的主包 example
2 一個應用模組包 inventory

高階應用模組

如果一個應用模組包包含子包,這些子包中的型別可能需要設為公共(public),以便同一模組的程式碼可以引用它們。

一個庫存和訂單應用模組
 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 中的程式碼僅可從 Inventory 或任何巢狀在 Inventory 內部的同級應用模組暴露的型別訪問。

  • 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 推薦的包結構。在一個完全模組化的應用中,使用開放應用模組通常暗示著模組化和包結構的次優性。

顯式的應用模組依賴

模組可以透過在包(由 package-info.java 檔案表示)上使用 @ApplicationModule 註解來選擇宣告其允許的依賴項。例如,由於 Kotlin 不支援該檔案,您也可以在位於應用模組根包中的單個型別上使用該註解。

Inventory 顯式配置模組依賴
  • 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 包,因此是唯一允許來自其他模組傳入依賴的包。如果您想向其他模組暴露額外的包,需要使用命名介面(named interfaces)。您可以透過使用 @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 中的程式碼將被允許依賴 SomeSpiInterface 以及駐留在 order.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 允許配置圍繞應用模組編排的一些核心方面,您可以透過在主 Spring Boot 應用類上使用 @Modulithic 註解來實現。

  • 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)
}

該註解暴露了以下屬性用於定製

註解屬性 描述

systemName

用於生成的文件中應用的人類可讀名稱。

sharedModules

將給定名稱的應用模組宣告為共享模組,這意味著它們將始終包含在應用模組整合測試中。

additionalPackages

指示 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生產就緒特性,如 Actuator 和可觀察性支援),您需要顯式地將以下內容宣告為編譯時依賴

  • 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-INFspring.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