Fundamentals

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

Spring Modulith supports developers implementing logical modules in Spring Boot applications. It allows them to apply structural validation, document the module arrangement, run integration tests for individual modules, observe the modules' interaction at runtime, and generally implement module interaction in a loosely coupled way. This section will discuss the fundamental concepts that developers need to understand before diving into the technical support.

Application modules

在 Spring Boot 应用程序中,应用程序模块是一个功能单元,包含以下部分:

In a Spring Boot application, an application module is a unit of functionality that consists of the following parts:

  • An API exposed to other modules implemented by Spring bean instances and application events published by the module, usually referred to as provided interface.

  • Internal implementation components that are not supposed to be accessed by other modules.

  • References to API exposed by other modules in the form of Spring bean dependencies, application events listened to and configuration properties exposed, usually referred to as required interface.

Spring Modulith 提供了在 Spring Boot 应用程序中表达模块的不同方法,主要区别在于总体安排中所涉及的复杂程度。这允许开发人员从简单开始,并在需要时自然地转向更复杂的手段。

Spring Moduliths provides different ways of expressing modules within Spring Boot applications, primarily differing in the level of complexity involved in the overall arrangement. This allows developers to start simple and naturally move to more sophisticated means as and if needed.

Simple Application Modules

应用程序的 主程序包 是包含主应用程序类的程序包,即使用 @SpringBootApplication 进行注释的类,它通常包含用于运行该类的 main(…) 方法。默认情况下,主包的每个直接子包都被视为 应用程序模块包

The application’s main package is the one that the main application class resides in. That is the class, that is annotated with @SpringBootApplication and usually contains the main(…) method used to run it. By default, each direct sub-package of the main package is considered an application module package.

如果此包不包含任何子包,它就会被视作一个简单的包。它允许使用 Java 的包范围将其内部的代码隐藏起来,以防止其他包中的代码引用这些类型,从而不让这些类型受到这些包的依赖注入的影响。因此,自然而然地,模块的 API 包括包中的所有公开类型。

If this package does not contain any sub-packages, it is considered a simple one. It allows to hide code inside it by using Java’s package scope to hide types from being referred to by code residing in other packages and thus not subject to dependency injection into those. Thus, naturally, the module’s API consists of all public types in the package.

让我们看一下一个示例安排 ([role="green"] 表示公有类型,[role="red"] 表示包私有类型)。

Let us have a look at an example arrangement ([role="green"] denotes a public type, [role="red"] a package-private one).

A single inventory application module
 Example
└─  src/main/java
   ├─  example                        1
   |  └─ [role="green"] Application.java
   └─  example.inventory              2
      ├─ [role="green"] InventoryManagement.java
      └─ [role="red"] SomethingInventoryInternal.java
1 The application’s main package example.
2 An application module package inventory.

Advanced Application Modules

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

If an application module package contains sub-packages, types in those might need to be made public so that it can be referred to from code of the very same module.

An inventory and order application module
 Example
└─  src/main/java
   ├─  example
   |  └─ [role="green"] Application.java
   ├─  example.inventory
   |  ├─ [role="green"] InventoryManagement.java
   |  └─ [role="red"] SomethingInventoryInternal.java
   ├─ * example.order*
   |  └─ [role="green"] OrderManagement.java
   └─  example.order.internal
      └─ [role="green"] SomethingOrderInternal.java

在这种排列中,order 包被视为 API 包。允许来自其他应用程序模块的代码引用其中的类型。order.internal 就像应用程序模块基础包的任何其他子包一样,被视为 内部 包。这些包中的代码不得从其他模块引用。请注意,SomethingOrderInternal 如何成为一个公开类型,这可能是因为 OrderManagement 依赖于它。不幸的是,这意味着也可以从其他包(如 inventory 包)引用它。在这种情况下,Java 编译器在防止这些非法引用方面并没有多大用处。

In such an arrangement, the order package is considered an API package. Code from other application modules is allowed to refer to types within that. order.internal, just as any other sub-package of the application module base package, is considered an internal one. Code within those must not be referred to from other modules. Note, how SomethingOrderInternal is a public type, likely because OrderManagement depends on it. This unfortunately means that it can also be referred to from other packages such as the inventory one. In this case, the Java compiler is not of much use to prevent these illegal references.

Open Application Modules

因为 above 中描述的安排仅向其他模块公开主动选择公开的类型,所以可视为封闭的。当将 Spring Modulith 应用于遗留应用程序时,将位于嵌套包中的所有类型都隐藏起来可能不足,或者可能需要将所有这些包也标记为要公开的包。

The arrangement described above are considered closed as they only expose types to other modules that are actively selected for exposure. When applying Spring Modulith to legacy applications, hiding all types located in nested packages from other modules might be inadequate or require marking all those packages for exposure, too.

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

To turn an application module into an open one, use the @ApplicationModule annotation on the package-info.java type.

Declaring an Application Modules as Open
  • Java

  • Kotlin

将应用程序模块声明为开放后,将对验证进行以下更改:

Declaring an application module as open will cause the following changes to the verification:

  • Access to application module internal types from other modules is generally allowed.

  • All types, also ones residing in sub-packages of the application module base package are added to the unnamed named interface, unless explicitly assigned to a named interface.

此功能主要用于现有项目的代码库,它们逐渐迁移到 Spring Modulith 建议的打包结构。在完全模块化的应用程序中,使用开放应用程序模块通常提示模块化和打包结构达不到最优。

This feature is intended to be primarily used with code bases of existing projects gradually moving to the Spring Modulith recommended packaging structure. In a fully-modularized application, using open application modules usually hints at sub-optimal modularization and packaging structures.

