此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Modulith 1.3.4spring-doc.cadn.net.cn

基础

Spring Modulith 支持开发人员在 Spring Boot 应用程序中实现逻辑模块。 它允许他们应用结构验证、记录模块安排、为单个模块运行集成测试、在运行时观察模块的交互,并且通常以松散耦合的方式实现模块交互。 本节将讨论开发人员在深入研究技术支持之前需要了解的基本概念。spring-doc.cadn.net.cn

应用程序模块

在 Spring Boot 应用程序中,应用程序模块是一个功能单元,它由以下部分组成:spring-doc.cadn.net.cn

  • 向 Spring Bean 实例实现的其他模块和模块发布的应用程序事件公开的 API,通常称为提供的接口spring-doc.cadn.net.cn

  • 不应被其他模块访问的内部实现组件。spring-doc.cadn.net.cn

  • 对其他模块以 Spring bean 依赖项、侦听的应用程序事件和公开的配置属性的形式公开的 API 的引用,通常称为必需接口spring-doc.cadn.net.cn

Spring Modulith 提供了在 Spring Boot 应用程序中表达模块的不同方式,主要区别在于整体安排所涉及的复杂程度。 这允许开发人员从简单开始,然后根据需要自然地转向更复杂的方法。spring-doc.cadn.net.cn

ApplicationModules类型

Spring Modulith 允许检查代码库,以根据给定的排列和可选配置派生应用程序模块模型。 这spring-modulith-coreartifact 包含ApplicationModules可以指向 Spring Boot 应用程序类:spring-doc.cadn.net.cn

创建应用程序模块模型
var modules = ApplicationModules.of(Application.class);
val modules = ApplicationModules.of(Application::class.java)

modules将包含从代码库派生的应用程序模块排列的内存中表示。 其中的哪些部分将被检测为模块取决于类指向的包所在的包下面的 Java 包结构。 了解有关 Simple Application Modules 中默认预期的排列方式的更多信息。 高级排列和定制选项在高级应用程序模块spring-doc.cadn.net.cn

为了获得所分析的 arrangement 的印象,我们只需将整个模型中包含的各个模块写入控制台:spring-doc.cadn.net.cn

将应用程序模块排列写入控制台
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 组件是如何被识别的,以及相应的可见性是如何呈现的。spring-doc.cadn.net.cn

排除包

如果您想从应用程序模块检查中排除某些 Java 类或完整包,您可以通过以下方式执行此作:spring-doc.cadn.net.cn

ApplicationModules.of(Application.class, JavaClass.Predicates.resideInAPackage("com.example.db")).verify();
ApplicationModules.of(Application::class.java, JavaClass.Predicates.resideInAPackage("com.example.db")).verify()

排除项的其他示例:spring-doc.cadn.net.cn

  • com.example.db— 匹配给定包中的所有文件com.example.db.spring-doc.cadn.net.cn

  • com.example.db..— 匹配给定包中的所有文件 (com.example.db) 和所有子软件包 (com.example.db.acom.example.db.b.c).spring-doc.cadn.net.cn

  • ..example..— 比赛a.example,a.example.ba.b.example.c.d,但不是a.exam.bspring-doc.cadn.net.cn

有关可能的匹配器的完整详细信息,请参见 ArchUnit 的 JavaDocPackageMatcher.spring-doc.cadn.net.cn

简单的应用程序模块

应用程序的主包是主应用程序类所在的包。 即 class ,它被注解为@SpringBootApplication,通常包含main(…)用于运行它的方法。 默认情况下,主包的每个直接子包都被视为一个应用程序模块包spring-doc.cadn.net.cn

如果此 package 不包含任何子 package,则认为它是一个简单的 package。 它允许通过使用 Java 的包范围来隐藏其中的代码,以隐藏驻留在其他包中的代码引用的类型,从而不受依赖项注入到这些包中的影响。 因此,该模块的 API 自然由包中的所有公共类型组成。spring-doc.cadn.net.cn

让我们看一个示例安排(表示 public 类型, package-private 类型)。spring-doc.cadn.net.cn

单个库存应用程序模块
 Example
└─  src/main/java
   ├─  example                        (1)
   |  └─  Application.java
   └─  example.inventory              (2)
      ├─  InventoryManagement.java
      └─  SomethingInventoryInternal.java
1 应用程序的 main 软件包example.
2 应用程序模块包inventory.

高级应用程序模块

如果应用程序模块包包含子包,则可能需要公开这些子包中的类型,以便可以从同一模块的代码中引用它。spring-doc.cadn.net.cn

库存和订单应用程序模块
 Example
└─  src/main/java
   ├─  example
   |  └─  Application.java
   ├─  example.inventory
   |  ├─  InventoryManagement.java
   |  └─  SomethingInventoryInternal.java
   ├─  example.order
   |  └─  OrderManagement.java
   └─  example.order.internal
      └─  SomethingOrderInternal.java

