diff --git a/src/docs/antora/modules/ROOT/pages/documentation.adoc b/src/docs/antora/modules/ROOT/pages/documentation.adoc index 883ddbf1..4d1be19c 100644 --- a/src/docs/antora/modules/ROOT/pages/documentation.adoc +++ b/src/docs/antora/modules/ROOT/pages/documentation.adoc @@ -13,7 +13,11 @@ Spring Modulith's `Documenter` abstraction can produce two different kinds of sn The documentation snippets can be generated by handing the `ApplicationModules` instance into a `Documenter`. .Generating application module component diagrams using `Documenter` -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- class DocumentationTests { @@ -28,6 +32,22 @@ class DocumentationTests { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +class DocumentationTests { + private val modules = ApplicationModules.of(Application::class) + + @Test + fun writeDocumentationSnippets() { + Documenter(modules) + .writeModulesAsPlantUml() + .writeIndividualModulesAsPlantUml() + } +} +---- +====== The first call on `Documenter` will generate a C4 component diagram containing all modules within the system. @@ -90,13 +110,23 @@ SHOW_LEGEND() === 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- DiagramOptions.defaults() .withStyle(DiagramStyle.UML); ---- - +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +DiagramOptions.defaults() + .withStyle(DiagramStyle.UML) +---- +====== This will cause the diagrams to look like this: .All modules and their relationships rendered as UML component diagram @@ -184,7 +214,11 @@ package "Application" <> { The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`: .Generating application module canvases using `Documenter` -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- class DocumentationTests { @@ -198,7 +232,22 @@ class DocumentationTests { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +class DocumentationTests { + private val modules = ApplicationModules.of(Application::class) + + @Test + fun writeDocumentationSnippets() { + 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: diff --git a/src/docs/antora/modules/ROOT/pages/events.adoc b/src/docs/antora/modules/ROOT/pages/events.adoc index 505edb56..d4dfdf01 100644 --- a/src/docs/antora/modules/ROOT/pages/events.adoc +++ b/src/docs/antora/modules/ROOT/pages/events.adoc @@ -7,8 +7,11 @@ To keep application modules as decoupled as possible from each other, their prim 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Service @RequiredArgsConstructor @@ -26,6 +29,20 @@ public class OrderManagement { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Service +class OrderManagement(val inventory: InventoryManagement) { + + @Transactional + fun complete(order: Order) { + 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]). @@ -34,7 +51,11 @@ It also means that we will have to touch the class whenever we would like to int We can change the application module interaction as follows: .Publishing an application event via Spring's `ApplicationEventPublisher` -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Service @RequiredArgsConstructor @@ -53,6 +74,21 @@ public class OrderManagement { } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Service +class OrderManagement(val events: ApplicationEventPublisher, val dependency: OrderInternal) { + + @Transactional + fun complete(order: Order) { + events.publishEvent(OrderCompleted(order.id)) + } +} +---- +====== + 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. @@ -61,7 +97,11 @@ Both for the good, as we get to a very simple consistency model (either both the 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Component class InventoryManagement { @@ -71,7 +111,19 @@ class InventoryManagement { void on(OrderCompleted event) { /* … */ } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Component +class InventoryManagement { + @Async + @TransactionalEventListener + fun on(event: OrderCompleted) { /* … */ } +} +---- +====== 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. @@ -82,7 +134,11 @@ Even worse, that doesn't even fully work, as the system might fail before the me 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Component class InventoryManagement { @@ -93,11 +149,29 @@ class InventoryManagement { void on(OrderCompleted event) { /* … */ } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Component +class InventoryManagement { + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + fun on(event: OrderCompleted) { /* … */ } +} +---- +====== 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Component class InventoryManagement { @@ -106,6 +180,18 @@ class InventoryManagement { void on(OrderCompleted event) { /* … */ } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Component +class InventoryManagement { + + @ApplicationModuleListener + fun on(event: OrderCompleted) { /* … */ } +} +---- +====== [[publication-registry]] == The Event Publication Registry @@ -257,7 +343,11 @@ To define a custom routing key via the `@Externalized` annotations, a pattern of 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Externalized("customer-created::#{#this.getLastname()}") // <2> class CustomerCreated { @@ -267,6 +357,18 @@ class CustomerCreated { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Externalized("customer-created::#{#this.getLastname()}") // <2> +class CustomerCreated { + fun getLastname(): String { // <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. @@ -274,10 +376,21 @@ That method is then used via the ``#this.getLastname()`` expression in key e 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Externalized("…::#{@beanName.someMethod(#this)}") ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Externalized("…::#{@beanName.someMethod(#this)}") +---- +====== [[externalization.api]] === Programmatic Event Externalization Configuration @@ -286,7 +399,11 @@ If the key calculation becomes more involved, it is advisable to rather delegate 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Configuration class ExternalizationConfiguration { @@ -302,6 +419,26 @@ class ExternalizationConfiguration { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Configuration +class ExternalizationConfiguration { + + @Bean + fun eventExternalizationConfiguration(): EventExternalizationConfiguration { + + 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. @@ -321,7 +458,11 @@ For a more holistic approach on testing modules that use < { + // Your module detection goes here + } +} +---- +====== This class needs to be registered in `META-INF/spring.factories` as follows: diff --git a/src/docs/antora/modules/ROOT/pages/moments.adoc b/src/docs/antora/modules/ROOT/pages/moments.adoc index 3a02a9f3..0bed6843 100644 --- a/src/docs/antora/modules/ROOT/pages/moments.adoc +++ b/src/docs/antora/modules/ROOT/pages/moments.adoc @@ -36,7 +36,11 @@ The dependency added to the project's classpath causes the following things in y By default, Moments uses a `Clock.systemUTC()` instance. To customize this, declare a bean of type `Clock`. -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Configuration class MyConfiguration { @@ -47,6 +51,20 @@ class MyConfiguration { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Configuration +class MyConfiguration { + + @Bean + fun myCustomClock(): Clock { + // Create a custom Clock here + } +} +---- +====== Moments exposes the following application properties for advanced customization: diff --git a/src/docs/antora/modules/ROOT/pages/runtime.adoc b/src/docs/antora/modules/ROOT/pages/runtime.adoc index ed944d7a..51e8f0a6 100644 --- a/src/docs/antora/modules/ROOT/pages/runtime.adoc +++ b/src/docs/antora/modules/ROOT/pages/runtime.adoc @@ -74,7 +74,11 @@ 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @Component class MyInitializer implements ApplicationModuleInitializer { @@ -85,5 +89,18 @@ class MyInitializer implements ApplicationModuleInitializer { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@Component +class MyInitializer : ApplicationModuleInitializer { + + override fun 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. diff --git a/src/docs/antora/modules/ROOT/pages/testing.adoc b/src/docs/antora/modules/ROOT/pages/testing.adoc index 3faa3dd3..aa206958 100644 --- a/src/docs/antora/modules/ROOT/pages/testing.adoc +++ b/src/docs/antora/modules/ROOT/pages/testing.adoc @@ -5,7 +5,11 @@ Spring Modulith allows to run integration tests bootstrapping individual applica 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- package example.order; @@ -15,6 +19,19 @@ class OrderIntegrationTests { // Individual test cases go here } ---- +Kotlin:: ++ +[source, kortlin, role="secondary"] +---- +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: @@ -64,7 +81,11 @@ If those contain bean references that cross module boundaries, the bootstrap wil 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @ApplicationModuleTest class InventoryIntegrationTests { @@ -72,7 +93,17 @@ class InventoryIntegrationTests { @MockBean SomeOtherComponent someOtherComponent; } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@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. @@ -88,7 +119,11 @@ Also, it requires dealing with quite a few infrastructure components: `Transacti 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @ApplicationModuleTest class SomeApplicationModuleTest { @@ -99,6 +134,20 @@ class SomeApplicationModuleTest { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@ApplicationModuleTest +class SomeApplicationModuleTest { + + @Test + fun someModuleIntegrationTest(scenario: Scenario) { + // Use the Scenario API to define your integration test + } +} +---- +====== The test definition itself usually follows the following skeleton: @@ -110,7 +159,11 @@ The test definition itself usually follows the following skeleton: `Scenario` exposes API to define these steps and guide you through the definition. .Defining a stimulus as starting point of the `Scenario` -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- // Start with an event publication scenario.publish(new MyApplicationEvent(…)).… @@ -118,6 +171,17 @@ scenario.publish(new MyApplicationEvent(…)).… // Start with a bean invocation scenario.stimulate(() -> someBean.someMethod(…)).… ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +// Start with an event publication +scenario.publish(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. @@ -130,12 +194,25 @@ The setup phase will be concluded by defining the actual expectation of the outc 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- ….andWaitForEventOfType(SomeOtherEvent.class) .matching(event -> …) // Use some predicate here .… ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +….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. @@ -143,7 +220,11 @@ In other words, the example above will cause the execution to eventually block u 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- // Executes the scenario ….toArrive(…) @@ -151,28 +232,66 @@ The terminal operations to execute the event-based `Scenario` are named `….toA // Execute and define assertions on the event received ….toArriveAndVerify(event -> …) ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +// 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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- scenario.publish(new MyApplicationEvent(…)) .andWaitForEventOfType(SomeOtherEvent.class) .matching(event -> …) .toArriveAndVerify(event -> …); ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- scenario.publish(new MyApplicationEvent(…)) .andWaitForStateChange(() -> someBean.someMethod(…))) .andVerify(result -> …); ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +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. @@ -184,7 +303,11 @@ This can be tweaked by using the `….andWaitForStateChange(…, Predicate)` ove 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"] +[tabs] +====== +Java:: ++ +[source, java, subs="+quotes", role="primary"] ---- scenario.publish(new MyApplicationEvent(…)) **.customize(it -> it.atMost(Duration.ofSeconds(2)))** @@ -192,11 +315,26 @@ scenario.publish(new MyApplicationEvent(…)) .matching(event -> …) .toArriveAndVerify(event -> …); ---- +Kotlin:: ++ +[source, kotlin, subs="+quotes", role="secondary"] +---- +scenario.publish(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] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- @ExtendWith(MyCustomizer.class) class MyTests { @@ -215,3 +353,24 @@ class MyTests { } } ---- +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +@ExtendWith(MyCustomizer::class) +class MyTests { + + @Test + fun myTestCase(scenario : Scenario) { + // scenario will be pre-customized with logic defined in MyCustomizer + } + + class MyCustomizer : ScenarioCustomizer { + + override fun getDefaultCustomizer(method : Method, context : ApplicationContext) : Function { + return it -> … + } + } +} +---- +====== diff --git a/src/docs/antora/modules/ROOT/pages/verification.adoc b/src/docs/antora/modules/ROOT/pages/verification.adoc index 52bf0571..871f63ac 100644 --- a/src/docs/antora/modules/ROOT/pages/verification.adoc +++ b/src/docs/antora/modules/ROOT/pages/verification.adoc @@ -4,11 +4,21 @@ We can verify whether our code arrangement adheres to the intended constraints by calling the `….verify()` method on our `ApplicationModules` instance: -[source, java] +[tabs] +====== +Java:: ++ +[source, java, role="primary"] ---- ApplicationModules.of(Application.class).verify(); ---- - +Kotlin:: ++ +[source, kotlin, role="secondary"] +---- +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.