Explicit Application Module Dependencies

模块可以通过包上的 @ApplicationModule 注解选择声明其允许的依赖关系,该注解通过 package-info.java 文件表示。例如,Kotlin 缺乏对该文件提供支持,因此您还可以在位于应用程序模块根包中的单个类型上使用该注解。

A module can opt into declaring its allowed dependencies by using the @ApplicationModule annotation on the package, represented through the package-info.java file. As, for example, Kotlin lacks support for that file, you can also use the annotation on a single type located in the application module’s root package.

Inventory explicitly configuring module dependencies
  • Java

  • Kotlin

import org.springframework.modulith.ApplicationModule

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

在这种情况下,inventory 模块中的代码只能引用 order 模块中的代码(以及首先未分配到任何模块中的代码)。了解如何在 Verifying Application Module Structure 中监控该代码。

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.

The ApplicationModules Type

Spring Moduliths 允许检查代码库,以基于给定的排列和可选的配置派生出一个应用程序模块模型。spring-modulith-core 工件包含可以指向 Spring Boot 应用程序类的 ApplicationModules

Spring Moduliths allows to inspect a codebase to derive an application module model based on the given arrangement and optional configuration. The spring-modulith-core artifact contains ApplicationModules that can be pointed to a Spring Boot application class:

Creating an application module model
  • Java

  • Kotlin

var modules = ApplicationModules.of(Application.class);
var modules = ApplicationModules.of(Application::class)

为了了解已分析的排列是什么样子的,我们可以只将包含在整个模型中的各个模块写到控制台中:

To get an impression of what the analyzed arrangement looks like, we can just write the individual modules contained in the overall model to the console:

Writing the application module arrangement to the console
  • Java

  • Kotlin

modules.forEach(System.out::println);
modules.forEach(println(it))
The console output of our application module arrangement
## 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 组件,并且也渲染了各自的可见性。

Note how each module is listed, the contained Spring components are identified, and the respective visibility is rendered, too.

Named Interfaces

根据 Advanced Application Modules 中的描述,默认情况下,应用程序模块的基本包被视为 API 包,因此是允许来自其他模块的传入依赖项的唯一包。如果你想向其他模块公开其他包,则需要使用 named interfaces。你可以通过使用 @NamedInterface 或明确标注了 @org.springframework.modulith.PackageInfo 的类型来注释这些包的 package-info.java 文件来实现此目的。

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.

A package arrangement to encapsulate an SPI named interface
 Example
└─  src/main/java
   ├─  example
   |  └─ [role="green"] Application.java
   ├─ …
   ├─  example.order
   |  └─ [role="green"] OrderManagement.java
   ├─ * example.order.spi*
   |  ├—  package-info.java
   |  └─ [role="green"] SomeSpiInterface.java
   └─  example.order.internal
      └─ [role="green"] SomethingOrderInternal.java
package-info.java in example.order.spi
  • Java

  • Kotlin

import org.springframework.modulith.PackageInfo
import org.springframework.modulith.NamedInterface

@PackageInfo
@NamedInterface("spi")
class ModuleMetadata {}

该声明的效果是双重的:首先,允许其他应用程序模块中的代码引用 SomeSpiInterface。应用程序模块能够在明确的依赖关系声明中引用命名接口。假设 inventory 模块正在使用它,则它可以像这样引用上面声明的命名接口:

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:

  • Java

  • Kotlin

请注意,我们将命名接口的名字 spi 通过双冒号 :: 连接起来。在此设置中,inventory 中的代码将被允许依赖 SomeSpiInterface 和驻留在 order.spi 接口中的其他代码,但不能依赖例如 OrderManagement。对于没有明确描述依赖关系的模块,应用程序模块根包 SPI 包都可以访问。

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.

Customizing Module Detection

如果默认的应用程序模块模型不适用于您的应用程序,可以通过提供 ApplicationModuleDetectionStrategy 的实现来自定义模块的检测。该接口公开了一个单一的方法 Stream<JavaPackage> getModuleBasePackages(JavaPackage),并且它将使用 Spring Boot 应用程序类所在的包被调用。然后,您可以检查驻留在该包内的包,并根据命名约定或类似约定选择要视为应用程序模块基础包的包。

If the default application module model does not work 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.

假设您声明了一个自定义的 ApplicationModuleDetectionStrategy 实现如下:

Assume you declare a custom ApplicationModuleDetectionStrategy implementation like this:

  • Java

  • Kotlin

class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {

  @Override
  public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
    // Your module detection goes here
  }
}
class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {

  override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
    // Your module detection goes here
  }
}

此类需要在 META-INF/spring.factories 中注册,如下所示:

This class needs to be registered in META-INF/spring.factories as follows:

org.springframework.modulith.core.ApplicationModuleDetectionStrategy=\
  example.CustomApplicationModuleDetectionStrategy

Customizing the Application Modules Arrangement

Spring Moduliths 允许通过要在 Spring Boot 应用程序主类上使用的 @Modulithic 注解配置围绕您通过应用程序模块排列创建的某些核心方面。

Spring Moduliths 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.

  • Java

  • Kotlin

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(DemoApplication.class, args);
  }
}
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.modulith.Modulithic

@Modulithic
@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
  runApplication<DemoApplication>(*args)
}

注释展示了用于定制的以下属性:

The annotation exposes the following attributes to customize:

Annotation attribute Description

systemName

The human readable name of the application to be used in generated documentation.

sharedModules

Declares the application modules with the given names as shared modules, which means that they will be always included in application module integration tests.

additionalPackages

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.