在这样的安排中,orderpackage 被视为 API 包。 允许来自其他应用程序模块的代码引用其中的类型。order.internal,就像应用程序模块基础包的任何其他子包一样,被视为内部子包。 这些模块中的代码不得从其他模块引用。 请注意作方法SomethingOrderInternal是公共类型,可能是因为OrderManagement取决于它。 不幸的是,这意味着它也可以从其他包中引用,例如inventory一。 在这种情况下,Java 编译器对于防止这些非法引用没有多大用处。spring-doc.cadn.net.cn

嵌套应用程序模块

从版本 1.3 开始,Spring Modulith 应用程序模块可以包含嵌套模块。 这允许在模块包含要依次逻辑分离的部件的情况下管理内部结构。 要定义嵌套的应用程序模块,请显式注释应该由@ApplicationModule.spring-doc.cadn.net.cn

 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如上所述的应用程序模块。 这@ApplicationModule注解nestedpackage 导致它反过来成为一个嵌套的应用程序模块。 在这种安排中,以下访问规则适用:spring-doc.cadn.net.cn

  • Nested 中的代码只能从 Inventory 或嵌套在 Inventory 中的同级应用程序模块公开的任何类型的代码中获得。spring-doc.cadn.net.cn

  • Nested 模块中的任何代码都可以访问父模块中的代码,甚至是内部模块中的代码。 即,两者NestedApiNestedInternal可以访问inventory.internal.SomethingInventoryInternal.spring-doc.cadn.net.cn

  • 嵌套模块中的代码还可以访问顶级应用程序模块的公开类型。 任何nested(或任何子包)可以访问OrderManagement.spring-doc.cadn.net.cn

打开应用程序模块

上述安排被认为是封闭的,因为它们只将类型公开给主动选择公开的其他模块。 当将 Spring Modulith 应用于遗留应用程序时,对其他模块隐藏位于嵌套包中的所有类型可能是不够的,或者还需要标记所有这些包以进行公开。spring-doc.cadn.net.cn

要将应用程序模块转换为开放模块,请使用@ApplicationModule注解package-info.java类型。spring-doc.cadn.net.cn

将应用程序模块声明为打开
@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  type = Type.OPEN
)
package example.inventory

将应用程序模块声明为 open 将导致验证发生以下更改:spring-doc.cadn.net.cn

此功能主要用于现有项目的代码库,这些项目正在逐渐转向 Spring Modulith 推荐的打包结构。 在完全模块化的应用程序中,使用开放式应用程序模块通常暗示着次优的模块化和打包结构。

显式应用程序模块依赖项

模块可以通过使用@ApplicationModule注解,通过package-info.java文件。 例如,由于 Kotlin 不支持该文件,因此您还可以对位于应用程序模块的根包中的单个类型使用注释。spring-doc.cadn.net.cn

清单显式配置模块依赖项
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order"
)
package example.inventory;
package example.inventory

import org.springframework.modulith.ApplicationModule

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

In this case code within the inventory module was only allowed to refer to code in the order module (and code not assigned to any module in the first place). Find out about how to monitor that in Verifying Application Module Structure.spring-doc.cadn.net.cn

Named Interfaces

By default and as described in Advanced Application Modules, an application module’s base package is considered the API package and thus is the only package to allow incoming dependencies from other modules. In case you would like to expose additional packages to other modules, you need to use named interfaces. You achieve that by annotating the package-info.java file of those packages with @NamedInterface or a type explicitly annotated with @org.springframework.modulith.PackageInfo.spring-doc.cadn.net.cn

A package arrangement to encapsulate an SPI named interface
 Example
└─  src/main/java
   ├─  example
   |  └─  Application.java
   ├─ …
   ├─  example.order
   |  └─  OrderManagement.java
   ├─  example.order.spi
   |  ├—  package-info.java
   |  └─  SomeSpiInterface.java
   └─  example.order.internal
      └─  SomethingOrderInternal.java
package-info.java in example.order.spi
@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 {}

The effect of that declaration is twofold: first, code in other application modules is allowed to refer to SomeSpiInterface. Application modules are able to refer to the named interface in explicit dependency declarations. Assume the inventory module was making use of that, it could refer to the above declared named interface like this:spring-doc.cadn.net.cn

Defining allowed dependencies to dedicated named interfaces
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: spi"
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: spi"
)
package example.inventory

Note how we concatenate the named interface’s name spi via the double colon ::. In this setup, code in inventory would be allowed to depend on SomeSpiInterface and other code residing in the order.spi interface, but not on OrderManagement for example. For modules without explicitly described dependencies, both the application module root package and the SPI one are accessible.spring-doc.cadn.net.cn

