Skip to content

Commit 9593031

Browse files
committed
GH-6 - More reference documentation.
1 parent d43f75a commit 9593031

File tree

6 files changed

+211
-14
lines changed

6 files changed

+211
-14
lines changed

src/docs/asciidoc/10-fundamentals.adoc

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Thus, naturally, the module's API consists of all public types in the package.
3030

3131
Let us have a look at an example arrangement (icon:plus-circle[] denotes a public type, icon:minus-circle[] a package protected one).
3232

33+
.A single inventory application module
3334
[source, subs="macros"]
3435
----
3536
icon:cubes[] Example
@@ -48,7 +49,8 @@ icon:cubes[] Example
4849

4950
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.
5051

51-
[source, subs="macros"]
52+
.An inventory and order application module
53+
[source, subs="macros, quotes"]
5254
----
5355
icon:cubes[] Example
5456
└─ icon:folder[] src/main/java
@@ -57,7 +59,7 @@ icon:cubes[] Example
5759
├─ icon:cube[] example.inventory
5860
| ├─ icon:plus-circle[] InventoryManagement.java
5961
| └─ icon:minus-circle[] SomethingInventoryInternal.java
60-
├─ icon:cube[] example.order
62+
├─ **icon:cube[] example.order**
6163
| └─ icon:plus-circle[] OrderManagement.java
6264
└─ icon:cube[] example.order.internal
6365
└─ icon:plus-circle[] SomethingOrderInternal.java
@@ -70,24 +72,43 @@ Code within those must not be referred to from other modules.
7072
Note, how `SomethingOrderInternal` is a public type, likely because `OrderManagement` depends on it.
7173
This unfortunately means, that it can also be referred to from other packages such as the `inventory` one.
7274

75+
[[fundamentals.modules.explicit-dependencies]]
76+
=== Explicit Application Module Dependencies
77+
A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type.
78+
79+
.Inventory explicitly configuring module dependencies
80+
[source, java]
81+
----
82+
@org.springframework.modulith.ApplicationModule(
83+
allowedDependencies = "order"
84+
)
85+
package example.inventory;
86+
----
87+
88+
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).
89+
Find out about how to monitor that in <<verification>>.
90+
7391
[[fundamentals.modules.application-modules]]
7492
=== The `ApplicationModules` Type
7593

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

97+
.Creating an application module model
7998
[source, java]
8099
----
81100
var modules = ApplicationModules.of(Application.class);
82101
----
83102

84103
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:
85104

105+
.Writing the application module arranagement to the console
86106
[source, java]
87107
----
88108
modules.forEach(System.out::println);
89109
----
90110

111+
.The console output of our application module arrangement
91112
[source]
92113
----
93114
## example.inventory ##
@@ -109,3 +130,49 @@ Note, how each module is listed and the contained Spring components are identifi
109130

110131
[[fundamentals.modules.named-interface]]
111132
=== Named Interfaces
133+
134+
By default and as described in <<fundamentals.modules.advanced>>, an application module's base package is considered the API package and thus is the only package to allow incoming dependencies from other modules.
135+
In case you would like to expose additional packages to other modules, you need to use __named interfaces__.
136+
You achieve that by annotating the `package-info.java` file of those package with `@NamedInterface`.
137+
138+
.A package arrangement to encapsulate an SPI named interface
139+
[source, text, subs="macros, quotes"]
140+
----
141+
icon:cubes[] Example
142+
└─ icon:folder[] src/main/java
143+
├─ icon:cube[] example
144+
| └─ icon:plus-circle[] Application.java
145+
├─ …
146+
├─ icon:cube[] example.order
147+
| └─ icon:plus-circle[] OrderManagement.java
148+
├─ **icon:cube[] example.order.spi**
149+
| ├— icon:coffee[] package-info.java
150+
| └─ icon:plus-circle[] SomeSpiInterface.java
151+
└─ icon:cube[] example.order.internal
152+
└─ icon:plus-circle[] SomethingOrderInternal.java
153+
----
154+
155+
.`package-info.java` in `example.order.spi`
156+
[source, java]
157+
----
158+
@org.springframework.modulith.NamedInterface("spi")
159+
package example.order.spi;
160+
----
161+
162+
The effect of that declaration is two fold: first, code in other application modules is allowed to refer to `SomeSpiInterface`.
163+
Application modules are able to refer to the named interface in explicit dependency declarations.
164+
Assume the __inventory__ module was making use of that, it could refer to the above declared named interface like this:
165+
166+
[source, java]
167+
----
168+
@org.springframework.modulith.ApplicationModule(
169+
allowedDependencies = "order::spi"
170+
)
171+
package example.inventory;
172+
----
173+
174+
Note how we concatenate the named interface's name `spi` via the double colon `::`.
175+
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.
176+
For modules without explicitly described dependencies, both the application module root package *and* the SPI one are accessible.
177+
178+

