基礎知識
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.a或com.example.db.b.c) 中的所有檔案。 -
..example..— 匹配a.example、a.example.b或a.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 中的程式碼僅可從 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 推薦的打包結構。在完全模組化的應用程式中,使用開放應用模組通常暗示著次優的模組化和打包結構。 |
顯式應用模組依賴
模組可以選擇透過在包上使用 @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 顯式註解型別來實現這一點。
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 允許配置圍繞您透過 @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 支援透過 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。
自定義命名介面檢測
如果您想以程式設計方式描述應用模組的命名介面,請註冊一個 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 公開的方法。