If you wanted to express that an application module is allowed to refer to all explicitly declared named interfaces, you can use the asterisk () as follows:*spring-doc.cadn.net.cn

Using the asterisk to declare allowed dependencies to all declared named interfaces
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: *"
)
package example.inventory;
@org.springframework.modulith.ApplicationModule(
  allowedDependencies = "order :: *"
)
package example.inventory

If you require more generic control about the named interfaces of an application module, check out the customization section.spring-doc.cadn.net.cn

Customizing the Application Modules Arrangement

Spring Modulith allows to configure some core aspects around the application module arrangement you create via the @Modulithic annotation to be used on the main Spring Boot application class.spring-doc.cadn.net.cn

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

The annotation exposes the following attributes to customize:spring-doc.cadn.net.cn

Annotation attribute Description

systemNamespring-doc.cadn.net.cn

The human readable name of the application to be used in generated documentation.spring-doc.cadn.net.cn

sharedModulesspring-doc.cadn.net.cn

Declares the application modules with the given names as shared modules, which means that they will always be included in application module integration tests.spring-doc.cadn.net.cn

additionalPackagesspring-doc.cadn.net.cn

Instructs Spring Modulith to treat the configured packages as additional root application packages. In other words, application module detection will be triggered for those as well.spring-doc.cadn.net.cn

Customizing Module Detection

By default, application modules will be expected to be located in direct sub-packages of the package the Spring Boot application class resides in. An alternative detection strategy can be activated to only consider packages explicitly annotated, either via Spring Modulith’s @ApplicationModule or jMolecules @Module annotation. That strategy can be activated by configuring the spring.modulith.detection-strategy to explicitly-annotated.spring-doc.cadn.net.cn

Switching the application module detection strategy to only consider annotated packages
spring.modulith.detection-strategy=explicitly-annotated

If neither the default application module detection strategy nor the manually annotated one works for your application, the detection of the modules can be customized by providing an implementation of ApplicationModuleDetectionStrategy. That interface exposes a single method Stream<JavaPackage> getModuleBasePackages(JavaPackage) and will be called with the package the Spring Boot application class resides in. You can then inspect the packages residing within that and select the ones to be considered application module base packages based on a naming convention or the like.spring-doc.cadn.net.cn

Assume you declare a custom ApplicationModuleDetectionStrategy implementation like this:spring-doc.cadn.net.cn

Implementing a custom ApplicationModuleDetectionStrategy
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
  }
}

This class can now be registered as spring.modulith.detection-strategy as follows:spring-doc.cadn.net.cn

spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy

If you are implementing the ApplicationModuleDetectionStrategy interface to customize the verification and documentation of modules, include the customization and its registration in your application’s test sources. However, if you are using Spring Modulith runtime components (such as the ApplicationModuleInitializers, or the production-ready features like the actuator and observability support), you need to explicitly declare the following as a compile-time dependency:spring-doc.cadn.net.cn

<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-core</artifactId>
</dependency>
dependencies {
  implementation 'org.springframework.modulith:spring-modulith-core'
}

Contributing Application Modules From Other Packages

While @Modulithic allows defining additionalPackages to trigger application module detection for packages other than the one of the annotated class, its usage requires knowing about those in advance. As of version 1.3, Spring Modulith supports external contributions of application modules via the ApplicationModuleSource and ApplicationModuleSourceFactory abstractions. An implementation of the latter can be registered in a spring.factories file located in META-INF.spring-doc.cadn.net.cn

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

Such a factory can either return arbitrary package names to get an ApplicationModuleDetectionStrategy applied, or explicitly return packages to create modules for.spring-doc.cadn.net.cn

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");
  }
}

The above example would use com.acme.toscan to detect explicitly declared modules within that and also create an application module from com.acme.module. The package names returned from these will subsequently be translated into ApplicationModuleSources via the corresponding getApplicationModuleSource(…) flavors exposed in ApplicationModuleDetectionStrategy.spring-doc.cadn.net.cn

Customizing Named Interface detection

If you would like to programatically describe the named interfaces of an application module, register an ApplicationModuleDetectionStrategy as described here and use the detectNamedInterfaces(JavaPackage, ApplicationModuleInformation) to implement a custom discovery algorithm.spring-doc.cadn.net.cn

Customizing the named interface detection using a custom ApplicationModuleDetectionStrategy
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()
  }
}

In the detectNamedInterfaces(…) implementation shown above, we build up a NamedInterfaces instance for all packages named api underneath the given application module’s base package. The Builder API exposes additional methods to select packages as named interfaces or explicitly exclude them from that. Note, that the builder will always include the unnamed named interface containing all public methods located in the application module’s base package as that interface is required for application modules.spring-doc.cadn.net.cn

For a more manual setup of a NamedInterfaces, be sure to check out its factory methods and the ones exposed by NamedInterface.spring-doc.cadn.net.cn