基本概念
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.a
或com.example.db.b.c
)。 -
..example..
— 匹配a.example
、a.example.b
或a.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 模組中的任何程式碼都可以訪問父模組中的程式碼,即使是內部程式碼。例如,
NestedApi
和NestedInternal
都可以訪問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 不支援該檔案,您也可以在位於應用模組根包中的單個型別上使用該註解。
-
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
註解的型別來實現。
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)
}
該註解暴露了以下屬性用於定製
註解屬性 | 描述 |
---|---|
|
用於生成的文件中應用的人類可讀名稱。 |
|
將給定名稱的應用模組宣告為共享模組,這意味著它們將始終包含在應用模組整合測試中。 |
|
指示 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 透過 ApplicationModuleSource
和 ApplicationModuleSourceFactory
抽象支援應用模組的外部貢獻。後者的一個實現可以註冊在位於 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
。