src/docs/asciidoc/20-verification.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ ApplicationModules.of(Application.class).verify();
1111
The verification includes the following rules:
1212

1313
* _No cycles on the application module level_ -- the dependencies between modules have to form directed, acyclic graph.
14-
* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected. See <<fundamentals.modules.advanced>> for details.
15-
* _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.
14+
* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected.
15+
See <<fundamentals.modules.advanced>> for details.
16+
* _Explicitly allowed application module dependencies only_ (optional) -- An application module can optionally define allowed dependencies via `@ApplicationModule(allowedDependencies = …)`.
17+
If those are configured, dependencies to other application modules are rejected.
18+
See <<fundamentals.modules.explicit-dependencies>> and <<fundamentals.modules.named-interfaces>> for details.
1619

1720
Spring Modulith optionally integrates with the jMolecules ArchUnit library and, if present, automatically triggers its verification rules described https://github.com/xmolecules/jmolecules-integrations/tree/main/jmolecules-archunit[here].

src/docs/asciidoc/30-testing.adoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44
Spring Modulith allows to run integration tests bootstrapping individual application modules in isolation or combination with others.
55
To achieve this, place JUnit test class in an application module package or any sub-package of that and annotate it with `@ApplicationModuleTest`:
66

7+
.A application module integration test class
78
[source, java]
89
----
10+
package example.order;
11+
912
@ApplicationModuleTest
1013
class OrderIntegrationTests {
1114
1215
// Individual test cases go here
1316
}
1417
----
1518

19+
This will run you integration test similar to what `@SpringBootTest` would have achieved but with the bootstrap actually limited to the application module the test resides in.
1620
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:
1721

18-
[source]
22+
.The log output of a application module integration test bootstrap
23+
[source, text, subs="macros"]
1924
----
2025
. ____ _ __ _ _
2126
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
@@ -36,10 +41,11 @@ If you configure the log level for `org.springframework.modulith` to `DEBUG`, yo
3641
… - + ….internal.OrderInternal
3742
… - Starting OrderIntegrationTests using Java 17.0.3 …
3843
… - No active profile set, falling back to 1 default profile: "default"
39-
… - Re-configuring auto-configuration and entity scan packages to: example.order.
44+
… - pass:quotes[**Re-configuring auto-configuration and entity scan packages to: example.order.**]
4045
----
4146

4247
Note, how the output contains the detailed information about the module included in the test run.
48+
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.
4349

4450
[[testing.bootstrap-modes]]
4551
== Bootstrap Modes
@@ -57,6 +63,7 @@ When an application module is bootstrapped, the Spring beans it contains will be
5763
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 <<testing.bootstrap-modes>> for details).
5864
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.
5965

66+
.Mocking Spring bean dependencies in other application modules
6067
[source, java]
6168
----
6269
@ApplicationModuleTest

src/docs/asciidoc/40-events.adoc

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ The `complete(…)` method creates functional gravity in the sense that it attra
3030
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 <<testing.efferent-dependencies>>).
3131
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.
3232

33+
We can change the application module interaction as follows:
34+
35+
.Publishing an application event via Spring's `ApplicationEventListener`
3336
[source, java]
3437
----
3538
@Service
@@ -49,12 +52,14 @@ public class OrderManagement {
4952
}
5053
----
5154

52-
Note, how we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
55+
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.
56+
For a more aggregate-driven approach to even 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.
5357
As event publication happens synchronously by default, the transactional semantics of the overall arrangement stay the same as in the example above.
5458
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.
5559

5660
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:
5761

62+
.An async, transactional event listener
5863
[source, java]
5964
----
6065
@Service
@@ -117,3 +122,46 @@ The following starters are available:
117122
* `spring-modulith-starter-jdbc` -- Using JDBC as persistence technology. Also works in JPA-based applications but bypasses your JPA provider for actual event persistence.
118123
* `spring-modulith-starter-mongodb` -- Using MongoDB behind Spring Data MongoDB.
119124

