基本概念
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
。