GH-285 - Migrate documentation to Antora.
This commit is contained in:
committed by
Oliver Drotbohm
parent
607d046621
commit
56036d151a
33
src/docs/antora/.github/workflows/deploy-docs.yml
vendored
Normal file
33
src/docs/antora/.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [ gh-pages ]
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
#schedule:
|
||||
#- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
# FIXME: enable when pushed to spring-projects
|
||||
# if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
37
src/docs/antora/antora-playbook.yml
Normal file
37
src/docs/antora/antora-playbook.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
antora:
|
||||
extensions:
|
||||
- '@springio/antora-extensions/partial-build-extension'
|
||||
- require: '@springio/antora-extensions/latest-version-extension'
|
||||
- require: '@springio/antora-extensions/inject-collector-cache-config-extension'
|
||||
- '@antora/collector-extension'
|
||||
- '@antora/atlas-extension'
|
||||
- require: '@springio/antora-extensions/root-component-extension'
|
||||
root_component_name: 'modulith'
|
||||
site:
|
||||
title: Spring Modulith
|
||||
url: https://docs.spring.io/spring-modulith/reference/
|
||||
content:
|
||||
sources:
|
||||
- url: ./../../..
|
||||
branches: HEAD
|
||||
start_path: src/docs/antora
|
||||
worktrees: true
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
chomp: 'all'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
sourcemap: true
|
||||
urls:
|
||||
latest_version_segment: ''
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
format: pretty
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip
|
||||
17
src/docs/antora/antora.yml
Normal file
17
src/docs/antora/antora.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: modulith
|
||||
version: true
|
||||
title: Spring Modulith
|
||||
nav:
|
||||
- modules/ROOT/nav.adoc
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: mvnw -N antora-component-version:antora-component-version resources:resources -am -Pdocumentation
|
||||
local: true
|
||||
scan:
|
||||
dir: target/classes/antora-resources/
|
||||
|
||||
asciidoc:
|
||||
attributes:
|
||||
attribute-missing: 'warn'
|
||||
chomp: 'all'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
src/docs/antora/modules/ROOT/assets/images/observability.png
Normal file
BIN
src/docs/antora/modules/ROOT/assets/images/observability.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
10
src/docs/antora/modules/ROOT/nav.adoc
Normal file
10
src/docs/antora/modules/ROOT/nav.adoc
Normal file
@@ -0,0 +1,10 @@
|
||||
* xref:index.adoc[Overview]
|
||||
* xref:fundamentals.adoc[]
|
||||
* xref:verification.adoc[]
|
||||
* xref:events.adoc[]
|
||||
* xref:testing.adoc[]
|
||||
* xref:moments.adoc[]
|
||||
* xref:documentation.adoc[]
|
||||
* xref:runtime.adoc[]
|
||||
* xref:production-ready.adoc[]
|
||||
* xref:appendix.adoc[]
|
||||
180
src/docs/antora/modules/ROOT/pages/appendix.adoc
Normal file
180
src/docs/antora/modules/ROOT/pages/appendix.adoc
Normal file
@@ -0,0 +1,180 @@
|
||||
[[appendix]]
|
||||
= Appendix
|
||||
:jdbc-schema-base: partial$spring-modulith-events-jdbc-src/main/resources
|
||||
|
||||
[appendix]
|
||||
[[configuration-properties]]
|
||||
== Spring Modulith Configuration Properties
|
||||
[cols="2,1,3", options="header, unbreakable"]
|
||||
|===
|
||||
|Property|Default value|Description
|
||||
|
||||
|`spring.modulith.default-async-termination`
|
||||
|`true`
|
||||
|Whether to configure defaults for the async processing termination, namely to wait for task completion for 2 seconds. See `TaskExecutionProperties` for details.
|
||||
|
||||
|`spring.modulith.events.jdbc.schema-initialization.enabled`
|
||||
|`false`
|
||||
|Whether to initialize the JDBC event publication schema.
|
||||
|
||||
|`spring.modulith.events.kafka.json-enabled`
|
||||
|`true`
|
||||
|Whether to enable JSON support for `KafkaTemplate`.
|
||||
|
||||
|`spring.modulith.events.mongodb.transaction-management.enabled`
|
||||
|`true`
|
||||
|Whether to automatically enable transactions for MongoDB. Requires the database to be run with a replica set.
|
||||
|
||||
|`spring.modulith.events.rabbitmq.json-enabled`
|
||||
|`true`
|
||||
|Whether to enable JSON support for `RabbitTemplate`.
|
||||
|
||||
|`spring.modulith.moments.enableTimeMachine`
|
||||
|`false`
|
||||
|Whether to enable the xref:moments.adoc[`TimeMachine`].
|
||||
|
||||
|`spring.modulith.moments.granularity`
|
||||
|`HOURS`
|
||||
|The granularity of events to publish. (`HOURS`, `DAYS`)
|
||||
|
||||
|`spring.modulith.moments.locale`
|
||||
|`Locale.getDefault()`
|
||||
|The `Locale` to use when determining week boundaries.
|
||||
|
||||
|`spring.modulith.moments.zoneId`
|
||||
|`ZoneOffset.UTC`
|
||||
|The timezone of the dates for the events being published.
|
||||
|
||||
|`spring.modulith.republish-outstanding-events-on-restart`
|
||||
|`false`
|
||||
|Whether to republish outstanding event publications on restarts of the application.
|
||||
|===
|
||||
|
||||
[appendix]
|
||||
[[artifacts]]
|
||||
== Spring Modulith modules
|
||||
|
||||
.Spring Modulith starter POMs
|
||||
[cols="2,1,3", options="header, unbreakable"]
|
||||
|===
|
||||
|Starter|Typical scope|Includes
|
||||
|
||||
|`spring-modulith-starter-core`
|
||||
|`compile`
|
||||
a|* `spring-modulith-api`
|
||||
* `spring-modulith-moments`
|
||||
* `spring-modulith-core` (runtime)
|
||||
* `spring-modulith-runtime` (runtime)
|
||||
|
||||
|`spring-modulith-starter-insight`
|
||||
|`runtime`
|
||||
a|* `spring-modulith-actuator` (runtime)
|
||||
* `spring-modulith-observability` (runtime)
|
||||
* `spring-boot-starter-actuator` (runtime)
|
||||
|
||||
|`spring-modulith-starter-jdbc`
|
||||
|`compile`
|
||||
a|* `spring-modulith-starter-core`
|
||||
* `spring-modulith-events-api`
|
||||
* `spring-modulith-events-core` (runtime)
|
||||
* `spring-modulith-events-jdbc` (runtime)
|
||||
* `spring-modulith-events-jackson` (runtime)
|
||||
|
||||
|`spring-modulith-starter-jpa`
|
||||
|`compile`
|
||||
a|* `spring-modulith-starter-core`
|
||||
* `spring-modulith-events-api`
|
||||
* `spring-modulith-events-core` (runtime)
|
||||
* `spring-modulith-events-jpa` (runtime)
|
||||
* `spring-modulith-events-jackson` (runtime)
|
||||
|
||||
|`spring-modulith-starter-mongodb`
|
||||
|`compile`
|
||||
a|* `spring-modulith-starter-core`
|
||||
* `spring-modulith-events-api`
|
||||
* `spring-modulith-events-core` (runtime)
|
||||
* `spring-modulith-events-mongodb` (runtime)
|
||||
* `spring-modulith-events-jackson` (runtime)
|
||||
|
||||
|`spring-modulith-starter-test`
|
||||
|`test`
|
||||
a|* `spring-modulith-docs`
|
||||
* `spring-modulith-test`
|
||||
|===
|
||||
|
||||
.Individual Spring Modulith JARs
|
||||
[cols="2,1,3", options="header, unbreakable"]
|
||||
|===
|
||||
|Module|Typical scope|Description
|
||||
|`spring-modulith-actuator`|`runtime`|A Spring Boot actuator to expose the application module structure via an actuator.
|
||||
|`spring-modulith-api`|`compile`|The abstractions to be used in your production code to customize Spring Modulith's default behavior.
|
||||
|`spring-modulith-core`|`runtime`|The core application module model and API.
|
||||
|`spring-modulith-docs`|`test`|The `Documenter` API to create Asciidoctor and PlantUML documentation from the module model.
|
||||
|`spring-modulith-events-amqp`|`runtime`|Event externalization support for AMQP.
|
||||
|`spring-modulith-events-api`|`runtime`|API to customize the event features of Spring Modulith.
|
||||
|`spring-modulith-events-core`|`runtime`|The core implementation of the event publication registry as well as the integration abstractions `EventPublicationRegistry` and `EventPublicationSerializer`.
|
||||
|`spring-modulith-events-jackson`|`runtime`|A Jackson-based implementation of the `EventPublicationSerializer`.
|
||||
|`spring-modulith-events-jdbc`|`runtime`|A JDBC-based implementation of the `EventPublicationRegistry`.
|
||||
|`spring-modulith-events-jms`|`runtime`|Event externalization support for JMS.
|
||||
|`spring-modulith-events-jpa`|`runtime`|A JPA-based implementation of the `EventPublicationRegistry`.
|
||||
|`spring-modulith-events-kafka`|`runtime`|Event externalization support for Kafka.
|
||||
|`spring-modulith-events-mongodb`|`runtime`|A MongoDB-based implementation of the `EventPublicationRegistry`.
|
||||
|`spring-modulith-moments`|`compile`|The Passage of Time events implementation described xref:moments.adoc[here].
|
||||
|`spring-modulith-runtime`|`runtime`|Support to bootstrap an `ApplicationModules` instance at runtime. Usually not directly depended on but transitively used by `spring-modulith-actuator` and `spring-modulith-observability`.
|
||||
|`spring-modulith-observability`|`runtime`|Observability infrastructure described <<observability, here>>.
|
||||
|===
|
||||
|
||||
[appendix]
|
||||
[[schemas]]
|
||||
== Event publication registry schemas
|
||||
|
||||
The JDBC-based event publication registry support expects the following database schemas to be present in the database.
|
||||
If you would like Spring Modulith to create the schema for you, set the application property `spring.modulith.events.jdbc-schema-initialization.enabled` to `true`.
|
||||
|
||||
[[schemas.h2]]
|
||||
=== H2
|
||||
|
||||
[source, sql]
|
||||
----
|
||||
include::{jdbc-schema-base}/schema-h2.sql[]
|
||||
----
|
||||
|
||||
[[schemas.hsqldb]]
|
||||
=== HSQLDB
|
||||
|
||||
[source, sql]
|
||||
----
|
||||
include::{jdbc-schema-base}/schema-hsqldb.sql[]
|
||||
----
|
||||
|
||||
[[schemas.mysql]]
|
||||
=== MySQL
|
||||
|
||||
[source, sql]
|
||||
----
|
||||
include::{jdbc-schema-base}/schema-mysql.sql[]
|
||||
----
|
||||
|
||||
[[schemas.postgresql]]
|
||||
=== PostgreSQL
|
||||
|
||||
[source, sql]
|
||||
----
|
||||
include::{jdbc-schema-base}/schema-postgresql.sql[]
|
||||
----
|
||||
|
||||
[appendix]
|
||||
[[migrating-from-moduliths]]
|
||||
== Migrating from Moduliths
|
||||
|
||||
|
||||
* `o.m.model.Modules` has been renamed to `o.s.m.model.ApplicationModules`
|
||||
* `o.m.model.ModuleDetectionStrategy` has been renamed to `o.s.m.model.ApplicationModuleDetectionStrategy`
|
||||
|
||||
* `@o.m.test.ModuleTest` has been renamed to `@o.s.m.test.ApplicationModuleTest`
|
||||
|
||||
* `o.m.docs.Documenter.Options` has been renamed to `o.s.m.docs.Documenter.DiagramOptions`
|
||||
* The diagram style of component diagrams now defaults to `DiagramStyle.C4` (override by calling `DiagramOptions.withStyle(DiagramStyle.UML)`)
|
||||
* The module canvas hides non exposed types by default.
|
||||
To include application-module-internal types in the canvas, configure `CanvasOptions` to `….revealInternals()`.
|
||||
* The output folder for component diagrams and application module canvases has moved from `moduliths-docs` to `spring-modulith-docs` located in your build's target folder (such as `target` for Maven).
|
||||
255
src/docs/antora/modules/ROOT/pages/documentation.adoc
Normal file
255
src/docs/antora/modules/ROOT/pages/documentation.adoc
Normal file
@@ -0,0 +1,255 @@
|
||||
[[documentation]]
|
||||
= Documenting Application Modules
|
||||
|
||||
The application module model created via `ApplicationModules` can be used to create documentation snippets for inclusion into developer documentation written in Asciidoc.
|
||||
Spring Modulith's `Documenter` abstraction can produce two different kinds of snippets:
|
||||
|
||||
* C4 and UML component diagrams describing the relationships between the individual application modules
|
||||
* A so-called __Application Module Canvas__, a tabular overview about the module and the most relevant elements in those (Spring beans, aggregate roots, events published and listened to as well as configuration properties).
|
||||
|
||||
[[component-diagrams]]
|
||||
== Generating Application Module Component diagrams
|
||||
|
||||
The documentation snippets can be generated by handing the `ApplicationModules` instance into a `Documenter`.
|
||||
|
||||
.Generating application module component diagrams using `Documenter`
|
||||
[source, java]
|
||||
----
|
||||
class DocumentationTests {
|
||||
|
||||
ApplicationModules modules = ApplicationModules.of(Application.class);
|
||||
|
||||
@Test
|
||||
void writeDocumentationSnippets() {
|
||||
|
||||
new Documenter(modules)
|
||||
.writeModulesAsPlantUml()
|
||||
.writeIndividualModulesAsPlantUml();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The first call on `Documenter` will generate a C4 component diagram containing all modules within the system.
|
||||
|
||||
.All modules and their relationships rendered as C4 component diagram
|
||||
[plantuml, c4-all-modules, svg]
|
||||
....
|
||||
top to bottom direction
|
||||
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
|
||||
|
||||
Container_Boundary("Modulith.Application_boundary", "Application") {
|
||||
Component(Modulith.Application.core, "core", "Module", "", $tags="")
|
||||
Component(Modulith.Application.catalog, "catalog", "Module", "", $tags="")
|
||||
Component(Modulith.Application.inventory, "inventory", "Module", "", $tags="")
|
||||
Component(Modulith.Application.order, "order", "Module", "", $tags="")
|
||||
Component(Modulith.Application.customer, "customer", "Module", "", $tags="")
|
||||
}
|
||||
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.core, "depends on", $tags="")
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.customer, "uses", $tags="")
|
||||
Rel_D(Modulith.Application.catalog, Modulith.Application.core, "depends on", $tags="")
|
||||
Rel_D(Modulith.Application.inventory, Modulith.Application.order, "listens to", $tags="")
|
||||
Rel_D(Modulith.Application.inventory, Modulith.Application.catalog, "uses", $tags="")
|
||||
Rel_D(Modulith.Application.inventory, Modulith.Application.order, "uses", $tags="")
|
||||
Rel_D(Modulith.Application.inventory, Modulith.Application.core, "uses", $tags="")
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.catalog, "depends on", $tags="")
|
||||
|
||||
SHOW_LEGEND()
|
||||
....
|
||||
|
||||
The second call will create additional diagrams that only include the individual module and the ones they directly depend on on the canvas.
|
||||
|
||||
.A subset of application modules and their relationships starting from the order module rendered as C4 component diagram
|
||||
[plantuml, c4-individual-modules, svg]
|
||||
....
|
||||
top to bottom direction
|
||||
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
|
||||
|
||||
Container_Boundary("Modulith.Application_boundary", "Application") {
|
||||
Component(Modulith.Application.core, "core", "Module", "", $tags="")
|
||||
Component(Modulith.Application.catalog, "catalog", "Module", "", $tags="")
|
||||
Component(Modulith.Application.order, "order", "Module", "", $tags="")
|
||||
Component(Modulith.Application.customer, "customer", "Module", "", $tags="")
|
||||
}
|
||||
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.core, "depends on", $tags="")
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.customer, "uses", $tags="")
|
||||
Rel_D(Modulith.Application.catalog, Modulith.Application.core, "depends on", $tags="")
|
||||
Rel_D(Modulith.Application.order, Modulith.Application.catalog, "depends on", $tags="")
|
||||
|
||||
SHOW_LEGEND()
|
||||
....
|
||||
|
||||
[[component-diagrams.uml]]
|
||||
=== Using Traditional UML Component Diagrams
|
||||
|
||||
If you prefer the traditional UML style component diagrams, tweak the `DiagramOptions` to rather use that style as follows:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
DiagramOptions.defaults()
|
||||
.withStyle(DiagramStyle.UML);
|
||||
----
|
||||
|
||||
This will cause the diagrams to look like this:
|
||||
|
||||
.All modules and their relationships rendered as UML component diagram
|
||||
[plantuml, uml-all-modules, svg]
|
||||
....
|
||||
skinparam {
|
||||
shadowing false
|
||||
arrowColor #707070
|
||||
actorBorderColor #707070
|
||||
componentBorderColor #707070
|
||||
rectangleBorderColor #707070
|
||||
noteBackgroundColor #ffffff
|
||||
noteBorderColor #707070
|
||||
defaultTextAlignment center
|
||||
wrapWidth 200
|
||||
maxMessageSize 100
|
||||
componentStyle uml1
|
||||
}
|
||||
package "Application" <<Container>> {
|
||||
component 4 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.catalog
|
||||
]
|
||||
component 3 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.core
|
||||
]
|
||||
component 7 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.customer
|
||||
]
|
||||
component 5 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.inventory
|
||||
]
|
||||
component 6 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.order
|
||||
]
|
||||
}
|
||||
4 .[#707070].> 3 : depends on
|
||||
5 .[#707070].> 4 : uses
|
||||
5 .[#707070].> 3 : uses
|
||||
5 .[#707070].> 6 : uses
|
||||
5 .[#707070].> 6 : listens to
|
||||
6 .[#707070].> 4 : depends on
|
||||
6 .[#707070].> 3 : depends on
|
||||
6 .[#707070].> 7 : uses
|
||||
....
|
||||
|
||||
.A subset of application modules and their relationships starting from the order module rendered as UML component diagram
|
||||
[plantuml,uml-individiual-module, svg]
|
||||
....
|
||||
skinparam {
|
||||
shadowing false
|
||||
arrowColor #707070
|
||||
actorBorderColor #707070
|
||||
componentBorderColor #707070
|
||||
rectangleBorderColor #707070
|
||||
noteBackgroundColor #ffffff
|
||||
noteBorderColor #707070
|
||||
defaultTextAlignment center
|
||||
wrapWidth 200
|
||||
maxMessageSize 100
|
||||
componentStyle uml1
|
||||
}
|
||||
package "Application" <<Container>> {
|
||||
component 4 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.catalog
|
||||
]
|
||||
component 3 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.core
|
||||
]
|
||||
component 7 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.customer
|
||||
]
|
||||
component 6 <<Component: Module>> #dddddd [
|
||||
com.acme.commerce.order
|
||||
]
|
||||
}
|
||||
4 .[#707070].> 3 : depends on
|
||||
6 .[#707070].> 4 : depends on
|
||||
6 .[#707070].> 3 : depends on
|
||||
6 .[#707070].> 7 : uses
|
||||
....
|
||||
|
||||
[[application-module-canvas]]
|
||||
== Generating Application Module Canvases
|
||||
|
||||
The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`:
|
||||
|
||||
.Generating application module canvases using `Documenter`
|
||||
[source, java]
|
||||
----
|
||||
class DocumentationTests {
|
||||
|
||||
ApplicationModules modules = ApplicationModules.of(Application.class);
|
||||
|
||||
@Test
|
||||
void writeDocumentationSnippets() {
|
||||
|
||||
new Documenter(modules)
|
||||
.writeModuleCanvases();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
By default, the documentation will be generated to `spring-modulith-docs` folder in your build system's build folder.
|
||||
A generated canvas looks like this:
|
||||
|
||||
.A sample Application Module Canvas
|
||||
[cols="1h,4a"]
|
||||
|===
|
||||
|Base package
|
||||
|`com.acme.commerce.inventory`
|
||||
|Spring components
|
||||
|_Services_
|
||||
|
||||
* `c.a.c.i.InventoryManagement`
|
||||
|
||||
_Repositories_
|
||||
|
||||
* `c.a.c.i.Inventory`
|
||||
|
||||
_Event listeners_
|
||||
|
||||
* `c.a.c.i.InternalInventoryListeners` listening to `o.s.m.m.DayHasPassed`, `c.a.c.i.QuantityReduced`
|
||||
* `c.a.c.i.InventoryOrderEventListener` listening to `c.a.c.o.OrderCanceled`, `c.a.c.o.OrderCompleted`
|
||||
|
||||
_Configuration properties_
|
||||
|
||||
* `c.a.c.i.InventoryProperties`
|
||||
|
||||
_Others_
|
||||
|
||||
* `c.a.c.i.InventoryItemCreationListener`
|
||||
|Aggregate roots
|
||||
|* `c.a.c.i.InventoryItem`
|
||||
|Published events
|
||||
|* `c.a.c.i.QuantityReduced` created by:
|
||||
** `c.a.c.i.InventoryItem.decreaseQuantity(…)`
|
||||
* `c.a.c.i.StockShort` created by:
|
||||
** `c.a.c.i.InternalInventoryListeners.on(…)`
|
||||
|
||||
|Events listened to
|
||||
|* `c.a.c.o.OrderCompleted`
|
||||
* `c.a.c.o.OrderCanceled`
|
||||
|Properties
|
||||
|* `acme.commerce.inventory.restock-threshold` -- `c.a.c.c.Quantity`. The threshold at which a `InventoryEvents.StockShort` is supposed to be triggered during inventory updates.
|
||||
|===
|
||||
|
||||
It consists of the following sections:
|
||||
|
||||
* __The application module's base package.__
|
||||
* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words, beans that are located in either the API package or any xref:fundamentals.adoc#modules.named-interfaces[named interface package].
|
||||
This will detect component stereotypes defined by https://github.com/xmolecules/jmolecules/tree/main/jmolecules-architecture[jMolecules architecture abstractions], but also standard Spring stereotype annotations.
|
||||
* __Exposed aggregate roots__ -- Any entities that we find repositories for or explicitly declared as aggregate via jMolecules.
|
||||
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
|
||||
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.
|
||||
* __Configuration properties__ -- Spring Boot Configuration properties exposed by the application module.
|
||||
Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties.
|
||||
362
src/docs/antora/modules/ROOT/pages/events.adoc
Normal file
362
src/docs/antora/modules/ROOT/pages/events.adoc
Normal file
@@ -0,0 +1,362 @@
|
||||
[[events]]
|
||||
:imagesdir: images
|
||||
[[working-with-application-events]]
|
||||
= Working with Application Events
|
||||
|
||||
To keep application modules as decoupled as possible from each other, their primary means of interaction should be event publication and consumption.
|
||||
This avoids the originating module to know about all potentially interested parties, which is a key aspect to enable application module integration testing (see xref:testing.adoc[Integration Testing Application Modules]).
|
||||
|
||||
Often we will find application components defined like this:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrderManagement {
|
||||
|
||||
private final InventoryManagement inventory;
|
||||
|
||||
@Transactional
|
||||
public void complete(Order order) {
|
||||
|
||||
// State transition on the order aggregate go here
|
||||
|
||||
// Invoke related functionality
|
||||
inventory.updateStockFor(order);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The `complete(…)` method creates functional gravity in the sense that it attracts related functionality and thus interaction with Spring beans defined in other application modules.
|
||||
This especially makes the component harder to test as we need to have instances available of those depended on beans just to create an instance of `OrderManagement` (see xref:testing.adoc#efferent-dependencies[Dealing with Efferent Dependencies]).
|
||||
It also means that we will have to touch the class whenever we would like to integrate further functionality with the business event order completion.
|
||||
|
||||
We can change the application module interaction as follows:
|
||||
|
||||
.Publishing an application event via Spring's `ApplicationEventPublisher`
|
||||
[source, java]
|
||||
----
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrderManagement {
|
||||
|
||||
private final ApplicationEventPublisher events;
|
||||
private final OrderInternal dependency;
|
||||
|
||||
@Transactional
|
||||
public void complete(Order order) {
|
||||
|
||||
// State transition on the order aggregate go here
|
||||
|
||||
events.publishEvent(new OrderCompleted(order.getId()));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note, how, instead of depending on the other application module's Spring bean, we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
|
||||
For a more aggregate-driven approach to event publication, see https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#core.domain-events[Spring Data's application event publication mechanism] for details.
|
||||
As event publication happens synchronously by default, the transactional semantics of the overall arrangement stay the same as in the example above.
|
||||
Both for the good, as we get to a very simple consistency model (either both the status change of the order _and_ the inventory update succeed or none of them does), but also for the bad as more triggered related functionality will widen the transaction boundary and potentially cause the entire transaction to fail, even if the functionality that is causing the error is not crucial.
|
||||
|
||||
A different way of approaching this is by moving the event consumption to asynchronous handling at transaction commit and treat secondary functionality exactly as that:
|
||||
|
||||
.An async, transactional event listener
|
||||
[source, java]
|
||||
----
|
||||
@Component
|
||||
class InventoryManagement {
|
||||
|
||||
@Async
|
||||
@TransactionalEventListener
|
||||
void on(OrderCompleted event) { /* … */ }
|
||||
}
|
||||
----
|
||||
|
||||
This now effectively decouples the original transaction from the execution of the listener.
|
||||
While this avoids the expansion of the original business transaction, it also creates a risk: if the listener fails for whatever reason, the event publication is lost, unless each listener actually implements its own safety net.
|
||||
Even worse, that doesn't even fully work, as the system might fail before the method is even invoked.
|
||||
|
||||
[[aml]]
|
||||
== Application Module Listener
|
||||
|
||||
To run a transactional event listener in a transaction itself, it would need to be annotated with `@Transactional` in turn.
|
||||
|
||||
.An async, transactional event listener running in a transaction itself
|
||||
[source, java]
|
||||
----
|
||||
@Component
|
||||
class InventoryManagement {
|
||||
|
||||
@Async
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
@TransactionalEventListener
|
||||
void on(OrderCompleted event) { /* … */ }
|
||||
}
|
||||
----
|
||||
|
||||
To ease the declaration of what is supposed to describe the default way of integrating modules via events, Spring Modulith provides `@ApplicationModuleListener` to shortcut the declaration
|
||||
|
||||
.An application module listener
|
||||
[source, java]
|
||||
----
|
||||
@Component
|
||||
class InventoryManagement {
|
||||
|
||||
@ApplicationModuleListener
|
||||
void on(OrderCompleted event) { /* … */ }
|
||||
}
|
||||
----
|
||||
|
||||
[[publication-registry]]
|
||||
== The Event Publication Registry
|
||||
|
||||
Spring Modulith ships with an event publication registry that hooks into the core event publication mechanism of Spring Framework.
|
||||
On event publication, it finds out about the transactional event listeners that will get the event delivered and writes entries for each of them (dark blue) into an event publication log as part of the original business transaction.
|
||||
|
||||
.The transactional event listener arrangement before execution
|
||||
image::event-publication-registry-start.png[]
|
||||
|
||||
Each transactional event listener is wrapped into an aspect that marks that log entry as completed if the execution of the listener succeeds.
|
||||
In case the listener fails, the log entry stays untouched so that retry mechanisms can be deployed depending on the application's needs.
|
||||
By default, all incomplete event publications are resubmitted at application startup.
|
||||
|
||||
.The transactional event listener arrangement after execution
|
||||
image::event-publication-registry-end.png[]
|
||||
|
||||
[[publication-registry.managing-publications]]
|
||||
=== Managing Event Publications
|
||||
|
||||
Event publications may need to be managed in a variety of ways during the runtime of an application.
|
||||
Incomplete publications might have to be re-submitted to the corresponding listeners after a given amount of time.
|
||||
Completed publications on the other hand, will likely have to be purged from the database or moved into an archive store.
|
||||
As the needs for that kind of housekeeping strongly vary from application to application, Spring Modulith offers API to deal with both kinds of publications.
|
||||
That API is available through the `spring-modulith-events-api` artifact, that you can add to your application:
|
||||
|
||||
.Using Spring Modulith Events API artifact
|
||||
[source, xml, subs="+attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events-api</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
This artifact contains two primary abstractions, that are available to application code as Spring Beans:
|
||||
|
||||
* `CompletedEventPublications` -- This interface allows accessing all completed event publications, and provides API to immediately purge all of them from the database or the completed publications older that a given duration (for example, 1 minute).
|
||||
* `IncompleteEventPublications`-- This interface allows accessing all incomplete event publications to resubmit either the ones matching a given predicate or older than a given `Duration` relative to the original publishing date.
|
||||
|
||||
[[publication-registry.publication-repositories]]
|
||||
=== Event Publication Repositories
|
||||
|
||||
To actually write the event publication log, Spring Modulith exposes an `EventPublicationRepository` SPI and implementations for popular persistence technologies that support transactions, like JPA, JDBC and MongoDB.
|
||||
You select the persistence technology to be used by adding the corresponding JAR to your Spring Modulith application.
|
||||
We have prepared dedicated xref:events.adoc#starters[starters] to ease that task.
|
||||
|
||||
The JDBC-based implementation can create a dedicated table for the event publication log when the respective configuration property (`spring.modulith.events.jdbc-schema-initialization.enabled`) is set to `true`.
|
||||
For details, please consult the xref:appendix.adoc#schemas[schema overview] in the appendix.
|
||||
|
||||
[[publication-registry.serialization]]
|
||||
=== Event Serializer
|
||||
|
||||
Each log entry contains the original event in serialized form.
|
||||
The `EventSerializer` abstraction contained in `spring-modulith-events-core` allows plugging different strategies for how to turn the event instances into a format suitable for the datastore.
|
||||
Spring Modulith provides a Jackson-based JSON implementation through the `spring-modulith-events-jackson` artifact, which registers a `JacksonEventSerializer` consuming an `ObjectMapper` through standard Spring Boot auto-configuration by default.
|
||||
|
||||
[[publication-registry.customize-publication-date]]
|
||||
=== Customizing the Event Publication Date
|
||||
By default, the Event Publication Registry will use the date returned by the `Clock.systemUTC()` as event publication date.
|
||||
If you want to customize this, register a bean of type clock with the application context:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Configuration
|
||||
class MyConfiguration {
|
||||
|
||||
@Bean
|
||||
Clock myCustomClock() {
|
||||
return … // Your custom Clock instance created here.
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[externalization]]
|
||||
== Externalizing Events
|
||||
|
||||
Some of the events exchanged between application modules might be interesting to external systems.
|
||||
Spring Modulith allows publishing selected events to a variety of message brokers.
|
||||
To use that support you need to take the following steps:
|
||||
|
||||
1. Add the <<events.externalization.infrastructure, broker-specific Spring Modulith artifact>> to your project.
|
||||
2. Select event types to be externalized by annotating them with either Spring Modulith's or jMolecules' `@Externalized` annotation.
|
||||
3. Specify the broker-specific routing target in the annotation's value.
|
||||
|
||||
To find out how to use other ways of selecting events for externalization, or customize their routing within the broker, check out <<events.externalization.fundamentals>>.
|
||||
|
||||
[[externalization.infrastructure]]
|
||||
=== Supported Infrastructure
|
||||
|
||||
[%header,cols="1,3,6"]
|
||||
|===
|
||||
|Broker|Artifact|Description
|
||||
|
||||
|Kafka
|
||||
|`spring-modulith-events-kafka`
|
||||
|Uses Spring Kafka for the interaction with the broker.
|
||||
The logical routing key will be used as
|
||||
|
||||
|AMQP
|
||||
|`spring-modulith-events-amqp`
|
||||
|Uses Spring AMQP for the interaction with any compatible broker.
|
||||
Requires an explicit dependency declaration for Spring Rabbit for example.
|
||||
The logical routing key will be used as AMQP routing key.
|
||||
|
||||
|JMS
|
||||
|`spring-modulith-events-jms`
|
||||
|Uses Spring's core JMS support.
|
||||
Does not support routing keys.
|
||||
|===
|
||||
|
||||
[[externalization.fundamentals]]
|
||||
=== Fundamentals of Event Externalization
|
||||
|
||||
The event externalization performs three steps on each application event published.
|
||||
|
||||
1. _Determining whether the event is supposed to be externalized_ -- We refer to this as "`event selection`".
|
||||
By default, only event types located within a Spring Boot auto-configuration package and annotated with one of the supported `@Externalized` annotations are selected for externalization.
|
||||
2. _Mapping the event (optional)_ -- By default, the event is serialized to JSON using the Jackson `ObjectMapper` present in the application and published as is.
|
||||
The mapping step allows developers to either customize the representation or even completely replace the original event with a representation suitable for external parties.
|
||||
Note, that the mapping step precedes the actual serialization of the to be published object.
|
||||
3. _Determining a routing target_ -- Message broker clients need a logical target to publish the message to.
|
||||
The target usually identifies physical infrastructure (a topic, exchange, or queue depending on the broker) and is often statically derived from the event type.
|
||||
Unless defined in the `@Externalized` annotation specifically, Spring Modulith uses the application-local type name as target.
|
||||
In other words, in a Spring Boot application with a base package of `com.acme.app`, an event type `com.acme.app.sample.SampleEvent` would get published to `sample.SampleEvent`.
|
||||
+
|
||||
Some brokers also allow to define a rather dynamic routing key, that is used for different purposes within the actual target.
|
||||
By default, no routing key is used.
|
||||
|
||||
[[externalization.annotations]]
|
||||
=== Annotation-based Event Externalization Configuration
|
||||
|
||||
To define a custom routing key via the `@Externalized` annotations, a pattern of `$target::$key` can be used for the target/value attribute available in each of the particular annotations.
|
||||
The key can be a SpEL expression which will get the event instance configured as root object.
|
||||
|
||||
.Defining a dynamic routing key via SpEL expression
|
||||
[source, java]
|
||||
----
|
||||
@Externalized("customer-created::#{#this.getLastname()}") // <2>
|
||||
class CustomerCreated {
|
||||
|
||||
String getLastname() { // <1>
|
||||
// …
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The `CustomerCreated` event exposes the lastname of the customer via an accessor method.
|
||||
That method is then used via the ``#this.getLastname()`` expression in key expression following the `::` delimiter of the target declaration.
|
||||
|
||||
If the key calculation becomes more involved, it is advisable to rather delegate that into a Spring bean that takes the event as argument:
|
||||
|
||||
.Invoking a Spring bean to calculate a routing key
|
||||
[source, java]
|
||||
----
|
||||
@Externalized("…::#{@beanName.someMethod(#this)}")
|
||||
----
|
||||
|
||||
[[externalization.api]]
|
||||
=== Programmatic Event Externalization Configuration
|
||||
|
||||
|
||||
The `spring-modulith-events-api` artifact contains `EventExternalizationConfiguration` that allows developers to customize all of the above mentioned steps.
|
||||
|
||||
.Programmatically configuring event externalization
|
||||
[source, java]
|
||||
----
|
||||
@Configuration
|
||||
class ExternalizationConfiguration {
|
||||
|
||||
@Bean
|
||||
EventExternalizationConfiguration eventExternalizationConfiguration() {
|
||||
|
||||
return EventExternalizationConfiguration.externalizing() // <1>
|
||||
.select(EventExternalizationConfiguration.annotatedAsExternalized()) // <2>
|
||||
.mapping(SomeEvent.class, it -> …) // <3>
|
||||
.routeKey(WithKeyProperty.class, WithKeyProperty::getKey) // <4>
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> We start by creating a default instance of `EventExternalizationConfiguration`.
|
||||
<2> We customize the event selection by calling one of the `select(…)` methods on the `Selector` instance returned by the previous call.
|
||||
This step fundamentally disables the application base package filter as we only look for the annotation now.
|
||||
Convenience methods to easily select events by type, by packages, packages and annotation exist.
|
||||
Also, a shortcut to define selection and routing in one step.
|
||||
<3> We define a mapping step for `SomeEvent` instances.
|
||||
Note, that the routing will still be determined by the original event instance, unless you additionally call `….routeMapped()` on the router.
|
||||
<4> We finally determine a routing key by defining a method handle to extract a value of the event instance.
|
||||
Alternatively, a full `RoutingKey` can be produced for individual events by using the general `route(…)` method on the `Router` instance returned from the previous call.
|
||||
|
||||
[[testing]]
|
||||
== Testing published events
|
||||
|
||||
NOTE: The following section describes a testing approach solely focused on tracking Spring application events.
|
||||
For a more holistic approach on testing modules that use <<events.aml, `@ApplicationModuleListener`>>, please check out the <<testing.scenarios, `Scenario` API>>.
|
||||
|
||||
Spring Modulith's `@ApplicationModuleTest` enables the ability to get a `PublishedEvents` instance injected into the test method to verify a particular set of events has been published during the course of the business operation under test.
|
||||
|
||||
.Event-based integration testing of the application module arrangement
|
||||
[source, java, subs="quotes"]
|
||||
----
|
||||
@ApplicationModuleTest
|
||||
class OrderIntegrationTests {
|
||||
|
||||
@Test
|
||||
void someTestMethod(**PublishedEvents events**) {
|
||||
|
||||
// …
|
||||
var matchingMapped = events.ofType(OrderCompleted.class)
|
||||
.matching(OrderCompleted::getOrderId, reference.getId());
|
||||
|
||||
assertThat(matchingMapped).hasSize(1);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note, how `PublishedEvents` exposes API to select events matching a certain criteria.
|
||||
The verification is concluded by an AssertJ assertion that verifies the number of elements expected.
|
||||
If you are using AssertJ for those assertions anyway, you can also use `AssertablePublishedEvents` as test method parameter type and use the fluent assertion APIs provided through that.
|
||||
|
||||
.Using `AssertablePublishedEvents` to verify event publications
|
||||
[source, java, subs="quotes"]
|
||||
----
|
||||
@ApplicationModuleTest
|
||||
class OrderIntegrationTests {
|
||||
|
||||
@Test
|
||||
void someTestMethod(**AssertablePublishedEvents events**) {
|
||||
|
||||
// …
|
||||
assertThat(events)
|
||||
.contains(OrderCompleted.class)
|
||||
.matching(OrderCompleted::getOrderId, reference.getId());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note, how the type returned by the `assertThat(…)` expression allows to define constraints on the published events directly.
|
||||
|
||||
[[starters]]
|
||||
== Spring Boot Event Registry Starters
|
||||
|
||||
Using the transactional event publication log requires a combination of artifacts added to your application.
|
||||
To ease that task, Spring Modulith provides starter POMs that are centered around the xref:events.adoc#publication-repositories[persistence technology] to be used and default to the Jackson-based `EventSerializer` implementation.
|
||||
The following starters are available:
|
||||
|
||||
* `spring-modulith-starter-jpa` -- Using JPA as persistence technology.
|
||||
* `spring-modulith-starter-jdbc` -- Using JDBC as persistence technology.
|
||||
Also works in JPA-based applications but bypasses your JPA provider for actual event persistence.
|
||||
* `spring-modulith-starter-mongodb` -- Using MongoDB behind Spring Data MongoDB.
|
||||
Also enables MongoDB transactions and requires a replica set setup of the server to interact with.
|
||||
The transaction auto-configuration can be disabled by setting the `spring.modulith.events.mongobd.transaction-management.enabled` property to `false`.
|
||||
207
src/docs/antora/modules/ROOT/pages/fundamentals.adoc
Normal file
207
src/docs/antora/modules/ROOT/pages/fundamentals.adoc
Normal file
@@ -0,0 +1,207 @@
|
||||
[[fundamentals]]
|
||||
= Fundamentals
|
||||
|
||||
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.
|
||||
|
||||
[[modules]]
|
||||
== Application modules
|
||||
|
||||
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 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.
|
||||
|
||||
[[modules.simple]]
|
||||
=== Simple Application Modules
|
||||
|
||||
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_.
|
||||
|
||||
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 for dependency injection into those.
|
||||
Thus, naturally, the module's API consists of all public types in the package.
|
||||
|
||||
Let us have a look at an example arrangement (icon:plus-circle[role=green] denotes a public type, icon:minus-circle[role=red] a package protected one).
|
||||
|
||||
.A single inventory application module
|
||||
[source, subs="+specialchars, macros"]
|
||||
----
|
||||
icon:cubes[] Example
|
||||
└─ icon:folder[] src/main/java
|
||||
├─ icon:cube[] example <1>
|
||||
| └─ icon:plus-circle[role=green] Application.java
|
||||
└─ icon:cube[] example.inventory <2>
|
||||
├─ icon:plus-circle[role=green] InventoryManagement.java
|
||||
└─ icon:minus-circle[role=red] SomethingInventoryInternal.java
|
||||
----
|
||||
<1> The application's main package `example`.
|
||||
<2> An application module package `inventory`.
|
||||
|
||||
[[modules.advanced]]
|
||||
=== 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
|
||||
[source, subs="macros, quotes"]
|
||||
----
|
||||
icon:cubes[] Example
|
||||
└─ icon:folder[] src/main/java
|
||||
├─ icon:cube[] example
|
||||
| └─ icon:plus-circle[role=green] Application.java
|
||||
├─ icon:cube[] example.inventory
|
||||
| ├─ icon:plus-circle[role=green] InventoryManagement.java
|
||||
| └─ icon:minus-circle[role=red] SomethingInventoryInternal.java
|
||||
├─ **icon:cube[] example.order**
|
||||
| └─ icon:plus-circle[role=green] OrderManagement.java
|
||||
└─ icon:cube[] example.order.internal
|
||||
└─ icon:plus-circle[role=green] SomethingOrderInternal.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 are considered _internal_ ones.
|
||||
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.
|
||||
|
||||
[[modules.explicit-dependencies]]
|
||||
=== Explicit Application Module Dependencies
|
||||
A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type.
|
||||
|
||||
.Inventory explicitly configuring module dependencies
|
||||
[source, java]
|
||||
----
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
allowedDependencies = "order"
|
||||
)
|
||||
package example.inventory;
|
||||
----
|
||||
|
||||
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 xref:verification.adoc[Verifying Application Module Structure].
|
||||
|
||||
[[modules.application-modules]]
|
||||
=== The `ApplicationModules` Type
|
||||
|
||||
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
|
||||
[source, java]
|
||||
----
|
||||
var modules = ApplicationModules.of(Application.class);
|
||||
----
|
||||
|
||||
To get an impression about 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 arranagement to the console
|
||||
[source, java]
|
||||
----
|
||||
modules.forEach(System.out::println);
|
||||
----
|
||||
|
||||
.The console output of our application module arrangement
|
||||
[source]
|
||||
----
|
||||
## 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
|
||||
----
|
||||
|
||||
Note, how each module is listed and the contained Spring components are identified and the respective visibility is rendered, too.
|
||||
|
||||
[[modules.named-interfaces]]
|
||||
=== Named Interfaces
|
||||
|
||||
By default and as described in xref:fundamentals.adoc#modules.advanced[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 package with `@NamedInterface`.
|
||||
|
||||
.A package arrangement to encapsulate an SPI named interface
|
||||
[source, text, subs="macros, quotes"]
|
||||
----
|
||||
icon:cubes[] Example
|
||||
└─ icon:folder[] src/main/java
|
||||
├─ icon:cube[] example
|
||||
| └─ icon:plus-circle[role=green] Application.java
|
||||
├─ …
|
||||
├─ icon:cube[] example.order
|
||||
| └─ icon:plus-circle[role=green] OrderManagement.java
|
||||
├─ **icon:cube[] example.order.spi**
|
||||
| ├— icon:coffee[] package-info.java
|
||||
| └─ icon:plus-circle[role=green] SomeSpiInterface.java
|
||||
└─ icon:cube[] example.order.internal
|
||||
└─ icon:plus-circle[role=green] SomethingOrderInternal.java
|
||||
----
|
||||
|
||||
.`package-info.java` in `example.order.spi`
|
||||
[source, java]
|
||||
----
|
||||
@org.springframework.modulith.NamedInterface("spi")
|
||||
package example.order.spi;
|
||||
----
|
||||
|
||||
The effect of that declaration is two fold: 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:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@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.
|
||||
|
||||
[[customizing-modules]]
|
||||
=== Customizing Module Detection
|
||||
|
||||
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.
|
||||
|
||||
Assume you declare a custom `ApplicationModuleDetectionStrategy` implementation like this:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
package example;
|
||||
|
||||
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
|
||||
|
||||
@Override
|
||||
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
// Your module detection goes here
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This class needs to be registered in `META-INF/spring.factories` as follows:
|
||||
|
||||
[source, text]
|
||||
----
|
||||
org.springframework.modulith.core.ApplicationModuleDetectionStrategy=\
|
||||
example.CustomApplicationModuleDetectionStrategy
|
||||
----
|
||||
52
src/docs/antora/modules/ROOT/pages/index.adoc
Normal file
52
src/docs/antora/modules/ROOT/pages/index.adoc
Normal file
@@ -0,0 +1,52 @@
|
||||
[[spring-modulith-reference-documentation]]
|
||||
= Spring Modulith
|
||||
Oliver Drotbohm
|
||||
:revnumber: {version}
|
||||
:revdate: {localdate}
|
||||
:icons: font
|
||||
|
||||
© 2022-2023 The original authors.
|
||||
|
||||
NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
|
||||
|
||||
|
||||
[[preface.project-metadata]]
|
||||
== Project Metadata
|
||||
|
||||
* Version control https://github.com/spring-projects/spring-modulith
|
||||
* Bug tracker: https://github.com/spring-projects/spring-modulith
|
||||
* Release repository: Maven central
|
||||
* Milestone repository: https://repo.spring.io/milestone
|
||||
* Snapshot repository: https://repo.spring.io/snapshot
|
||||
* Javadoc: https://docs.spring.io/spring-modulith/docs/{projectVersion}/api
|
||||
|
||||
[[using-spring-modulith]]
|
||||
== Using Spring Modulith
|
||||
|
||||
Spring Modulith consists of a set of libraries that can be used individually and depending on which features of it you would like to use.
|
||||
To ease the declaration of the individual modules, we recommend to declare the following BOM in your Maven POM:
|
||||
|
||||
.Using the Spring Modulith BOM
|
||||
[source, xml, subs="+attributes"]
|
||||
----
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-bom</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
----
|
||||
|
||||
The individual sections describing Spring Modulith features will refer to the individual artifacts that are needed to make use of the feature.
|
||||
For an overview about all modules available, have a look at xref:appendix.adoc#artifacts[Spring Modulith modules].
|
||||
|
||||
[[examples]]
|
||||
== Examples
|
||||
|
||||
If you would like to play with the features of the project and see them live in action, check out the examples https://github.com/spring-projects/spring-modulith/tree/{projectVersion}/spring-modulith-examples[here]
|
||||
|
||||
49
src/docs/antora/modules/ROOT/pages/moments.adoc
Normal file
49
src/docs/antora/modules/ROOT/pages/moments.adoc
Normal file
@@ -0,0 +1,49 @@
|
||||
[[moments]]
|
||||
= Moments -- a Passage of Time Events API
|
||||
|
||||
`spring-modulith-moments` is a Passage of Time Events implementation heavily inspired by Matthias Verraes https://verraes.net/2019/05/patterns-for-decoupling-distsys-passage-of-time-event/[blog post].
|
||||
It's an event-based approach to time to trigger actions that are tied to a particular period of time having passed.
|
||||
|
||||
To use the abstraction, include the following dependency in your project:
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-moments</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
The dependency added to the project's classpath causes the following things in your application:
|
||||
|
||||
* Application code can refer to `HourHasPassed`, `DayHasPassed`, `WeekHasPassed`, `MonthHasPassed`, `QuarterHasPassed`, `YearHasPassed` types in Spring event listeners to get notified if a certain amount of time has passed.
|
||||
* A bean of type `org.springframework.modulith.Moments` is available in the `ApplicationContext` that contains the logic to trigger these events.
|
||||
* If `spring.modulith.moments.enable-time-machine` is set to `true`, that instance will be a `org.springframework.modulith.TimeMachine` which allows to "shift" time and by that triggers all intermediate events, which is useful to integration test functionality that is triggered by the events.
|
||||
|
||||
By default, Moments uses a `Clock.systemUTC()` instance. To customize this, declare a bean of type `Clock`.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Configuration
|
||||
class MyConfiguration {
|
||||
|
||||
@Bean
|
||||
Clock myCustomClock() {
|
||||
// Create a custom Clock here
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Moments exposes the following application properties for advanced customization:
|
||||
|
||||
.Available application properties
|
||||
[%header, cols="3,1,2"]
|
||||
|===
|
||||
|Property|Default value|Description
|
||||
|`spring.modulith.moments.enable-time-machine`|false|If set to `true`, the `Moments` instance will be a `TimeMachine`, that exposes API to shift time forward. Useful for integration tests that expect functionality triggered by the Passage of Time Events.
|
||||
|`spring.modulith.moments.granularity`|hours|The minimum granularity of events to be fired. Alternative value `days` to avoid hourly events.
|
||||
|`spring.modulith.moments.locale`|`Locale.getDefault()`|The `Locale` to use when determining week boundaries.
|
||||
|`spring.modulith.moments.quarter-start-month`|`Months.JANUARY`|The month at which quarters start.
|
||||
|`spring.modulith.moments.zone-id`|`ZoneOffset#UTC`|The `ZoneId` to determine times which are attached to the events published.
|
||||
|===
|
||||
|
||||
142
src/docs/antora/modules/ROOT/pages/production-ready.adoc
Normal file
142
src/docs/antora/modules/ROOT/pages/production-ready.adoc
Normal file
@@ -0,0 +1,142 @@
|
||||
[[production-ready]]
|
||||
= Production-ready Features
|
||||
|
||||
Spring Modulith provides support to expose architectural information about your system as a Spring Boot actuator endpoint as well as observing the interaction between application modules by capturing metrics and traces.
|
||||
As a production-ready application is likely to require both, the most convenient way to activate those features is to use the Spring Modulith Insight starter as follows:
|
||||
|
||||
.Using the Spring Modulith Insight starter
|
||||
[source, xml, subs="+attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starter-insight</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
This will include the actuator and observability support as well as Spring Boot's actuator startup for general support for actuators.
|
||||
Note, that you will still have to add further dependencies to connect your application to your monitoring tools such as https://zipkin.io/[Zipkin], https://docs.wavefront.com/[Wavefront] etc. usually via https://opentelemetry.io/[OpenTelemetry] or https://github.com/openzipkin/brave[Brave].
|
||||
Find more information on that in https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing[the corresponding section] of Spring Boot's reference documentation.
|
||||
|
||||
[[actuator]]
|
||||
== [[observability.actuator]]Application Module Actuator
|
||||
|
||||
The application module structure can be exposed as Spring Boot actuator.
|
||||
To enable the actuator, add the `spring-modulith-actuator` dependency to the project:
|
||||
|
||||
.Using the Spring Modulith actuator support
|
||||
[source, xml, subs="+attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-actuator</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot actuator starter required to enable actuators in general -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<version>…</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Running the application will now expose an `modulith` actuator resource:
|
||||
|
||||
.Accessing the actuator HTTP resource
|
||||
[source, json]
|
||||
----
|
||||
GET http://localhost:8080/actuator
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "http://localhost:8080/actuator",
|
||||
"templated": false
|
||||
},
|
||||
"health-path": {
|
||||
"href": "http://localhost:8080/actuator/health/{*path}",
|
||||
"templated": true
|
||||
},
|
||||
"health": {
|
||||
"href": "http://localhost:8080/actuator/health",
|
||||
"templated": false
|
||||
},
|
||||
"modulith": { <1>
|
||||
"href": "http://localhost:8080/actuator/modulith",
|
||||
"templated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> The `modulith` actuator resource advertised.
|
||||
|
||||
The `modulith` resource adheres to the following structure:
|
||||
|
||||
.The JSON structure of the application modules actuator
|
||||
[%autowidth.stretch]
|
||||
|===
|
||||
|JSONPath|Description
|
||||
|
||||
|`+$.{moduleName}+`|The technical name of an application module. Target for module references in `dependencies.target`.
|
||||
|`+$.{moduleName}.displayName+`|The human-readable name of the application module.
|
||||
|`+$.{moduleName}.basePackage+`|The application module's base package.
|
||||
|`+$.{moduleName}.dependencies[]+`|All outgoing dependencies of the application module
|
||||
|`+$.{moduleName}.dependencies[].target+`|The name of the application module depended on. A reference to a `+{moduleName}+`.
|
||||
|`+$.{moduleName}.dependencies[].types[]+`|The types of dependencies towards the target module. Can either be `DEFAULT` (simple type dependency), `USES_COMPONENT` (Spring bean dependency) or `EVENT_LISTENER`.
|
||||
|===
|
||||
|
||||
An example module arrangement would look like this:
|
||||
|
||||
.An example response for the application modules actuator
|
||||
[source, json]
|
||||
----
|
||||
{
|
||||
"a": {
|
||||
"basePackage": "example.a",
|
||||
"displayName": "A",
|
||||
"dependencies": []
|
||||
},
|
||||
"b": {
|
||||
"basePackage": "example.b",
|
||||
"displayName": "B",
|
||||
"dependencies": [ {
|
||||
"target": "a",
|
||||
"types": [ "EVENT_LISTENER", "USES_COMPONENT" ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[observability]]
|
||||
== [[observability]]Observing Application Modules
|
||||
:imagesdir: images
|
||||
|
||||
The interaction between application modules can be intercepted to create Micrometer spans to ultimately end up in traces you can visualize in tools like https://zipkin.io/[Zipkin].
|
||||
To activate the instrumentation add the following runtime dependency to your project:
|
||||
|
||||
.Using the Spring Modulith observability support
|
||||
[source, xml, subs="+attributes"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-observability</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
NOTE: You will have to configure additional infrastructure dependencies depending on the tooling you want to pipe the observability metadata in.
|
||||
For details, please check the corresponding https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing[Spring Boot documentation] on which dependencies to include for your setup.
|
||||
|
||||
This will cause all Spring components that are part of the application module's API being decorated with an aspect that will intercept invocations and create Micrometer spans for them.
|
||||
A sample invocation trace can be seen below:
|
||||
|
||||
.A sample module invocation trace
|
||||
image::observability.png[]
|
||||
|
||||
In this particular case, triggering the payment changes the state of the order which then causes an order completion event being triggered.
|
||||
This gets picked up asynchronously by the engine that triggers another state change on the order, works for a couple of seconds and triggers the final state change on the order in turn.
|
||||
76
src/docs/antora/modules/ROOT/pages/runtime.adoc
Normal file
76
src/docs/antora/modules/ROOT/pages/runtime.adoc
Normal file
@@ -0,0 +1,76 @@
|
||||
[[runtime]]
|
||||
= Spring Modulith Runtime Support
|
||||
|
||||
The functionality described in previous chapters have all used the application module arrangement in either testing scenarios for verification and documentation purposes or were general support functionality that help to loosely couple modules but did not work with the application module structure directly.
|
||||
In this section we are going to describe Spring Modulith's support for
|
||||
|
||||
[[setup]]
|
||||
== Setting up Runtime Support for Application Modules
|
||||
|
||||
To enable the runtime support for Spring Modulith, make sure you include the `spring-modulith-runtime` JAR in your project.
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-runtime</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
IMPORTANT: It's worth noting that using the runtime support of Spring Modulith will mean that you include both https://www.archunit.org/[ArchUnit] and the https://jgrapht.org/[JGraphT] (required to topologically sort application modules) library in your application.
|
||||
|
||||
Adding this JAR will cause Spring Boot auto-configuration to run that registers the following components in your application:
|
||||
|
||||
* An `ApplicationModulesRuntime` that allows to access the `ApplicationModules`.
|
||||
* A `SpringBootApplicationRuntime` to back the former bean to detect the main application class.
|
||||
* An event listener for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.application-events-and-listeners[`ApplicationStartedEvent`]s that will invoke xref:runtime.adoc#application-module-initializer[`ApplicationModuleInitializer`] beans defined in the application context.
|
||||
|
||||
[[application-module-initializer]]
|
||||
== Application Module Initializers
|
||||
|
||||
When working with application modules, it is pretty common to need to execute some code specific to an individual module on application startup.
|
||||
This means that the execution order of that code needs to follow the dependency structure of the application modules.
|
||||
If a module B depends on module A, the initialization code of A has to run before the one for B, even if the initializers do not directly depend on another.
|
||||
|
||||
[plantuml, format='svg']
|
||||
....
|
||||
hide empty members
|
||||
|
||||
package org.springframework.modulith {
|
||||
interface ApplicationModuleInitializer
|
||||
}
|
||||
|
||||
package com.acme.moduleA {
|
||||
class InitializerA implements ApplicationModuleInitializer
|
||||
|
||||
class ComponentA
|
||||
}
|
||||
|
||||
package com.acme.moduleB {
|
||||
|
||||
class ComponentB
|
||||
|
||||
class InitializerB implements ApplicationModuleInitializer
|
||||
}
|
||||
|
||||
ComponentB --> ComponentA
|
||||
|
||||
....
|
||||
|
||||
While developers could of course define the execution order via Spring's standard `@Order` annotation or `Ordered` interface, Spring Modulith provides an `ApplicationModuleInitializer` interface for beans to be run on application startup.
|
||||
The execution order of those beans will automatically follow the application module dependency structure.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@Component
|
||||
class MyInitializer implements ApplicationModuleInitializer {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// Initialization code goes here
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note, that the `ApplicationModuleInitializer` beans will only be invoked if the `spring-modulith-runtime` JAR is on the classpath (see xref:runtime.adoc#setup[Setting up Runtime Support for Application Modules]) as that pulls in the dependencies that are needed to topologically sort the initializers according to the application module structure.
|
||||
217
src/docs/antora/modules/ROOT/pages/testing.adoc
Normal file
217
src/docs/antora/modules/ROOT/pages/testing.adoc
Normal file
@@ -0,0 +1,217 @@
|
||||
[[testing]]
|
||||
= Integration Testing Application Modules
|
||||
|
||||
Spring Modulith allows to run integration tests bootstrapping individual application modules in isolation or combination with others.
|
||||
To achieve this, place JUnit test class in an application module package or any sub-package of that and annotate it with `@ApplicationModuleTest`:
|
||||
|
||||
.A application module integration test class
|
||||
[source, java]
|
||||
----
|
||||
package example.order;
|
||||
|
||||
@ApplicationModuleTest
|
||||
class OrderIntegrationTests {
|
||||
|
||||
// Individual test cases go here
|
||||
}
|
||||
----
|
||||
|
||||
This will run your integration test similar to what `@SpringBootTest` would have achieved but with the bootstrap actually limited to the application module the test resides in.
|
||||
If you configure the log level for `org.springframework.modulith` to `DEBUG`, you will see detailed information about how the test execution customizes the Spring Boot bootstrap:
|
||||
|
||||
.The log output of a application module integration test bootstrap
|
||||
[source, text, subs="macros"]
|
||||
----
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
:: Spring Boot :: (v3.0.0-SNAPSHOT)
|
||||
|
||||
… - Bootstrapping @ApplicationModuleTest for example.order in mode STANDALONE (class example.Application)…
|
||||
… - ======================================================================================================
|
||||
… - ## example.order ##
|
||||
… - > Logical name: order
|
||||
… - > Base package: example.order
|
||||
… - > Direct module dependencies: none
|
||||
… - > Spring beans:
|
||||
… - + ….OrderManagement
|
||||
… - + ….internal.OrderInternal
|
||||
… - Starting OrderIntegrationTests using Java 17.0.3 …
|
||||
… - No active profile set, falling back to 1 default profile: "default"
|
||||
… - pass:quotes[**Re-configuring auto-configuration and entity scan packages to: example.order.**]
|
||||
----
|
||||
|
||||
Note, how the output contains the detailed information about the module included in the test run.
|
||||
It creates the application module module, finds the module to be run and limits the application of auto-configuration, component and entity scanning to the corresponding packages.
|
||||
|
||||
[[bootstrap-modes]]
|
||||
== Bootstrap Modes
|
||||
|
||||
The application module test can be bootstrapped in a variety of modes:
|
||||
|
||||
* `STANDALONE` (default) -- Runs the current module only.
|
||||
* `DIRECT_DEPENDENCIES` -- Runs the current module as well as all modules the current one directly depends on.
|
||||
* `ALL_DEPENDENCIES` -- Runs the current module and the entire tree of modules depended on.
|
||||
|
||||
[[efferent-dependencies]]
|
||||
== Dealing with Efferent Dependencies
|
||||
|
||||
When an application module is bootstrapped, the Spring beans it contains will be instantiated.
|
||||
If those contain bean references that cross module boundaries, the bootstrap will fail if those other modules are not included in the test run (see xref:testing.adoc#bootstrap-modes[Bootstrap Modes] for details).
|
||||
While a natural reaction might be to expand the scope of the application modules included, it is usually a better option to mock the target beans.
|
||||
|
||||
.Mocking Spring bean dependencies in other application modules
|
||||
[source, java]
|
||||
----
|
||||
@ApplicationModuleTest
|
||||
class InventoryIntegrationTests {
|
||||
|
||||
@MockBean SomeOtherComponent someOtherComponent;
|
||||
}
|
||||
----
|
||||
|
||||
Spring Boot will create bean definitions and instances for the types defined as `@MockBean` and add them to the `ApplicationContext` bootstrapped for the test run.
|
||||
|
||||
If you find your application module depending on too many beans of other ones, that is usually a sign of high coupling between them.
|
||||
The dependencies should be reviewed for whether they are candidates for replacement by publishing a domain event (see xref:events.adoc#events[null]).
|
||||
|
||||
[[scenarios]]
|
||||
== Defining Integration Test Scenarios
|
||||
|
||||
Integration testing application modules can become a quite elaborate effort.
|
||||
Especially if the integration of those is based on xref:events.adoc#aml[asynchronous, transactional event handling], dealing with the concurrent execution can be subject to subtle errors.
|
||||
Also, it requires dealing with quite a few infrastructure components: `TransactionOperations` and `ApplicationEventProcessor` to make sure events are published and delivered to transactional listeners, Awaitility to handle concurrency and AssertJ assertions to formulate expectations on the test execution's outcome.
|
||||
|
||||
To ease the definition of application module integration tests, Spring Modulith provides the `Scenario` abstraction that can be used by declaring it as test method parameter in tests declared as `@ApplicationModuleTest`.
|
||||
|
||||
.Using the `Scenario` API in a JUnit 5 test
|
||||
[source, java]
|
||||
----
|
||||
@ApplicationModuleTest
|
||||
class SomeApplicationModuleTest {
|
||||
|
||||
@Test
|
||||
public void someModuleIntegrationTest(Scenario scenario) {
|
||||
// Use the Scenario API to define your integration test
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The test definition itself usually follows the following skeleton:
|
||||
|
||||
1. A stimulus to the system is defined. This is usually either an event publication or an invocation of a Spring component exposed by the module.
|
||||
2. Optional customization of technical details of the execution (timeouts, etc.)
|
||||
3. The definition of some expected outcome, such as another application event being fired that matches some criteria or some state change of the module that can be detected by invoking exposed components.
|
||||
4. Optional, additional verifications made on the received event or observed, changed state.
|
||||
|
||||
`Scenario` exposes API to define these steps and guide you through the definition.
|
||||
|
||||
.Defining a stimulus as starting point of the `Scenario`
|
||||
[source, java]
|
||||
----
|
||||
// Start with an event publication
|
||||
scenario.publish(new MyApplicationEvent(…)).…
|
||||
|
||||
// Start with a bean invocation
|
||||
scenario.stimulate(() -> someBean.someMethod(…)).…
|
||||
----
|
||||
|
||||
Both the event publication and bean invocation will happen within a transaction callback to make sure the given event or any ones published during the bean invocation will be delivered to transactional event listeners.
|
||||
Note, that this will require a *new* transaction to be started, no matter whether the test case is already running inside a transaction or not.
|
||||
In other words, state changes of the database triggered by the stimulus will *never* be rolled back and have to be cleaned up manually.
|
||||
See the `….andCleanup(…)` methods for that purpose.
|
||||
|
||||
The resulting object can now get the execution customized though the generic `….customize(…)` method or specialized ones for common use cases like setting a timeout (`….waitAtMost(…)`).
|
||||
|
||||
The setup phase will be concluded by defining the actual expectation of the outcome of the stimulus.
|
||||
This can be an event of a particular type in turn, optionally further constraint by matchers:
|
||||
|
||||
.Expecting an event being published as operation result
|
||||
[source, java]
|
||||
----
|
||||
….andWaitForEventOfType(SomeOtherEvent.class)
|
||||
.matching(event -> …) // Use some predicate here
|
||||
.…
|
||||
----
|
||||
|
||||
These lines set up a completion criteria that the eventual execution will wait for to proceed.
|
||||
In other words, the example above will cause the execution to eventually block until either the default timeout is reached or a `SomeOtherEvent` is published that matches the predicate defined.
|
||||
|
||||
The terminal operations to execute the event-based `Scenario` are named `….toArrive…()` and allow to optionally access the expected event published, or the result object of the bean invocation defined in the original stimulus.
|
||||
|
||||
.Triggering the verification
|
||||
[source, java]
|
||||
----
|
||||
// Executes the scenario
|
||||
….toArrive(…)
|
||||
|
||||
// Execute and define assertions on the event received
|
||||
….toArriveAndVerify(event -> …)
|
||||
----
|
||||
|
||||
The choice of method names might look a bit weird when looking at the steps individually but they actually read quite fluent when combined.
|
||||
|
||||
.A complete `Scenario` definition
|
||||
[source, java]
|
||||
----
|
||||
scenario.publish(new MyApplicationEvent(…))
|
||||
.andWaitForEventOfType(SomeOtherEvent.class)
|
||||
.matching(event -> …)
|
||||
.toArriveAndVerify(event -> …);
|
||||
----
|
||||
|
||||
Alternatively to an event publication acting as expected completion signal, we can also inspect the state of the application module by invoking a method on one of the components exposed.
|
||||
The scenario would then rather look like this:
|
||||
|
||||
.Expecting a state change
|
||||
[source, java]
|
||||
----
|
||||
scenario.publish(new MyApplicationEvent(…))
|
||||
.andWaitForStateChange(() -> someBean.someMethod(…)))
|
||||
.andVerify(result -> …);
|
||||
----
|
||||
|
||||
The `result` handed into the `….andVerify(…)` method will be the value returned by the method invocation to detect the state change.
|
||||
By default, non-`null` values and non-empty ``Optional``s will be considered a conclusive state change.
|
||||
This can be tweaked by using the `….andWaitForStateChange(…, Predicate)` overload.
|
||||
|
||||
[[scenarios.customize]]
|
||||
=== Customizing Scenario Execution
|
||||
|
||||
To customize the execution of an individual scenario, call the `….customize(…)` method in the setup chain of the `Scenario`:
|
||||
|
||||
.Customizing a `Scenario` execution
|
||||
[source, java, subs="+quotes"]
|
||||
----
|
||||
scenario.publish(new MyApplicationEvent(…))
|
||||
**.customize(it -> it.atMost(Duration.ofSeconds(2)))**
|
||||
.andWaitForEventOfType(SomeOtherEvent.class)
|
||||
.matching(event -> …)
|
||||
.toArriveAndVerify(event -> …);
|
||||
----
|
||||
|
||||
To globally customize all `Scenario` instances of a test class, implement a `ScenarioCustomizer` and register it as JUnit extension.
|
||||
|
||||
.Registering a `ScenarioCustomizer`
|
||||
[source, java]
|
||||
----
|
||||
@ExtendWith(MyCustomizer.class)
|
||||
class MyTests {
|
||||
|
||||
@Test
|
||||
void myTestCase(Scenario scenario) {
|
||||
// scenario will be pre-customized with logic defined in MyCustomizer
|
||||
}
|
||||
|
||||
static class MyCustomizer implements ScenarioCustomizer {
|
||||
|
||||
@Override
|
||||
Function<ConditionFactory, ConditionFactory> getDefaultCustomizer(Method method, ApplicationContext context) {
|
||||
return it -> …;
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
21
src/docs/antora/modules/ROOT/pages/verification.adoc
Normal file
21
src/docs/antora/modules/ROOT/pages/verification.adoc
Normal file
@@ -0,0 +1,21 @@
|
||||
[[verification]]
|
||||
= Verifying Application Module Structure
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
We can verify whether our code arrangement adheres to the intended constraints by calling the `….verify()` method on our `ApplicationModules` instance:
|
||||
|
||||
[source, java]
|
||||
----
|
||||
ApplicationModules.of(Application.class).verify();
|
||||
----
|
||||
|
||||
The verification includes the following rules:
|
||||
|
||||
* _No cycles on the application module level_ -- the dependencies between modules have to form directed, acyclic graph.
|
||||
* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected.
|
||||
See xref:fundamentals.adoc#modules.advanced[Advanced Application Modules] for details.
|
||||
* _Explicitly allowed application module dependencies only_ (optional) -- An application module can optionally define allowed dependencies via `@ApplicationModule(allowedDependencies = …)`.
|
||||
If those are configured, dependencies to other application modules are rejected.
|
||||
See xref:fundamentals.adoc#modules.explicit-dependencies[Explicit Application Module Dependencies] and xref:fundamentals.adoc#modules.named-interfaces[Named Interfaces] for details.
|
||||
|
||||
Spring Modulith optionally integrates with the jMolecules ArchUnit library and, if present, automatically triggers its Domain-Driven Design verification rules described https://github.com/xmolecules/jmolecules-integrations/tree/main/jmolecules-archunit[here].
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../spring-modulith-events/spring-modulith-events-jdbc/src
|
||||
7
src/docs/antora/resources/antora-resources/antora.yml
Normal file
7
src/docs/antora/resources/antora-resources/antora.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: ${antora-component.version}
|
||||
prerelease: ${antora-component.prerelease}
|
||||
asciidoc:
|
||||
attributes:
|
||||
attribute-missing: 'warn'
|
||||
version: ${project.version}
|
||||
projectversion: ${project.version}
|
||||
Reference in New Issue
Block a user