125+
[[events.integration-testing]]
126+
== Integration Testing Application Modules Workting with Events
127+
128+
Integration tests for application modules that interact with other modules' Spring beans usually have those mocked and the test cases verify the interaction by verifying that that mock bean was invoked in a particular way.
129+
130+
.Traditional integration testing of the application module interaction
131+
[source, java, subs="quotes"]
132+
----
133+
@ApplicationModuleTest
134+
class InventoryIntegrationTests {
135+
136+
**@MockBean SomeOtherComponent someOtherComponent;**
137+
138+
@Test
139+
void someTestMethod() {
140+
141+
// Given
142+
// When
143+
// Then
144+
**verify(someOtherComponent).someMethodCall();**
145+
}
146+
}
147+
----
148+
149+
In an event-based application interaction model, the dependency to the other application module's Spring bean is gone and we have nothing to verify.
150+
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.
151+
152+
.Event-based intergration testing of the application module arrangement
153+
[source, java, subs="quotes"]
154+
----
155+
@ApplicationModuleTest
156+
class InventoryIntegrationTests {
157+
158+
@Test
159+
void someTestMethod(**PublishedEvents events**) {
160+
161+
// Given
162+
// When
163+
// Then
164+
**assertThat(events.…).…;**
165+
}
166+
}
167+
----
Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,81 @@
11
[[documentation]]
22
= Documenting Application Modules
33

4-
[[documentation.subheadline]]
5-
== Subheadline
6-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
7-
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
8-
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
9-
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
4+
The application module model created via `ApplicationModules` can be used to create documentation snippets for inclusion into developer documentation written in Asciidoc.
5+
Spring Modulith's `Documenter` abstraction can produce two different kinds of snippets:
6+
7+
* C4 and UML component diagrams describing the relationships between the individual application modules
8+
* 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).
9+
10+
[[documentation.component-diagrams]]
11+
== Generating Application Module Component diagrams
12+
13+
The documentation snippets can be generated by handing the `ApplicationModules` instance into a `Documenter`.
14+
15+
.Generating application module component diagrams using `Documenter`
16+
[source, java]
17+
----
18+
class DocumentationTests {
19+
20+
ApplicationModules modules = ApplicationModules.of(Application.class);
21+
22+
@Test
23+
void writeDocumentationSnippets() {
24+
25+
new Documenter(modules)
26+
.writeModulesAsPlantUml()
27+
.writeIndividualModulesAsPlantUml();
28+
}
29+
}
30+
----
31+
32+
The first call on `Documenter` will generate a C4 component diagram containin all modules within the system.
33+
The second call will create additional diagrams that only include the individual module and the ones they directly depend on on the canvas.
34+
35+
[[documentation.component-diagrams.uml]]
36+
=== Using Traditional UML Component Diagrams
37+
38+
If you prefer the traditional UML style component diagrams, tweak the `DiagramOptions` to rather use that style as follows:
39+
40+
[source, java]
41+
----
42+
DiagramOptions.defaults()
43+
.withStyle(DiagramStyle.UML);
44+
----
45+
46+
This will cause the diagrams to look like this:
47+
48+
TODO: Add diagram
49+
50+
[[documentation.application-module-canvas]]
51+
== Generating Application Module Canvases
52+
53+
The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`:
54+
55+
.Generating application module component diagrams using `Documenter`
56+
[source, java]
57+
----
58+
class DocumentationTests {
59+
60+
ApplicationModules modules = ApplicationModules.of(Application.class);
61+
62+
@Test
63+
void writeDocumentationSnippets() {
64+
65+
new Documenter(modules)
66+
.writeModuleCanvases();
67+
}
68+
}
69+
----
70+
71+
A canvas generated looks like this:
72+
73+
TODO: Include generated canvas
74+
75+
It consists of the following sections:
76+
* __The application module's name and base package.__
77+
* __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 <<fundamentals.modules.named-interface, named interface package>>.
78+
* __Exposed aggregate roots__
79+
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
80+
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener` or beans implementing `ApplicationListener`.
81+
* __Configuration properties__ -- Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties.

src/docs/asciidoc/index.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ NOTE: Copies of this document may be made for your own use and for distribution
1111

1212
:leveloffset: +1
1313

14-
include::00-preface.adoc[]
14+
// include::00-preface.adoc[]
1515

1616
include::10-fundamentals.adoc[]
1717

0 commit comments

Comments
 (0)