From 9593031c67ad99e773aefa3a63f1996583cba74d Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 13 Oct 2022 17:17:46 +0200 Subject: [PATCH] GH-6 - More reference documentation. --- src/docs/asciidoc/10-fundamentals.adoc | 71 ++++++++++++++++++++- src/docs/asciidoc/20-verification.adoc | 7 ++- src/docs/asciidoc/30-testing.adoc | 11 +++- src/docs/asciidoc/40-events.adoc | 50 ++++++++++++++- src/docs/asciidoc/60-documentation.adoc | 84 +++++++++++++++++++++++-- src/docs/asciidoc/index.adoc | 2 +- 6 files changed, 211 insertions(+), 14 deletions(-) diff --git a/src/docs/asciidoc/10-fundamentals.adoc b/src/docs/asciidoc/10-fundamentals.adoc index cce2a4d8..4a8fcd69 100644 --- a/src/docs/asciidoc/10-fundamentals.adoc +++ b/src/docs/asciidoc/10-fundamentals.adoc @@ -30,6 +30,7 @@ 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[] denotes a public type, icon:minus-circle[] a package protected one). +.A single inventory application module [source, subs="macros"] ---- icon:cubes[] Example @@ -48,7 +49,8 @@ icon:cubes[] Example 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. -[source, subs="macros"] +.An inventory and order application module +[source, subs="macros, quotes"] ---- icon:cubes[] Example └─ icon:folder[] src/main/java @@ -57,7 +59,7 @@ icon:cubes[] Example ├─ icon:cube[] example.inventory | ├─ icon:plus-circle[] InventoryManagement.java | └─ icon:minus-circle[] SomethingInventoryInternal.java - ├─ icon:cube[] example.order + ├─ **icon:cube[] example.order** | └─ icon:plus-circle[] OrderManagement.java └─ icon:cube[] example.order.internal └─ icon:plus-circle[] SomethingOrderInternal.java @@ -70,12 +72,29 @@ 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. +[[fundamentals.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 <>. + [[fundamentals.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); @@ -83,11 +102,13 @@ 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 ## @@ -109,3 +130,49 @@ Note, how each module is listed and the contained Spring components are identifi [[fundamentals.modules.named-interface]] === Named Interfaces + +By default and as described in <>, 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[] Application.java + ├─ … + ├─ icon:cube[] example.order + | └─ icon:plus-circle[] OrderManagement.java + ├─ **icon:cube[] example.order.spi** + | ├— icon:coffee[] package-info.java + | └─ icon:plus-circle[] SomeSpiInterface.java + └─ icon:cube[] example.order.internal + └─ icon:plus-circle[] 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. + + diff --git a/src/docs/asciidoc/20-verification.adoc b/src/docs/asciidoc/20-verification.adoc index fd989d98..dcc62fa6 100644 --- a/src/docs/asciidoc/20-verification.adoc +++ b/src/docs/asciidoc/20-verification.adoc @@ -11,7 +11,10 @@ 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 <> 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. +* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected. +See <> 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 <> and <> for details. 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]. diff --git a/src/docs/asciidoc/30-testing.adoc b/src/docs/asciidoc/30-testing.adoc index 30c6625e..9fcb4834 100644 --- a/src/docs/asciidoc/30-testing.adoc +++ b/src/docs/asciidoc/30-testing.adoc @@ -4,8 +4,11 @@ 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 { @@ -13,9 +16,11 @@ class OrderIntegrationTests { } ---- +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. 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: -[source] +.The log output of a application module integration test bootstrap +[source, text, subs="macros"] ---- . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ @@ -36,10 +41,11 @@ If you configure the log level for `org.springframework.modulith` to `DEBUG`, yo … - + ….internal.OrderInternal … - Starting OrderIntegrationTests using Java 17.0.3 … … - No active profile set, falling back to 1 default profile: "default" -… - Re-configuring auto-configuration and entity scan packages to: example.order. +… - 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. [[testing.bootstrap-modes]] == Bootstrap Modes @@ -57,6 +63,7 @@ When an application module is bootstrapped, the Spring beans it contains will be 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 <> 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 diff --git a/src/docs/asciidoc/40-events.adoc b/src/docs/asciidoc/40-events.adoc index 2dd7ba4c..d13917f2 100644 --- a/src/docs/asciidoc/40-events.adoc +++ b/src/docs/asciidoc/40-events.adoc @@ -30,6 +30,9 @@ The `complete(…)` method creates functional gravity in the sense that it attra 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 <>). 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 `ApplicationEventListener` [source, java] ---- @Service @@ -49,12 +52,14 @@ public class OrderManagement { } ---- -Note, how we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate. +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 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. 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] ---- @Service @@ -117,3 +122,46 @@ The following starters are available: * `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. +[[events.integration-testing]] +== Integration Testing Application Modules Workting with Events + +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. + +.Traditional integration testing of the application module interaction +[source, java, subs="quotes"] +---- +@ApplicationModuleTest +class InventoryIntegrationTests { + + **@MockBean SomeOtherComponent someOtherComponent;** + + @Test + void someTestMethod() { + + // Given + // When + // Then + **verify(someOtherComponent).someMethodCall();** + } +} +---- + +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. +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 intergration testing of the application module arrangement +[source, java, subs="quotes"] +---- +@ApplicationModuleTest +class InventoryIntegrationTests { + + @Test + void someTestMethod(**PublishedEvents events**) { + + // Given + // When + // Then + **assertThat(events.…).…;** + } +} +---- diff --git a/src/docs/asciidoc/60-documentation.adoc b/src/docs/asciidoc/60-documentation.adoc index 9a7c74a1..39180bd6 100644 --- a/src/docs/asciidoc/60-documentation.adoc +++ b/src/docs/asciidoc/60-documentation.adoc @@ -1,9 +1,81 @@ [[documentation]] = Documenting Application Modules -[[documentation.subheadline]] -== Subheadline -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +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). + +[[documentation.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 containin all modules within the system. +The second call will create additional diagrams that only include the individual module and the ones they directly depend on on the canvas. + +[[documentation.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: + +TODO: Add diagram + +[[documentation.application-module-canvas]] +== Generating Application Module Canvases + +The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`: + +.Generating application module component diagrams using `Documenter` +[source, java] +---- +class DocumentationTests { + + ApplicationModules modules = ApplicationModules.of(Application.class); + + @Test + void writeDocumentationSnippets() { + + new Documenter(modules) + .writeModuleCanvases(); + } +} +---- + +A canvas generated looks like this: + +TODO: Include generated canvas + +It consists of the following sections: +* __The application module's name and 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 <>. +* __Exposed aggregate roots__ +* __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` or beans implementing `ApplicationListener`. +* __Configuration properties__ -- Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties. diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index aa692ee5..aeacb2a7 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,7 +11,7 @@ NOTE: Copies of this document may be made for your own use and for distribution :leveloffset: +1 -include::00-preface.adoc[] +// include::00-preface.adoc[] include::10-fundamentals.adoc[]