Using module components
This section goes a bit more in depth on sharing components between modules, it explains:
Component ordering and how it is impacted by module dependencies
Optional module dependencies
How you can refresh a collection of components without module dependencies
This how-to is part of the Building a modular monolith series. It continues directly from the previous how-to on Module dependencies.
Component ordering
We created ExposedComponentOne
as an implementation of Supplier<String>
.
Let’s create another new module which has a component that retrieves all Supplier<String>
implementations and returns their class names.
Start by adding the module descriptor for ModuleThree
which declares a dependency on ModuleOne
:
package com.example.demo.modules.three;
@AcrossDepends(required="ModuleOne")
public class ModuleThree extends AcrossModule
{
@Override
public String getName() {
return "ModuleThree";
}
@Override
protected void registerDefaultApplicationContextConfigurers( Set<ApplicationContextConfigurer> contextConfigurers ) {
contextConfigurers.add( ComponentScanConfigurer.forAcrossModule( ModuleThree.class ) );
}
}
Add a SupplierService
component which wires all Supplier<String>
beans.
package com.example.demo.modules.three;
@Service
@RequiredArgsConstructor
public class SupplierService
{
private Collection<Supplier<String>> suppliers = Collections.emptyList();
@Autowired
public void setSuppliers( Collection<Supplier<String>> suppliers ) {
this.suppliers = suppliers;
}
public Collection<String> getSupplierNames() {
return suppliers.stream()
.map( Object::getClass )
.map( Class::getSimpleName )
.collect( Collectors.toList() );
}
}
Because we will change it later on, we deliberately use setter injection with @Autowired
in this class.
We want to have the SupplierService
exposed for other modules, but instead of a regular @Component @Exposed
we use the @Service
annotation.
Beans annotated with @Service
are exposed by default.
Your project structure should now look like:
com.example.demo/
modules/
one/
ModuleOne
InternalComponentOne
ExposedComponentOne
two/
ModuleTwo
InternalComponentTwo
three/
ModuleThree.java
SupplierService.java
DemoApplication
Add an integration test to check that our SupplierService
finds the ExposedComponentOne
implementation.
@Test
public void supplierServiceFromModuleThreeListsDetectedSuppliers() {
try (AcrossTestContext ctx = AcrossTestBuilders.standard(false)
.modules(new ModuleThree(), new ModuleOne())
.build()) {
SupplierService supplierService = ctx.getBeanOfType(SupplierService.class);
assertEquals(Collections.singletonList("ExposedComponentOne"), supplierService.getSupplierNames());
}
}
This test should succeed:
-
because of the module dependencies,
ModuleThree
starts afterModuleOne
and can access the exposedExposedComponentOne
-
SupplierService
is created with the list ofSupplier<String>
beans it can find, which currently is onlyExposedComponentOne
-
SupplierService
is itself is exposed and can be accessed from the unit test
Adding an exposed component
Let’s also add an exposed component implementing Supplier<String>
to ModuleTwo
:
package com.example.demo.modules.two;
@Component
@Exposed
public class ExposedComponentTwo implements Supplier<String>
{
@Override
public String get() {
return "hello from module two";
}
}
If we want to ensure that our SupplierService
can also detect this component, we now also need to make sure that ModuleThree
starts after ModuleTwo
.
We could add another required dependency, but that would mean that ModuleThree
will only start if both ModuleOne
and ModuleTwo
are present.
But what if we simply want ModuleThree
to list the available suppliers, without enforcing any other modules to be present?
Optional module dependencies
Suppose we change our integration test for the the SupplierService
to the following:
@Test
public void supplierServiceFromModuleThreeListsDetectedSuppliersInOrder() {
expectSuppliers( Collections.emptyList() );
expectSuppliers( Collections.singletonList( "ExposedComponentOne" ), new ModuleOne() );
expectSuppliers( Arrays.asList( "ExposedComponentOne", "ExposedComponentTwo" ), new ModuleOne(), new ModuleTwo() );
}
private void expectSuppliers( Collection<String> names, AcrossModule... additionalModules ) {
try (AcrossTestContext ctx = AcrossTestBuilders.standard( false )
.modules( new ModuleThree() )
.modules( additionalModules )
.build()) {
SupplierService supplierService = ctx.getBeanOfType( SupplierService.class );
assertEquals( names, supplierService.getSupplierNames() );
}
}
This test bootstraps different module combinations, and tests that the SupplierService
always detects the correct set of suppliers.
One way we can make this test succeed is to put optional dependencies on ModuleThree
:
@AcrossDepends(optional={"ModuleOne", "ModuleTwo"})
public class ModuleThree extends AcrossModule
{
...
}
And to make the Collection<Supplier<String>>
dependency optional as well:
public class SupplierService
{
...
@Autowired(required=false)
public void setSuppliers(Collection<Supplier<String>> suppliers) {
this.suppliers = suppliers;
}
....
}
The difference between a required and an optional module dependency is as follows:
-
If a required dependency is missing, the bootstrap will fail. If an optional dependency is missing, bootstrap will continue as normal.
-
Cyclic required dependencies are not allowed, and a required dependency is guaranteed to have started before the module depending on it. Cyclic optional dependencies are not advised but possible: a best-effort attempt will be made to start an optional dependency before the module depending on it.
Even though the test is now successful, this is not an optimal approach: whenever we add another module we would have to update the ModuleThree
dependencies to ensure it can detect the Supplier
.
A different way to tackle this type of problem is to use a refreshable collection.
Using a refreshable collection
A refreshable collection is a collection type dependency that will update itself once all modules in an application have been started.
Remove @AcrossDepends
from the ModuleThree
class, and replace the @Autowired(required=false)
from the SupplierService
by @RefreshableCollection
:
public class SupplierService
{
...
@RefreshableCollection
public void setSuppliers(Collection<Supplier<String>> suppliers) {
this.suppliers = suppliers;
}
....
}
When you run the tests you will see they all succeed.
Even though ModuleThree
no longer has any module dependencies and might even bootstrap before ModuleOne
and ModuleTwo
, the collection of suppliers is always up-to-date once the entire application has started.
Another very important fact is that result of SupplierService.getSupplierNames()
is deterministic.
No matter how many times you re-run the test, it will always succeed, meaning that the beans are always returned in exactly the same order.
When you get a collection of beans from different modules, they will be implicitly ordered in the bootstrap order of the modules that defined them.
We find the same reliable ordering principle in event handling as well, let’s look at an example with events.