GH-613 - Add SPI to support external ApplicationModuleSource contributions.

We now expose ApplicationModuleSourceFactory as Spring Factories-based SPI interface to further contribute ApplicationModuleSource instances either from a provided root package subject for module detection through a (potentially customized) ApplicationModuleDetectionStrategy or by explicitly listing particular module base packages.
This commit is contained in:
Oliver Drotbohm
2024-09-04 08:51:19 +02:00
parent b370d5a3f2
commit 1062f53bfa
19 changed files with 697 additions and 78 deletions

View File

@@ -102,7 +102,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
Assert.notNull(source, "Base package must not be null!");
Assert.notNull(exclusions, "Exclusions must not be null!");
JavaPackage basePackage = source.moduleBasePackage();
JavaPackage basePackage = source.getModuleBasePackage();
this.source = source;
this.basePackage = basePackage;
@@ -144,7 +144,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
* @return will never be {@literal null} or empty.
*/
public String getName() {
return source.moduleName();
return source.getModuleName();
}
/**

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.core;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.util.Assert;
@@ -29,9 +30,25 @@ import org.springframework.util.Assert;
* @author Oliver Drotbohm
* @since 1.3
*/
record ApplicationModuleSource(
JavaPackage moduleBasePackage,
String moduleName) {
public class ApplicationModuleSource {
private final JavaPackage moduleBasePackage;
private final String moduleName;
/**
* Creates a new {@link ApplicationModuleSource} for the given module base package and module name.
*
* @param moduleBasePackage must not be {@literal null}.
* @param moduleName must not be {@literal null} or empty.
*/
private ApplicationModuleSource(JavaPackage moduleBasePackage, String moduleName) {
Assert.notNull(moduleBasePackage, "JavaPackage must not be null!");
Assert.hasText(moduleName, "Module name must not be null or empty!");
this.moduleBasePackage = moduleBasePackage;
this.moduleName = moduleName;
}
/**
* Returns a {@link Stream} of {@link ApplicationModuleSource}s by applying the given
@@ -66,4 +83,46 @@ record ApplicationModuleSource(
return new ApplicationModuleSource(pkg, name);
}
/**
* @return will never be {@literal null}.
*/
public JavaPackage getModuleBasePackage() {
return moduleBasePackage;
}
/**
* @return will never be {@literal null} or empty.
*/
public String getModuleName() {
return moduleName;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof ApplicationModuleSource that)) {
return false;
}
return Objects.equals(this.moduleName, that.moduleName)
&& Objects.equals(this.moduleBasePackage, that.moduleBasePackage);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(moduleName, moduleBasePackage);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.core.io.support.SpringFactoriesLoader;
import com.tngtech.archunit.core.domain.JavaClasses;
/**
* Lookup of external {@link ApplicationModuleSource} contributions via {@link ApplicationModuleSourceFactory}
* implementations.
*
* @author Oliver Drotbohm
* @since 1.3
*/
class ApplicationModuleSourceContributions {
static String LOCATION = SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION;
private final List<String> rootPackages;
private final List<ApplicationModuleSource> sources;
/**
* Creates a new {@link ApplicationModuleSourceContributions} for the given importer function, default
* {@link ApplicationModuleDetectionStrategy} and whether to use fully-qualified module names.
*
* @param importer must not be {@literal null}.
* @param defaultStrategy must not be {@literal null}.
* @param useFullyQualifiedModuleNames whether to use fully-qualified module names.
*/
public static ApplicationModuleSourceContributions of(Function<Collection<String>, JavaClasses> importer,
ApplicationModuleDetectionStrategy defaultStrategy, boolean useFullyQualifiedModuleNames) {
var loader = SpringFactoriesLoader.forResourceLocation(LOCATION, ApplicationModules.class.getClassLoader());
return new ApplicationModuleSourceContributions(loader.load(ApplicationModuleSourceFactory.class), importer,
defaultStrategy, useFullyQualifiedModuleNames);
}
/**
* Creates a new {@link ApplicationModuleSourceContributions} for the given {@link ApplicationModuleSourceFactory}s,
* importer function, default {@link ApplicationModuleDetectionStrategy} and whether to use fully-qualified module
* names.
*
* @param factories must not be {@literal null}.
* @param importer must not be {@literal null}.
* @param defaultStrategy must not be {@literal null}.
* @param useFullyQualifiedModuleNames whether to use fully-qualified module names.
*/
ApplicationModuleSourceContributions(List<? extends ApplicationModuleSourceFactory> factories,
Function<Collection<String>, JavaClasses> importer,
ApplicationModuleDetectionStrategy defaultStrategy, boolean useFullyQualifiedModuleNames) {
this.rootPackages = new ArrayList<>();
this.sources = new ArrayList<>();
factories.forEach(factory -> {
var contributedPackages = factory.getRootPackages();
var factoryStrategy = factory.getApplicationModuleDetectionStrategy();
var classes = importer.apply(contributedPackages);
var strategy = factoryStrategy == null ? defaultStrategy : factoryStrategy;
// Add discovered ApplicationModuleSources
rootPackages.addAll(contributedPackages);
contributedPackages.stream()
.map(it -> JavaPackage.of(Classes.of(classes), it))
.flatMap(it -> factory.getApplicationModuleSources(it, strategy, useFullyQualifiedModuleNames))
.forEach(this.sources::add);
// Add enumerated ApplicationModuleSources
Function<String, JavaPackage> packageRegistrar = it -> {
return JavaPackage.of(Classes.of(importer.apply(List.of(it))), it);
};
factory.getApplicationModuleSources(packageRegistrar, useFullyQualifiedModuleNames).forEach(this.sources::add);
});
}
/**
* @return will never be {@literal null}.
*/
public Stream<String> getRootPackages() {
return rootPackages.stream();
}
/**
* @return will never be {@literal null}.
*/
public Stream<ApplicationModuleSource> getSources() {
return sources.stream();
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
/**
* SPI to allow build units contribute additional {@link ApplicationModuleSource}s in the form of either declaring them
* directly via {@link #getModuleBasePackages()} and {@link #getApplicationModuleSources(Function, boolean)} or via
* provided {@link #getRootPackages()} and subsequent resolution via
* {@link #getApplicationModuleSources(JavaPackage, ApplicationModuleDetectionStrategy, boolean)} for each of the
* packages provided. <br>
* The following snippet would register {@link ApplicationModuleSource}s for {@code com.acme.foo} and
* {@code com.acme.bar} directly:
*
* <pre>
* {@code
* class MyCustomFactory implements ApplicationModuleSourceFactory {
*
* &#64;Override
* public List<String> getModuleBasePackages() {
* return List.of("com.acme.foo", "com.acme.bar");
* }
* }
* }
* </pre>
*
* The following snippet would register all modules located underneath {@code com.acme} found via the
* {@link ApplicationModuleDetectionStrategy#explicitlyAnnotated()} strategy:
*
* <pre>
* {@code
* class MyCustomFactory implements ApplicationModuleSourceFactory {
*
* &#64;Override
* public List<String> getRootPackages() {
* return List.of("com.acme");
* }
*
* &#64;Override
* ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
* return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
* }
* }
* }
* </pre>
*
* @author Oliver Drotbohm
* @since 1.3
*/
public interface ApplicationModuleSourceFactory {
/**
* Returns the additional root packages to be considered. The ones returned from this method will be scanned for
* {@link ApplicationModuleSource}s via
* {@link #getApplicationModuleSources(JavaPackage, ApplicationModuleDetectionStrategy, boolean)} using the
* {@link ApplicationModuleDetectionStrategy} returned from {@link #getApplicationModuleDetectionStrategy()}. If the
* latter is {@literal null}, the default {@link ApplicationModuleDetectionStrategy} is used.
*
* @return must not be {@literal null}.
*/
default List<String> getRootPackages() {
return Collections.emptyList();
}
/**
* Returns additional module base packages to create {@link ApplicationModuleSource}s from. Subsequently handled by
* {@link #getApplicationModuleSources(Function, boolean)}.
*
* @return must not be {@literal null}.
*/
default List<String> getModuleBasePackages() {
return Collections.emptyList();
}
/**
* Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModuleSource}s from
* the packages returned by {@link #getRootPackages()}. If {@literal null} is returned, the default
* {@link ApplicationModuleDetectionStrategy} will be used.
*
* @return can be {@literal null}.
*/
@Nullable
default ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
return null;
}
/**
* Creates all {@link ApplicationModuleSource}s using the given base package and
* {@link ApplicationModuleDetectionStrategy}.
*
* @param rootPackage will never be {@literal null}.
* @param strategy will never be {@literal null}.
* @param useFullyQualifiedModuleNames whether to use fully-qualified names for application modules.
* @return must not be {@literal null}.
* @see ApplicationModuleSource#from(JavaPackage, ApplicationModuleDetectionStrategy, boolean)
*/
default Stream<ApplicationModuleSource> getApplicationModuleSources(JavaPackage rootPackage,
ApplicationModuleDetectionStrategy strategy, boolean useFullyQualifiedModuleNames) {
return ApplicationModuleSource.from(rootPackage, strategy, useFullyQualifiedModuleNames);
}
/**
* Creates {@link ApplicationModuleSource} for individually, manually described application modules.
*
* @param packages will never be {@literal null}.
* @param useFullyQualifiedModuleNames whether to use fully-qualified names for application modules.
* @return must not be {@literal null}.
* @see ApplicationModuleSource#from(JavaPackage, String)
*/
default Stream<ApplicationModuleSource> getApplicationModuleSources(Function<String, JavaPackage> packages,
boolean useFullyQualifiedModuleNames) {
return getModuleBasePackages().stream()
.map(packages)
.map(it -> ApplicationModuleSource.from(it, useFullyQualifiedModuleNames ? it.getName() : it.getLocalName()));
}
}

View File

@@ -96,12 +96,11 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*
* @param metadata must not be {@literal null}.
* @param ignored must not be {@literal null}.
* @param useFullyQualifiedModuleNames can be {@literal null}.
* @param option must not be {@literal null}.
*/
protected ApplicationModules(ModulithMetadata metadata,
DescribedPredicate<? super JavaClass> ignored, boolean useFullyQualifiedModuleNames, ImportOption option) {
this(metadata, metadata.getBasePackages(), ignored, useFullyQualifiedModuleNames, option);
DescribedPredicate<? super JavaClass> ignored, ImportOption option) {
this(metadata, metadata.getBasePackages(), ignored, metadata.useFullyQualifiedModuleNames(), option);
}
/**
@@ -113,8 +112,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
* @param useFullyQualifiedModuleNames can be {@literal null}.
* @param option must not be {@literal null}.
* @deprecated since 1.2, for removal in 1.3. Use {@link ApplicationModules(ModulithMetadata, DescribedPredicate,
* boolean, ImportOption)} instead and set up {@link ModulithMetadata} to contain the packages you want to
* use.
* ImportOption)} instead and set up {@link ModulithMetadata} to contain the packages you want to use.
*/
@Deprecated(forRemoval = true)
protected ApplicationModules(ModulithMetadata metadata, Collection<String> packages,
@@ -128,8 +126,9 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
DescribedPredicate<? super JavaClass> excluded = DescribedPredicate.or(ignored, IS_AOT_TYPE, IS_SPRING_CGLIB_PROXY);
this.metadata = metadata;
this.allClasses = new ClassFileImporter() //
.withImportOption(option) //
var importer = new ClassFileImporter() //
.withImportOption(option);
this.allClasses = importer //
.importPackages(packages) //
.that(not(excluded));
@@ -138,9 +137,14 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
Classes classes = Classes.of(allClasses);
var strategy = ApplicationModuleDetectionStrategyLookup.getStrategy();
var sources = packages.stream() //
var contributions = ApplicationModuleSourceContributions.of(
pkgs -> importer.importPackages(pkgs).that(not(excluded)), strategy, useFullyQualifiedModuleNames);
var directSources = packages.stream() //
.map(it -> JavaPackage.of(classes, it))
.flatMap(it -> ApplicationModuleSource.from(it, strategy, useFullyQualifiedModuleNames))
.flatMap(it -> ApplicationModuleSource.from(it, strategy, useFullyQualifiedModuleNames));
var sources = Stream.concat(directSources, contributions.getSources())
.distinct()
.collect(Collectors.toUnmodifiableSet());
@@ -148,12 +152,12 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
.map(it -> {
return new ApplicationModule(it,
JavaPackages.onlySubPackagesOf(it.moduleBasePackage(),
sources.stream().map(ApplicationModuleSource::moduleBasePackage).toList())); //
JavaPackages.onlySubPackagesOf(it.getModuleBasePackage(),
sources.stream().map(ApplicationModuleSource::getModuleBasePackage).toList())); //
})
.collect(toMap(ApplicationModule::getName, Function.identity()));
this.rootPackages = packages.stream() //
this.rootPackages = Stream.concat(packages.stream(), contributions.getRootPackages()) //
.map(it -> JavaPackage.of(classes, it).toSingle()) //
.toList();
@@ -643,8 +647,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
return CACHE.computeIfAbsent(cacheKey, key -> {
var metadata = key.getMetadata();
var modules = new ApplicationModules(metadata, key.getIgnored(),
metadata.useFullyQualifiedModuleNames(), key.getOptions());
var modules = new ApplicationModules(metadata, key.getIgnored(), key.getOptions());
var sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contributed;
import java.util.List;
import org.springframework.modulith.core.ApplicationModuleDetectionStrategy;
import org.springframework.modulith.core.ApplicationModuleSourceFactory;
/**
* Example {@link ApplicationModuleSourceFactory}.
*
* @author Oliver Drotbohm
*/
public class ApplicationModuleSourceContribution implements ApplicationModuleSourceFactory {
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getRootPackages()
*/
@Override
public List<String> getRootPackages() {
return List.of("contributed");
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getApplicationModuleDetectionStrategy()
*/
@Override
public ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getModuleBasePackages()
*/
@Override
public List<String> getModuleBasePackages() {
return List.of("contributed.enumerated");
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule
package contributed.detected;

View File

@@ -0,0 +1 @@
package contributed.enumerated;

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import contributed.ApplicationModuleSourceContribution;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
/**
* Unit tests for {@link ApplicationModuleSourceContributions}.
*
* @author Oliver Drotbohm
* @since 1.3
*/
class ApplicationModuleSourceContributionsUnitTests {
@Test // GH-613
void detectsContributions() {
var factories = List.of(new ApplicationModuleSourceContribution());
var importer = new ClassFileImporter().withImportOption(new ImportOption.OnlyIncludeTests());
var strategy = ApplicationModuleDetectionStrategy.directSubPackage();
var contributions = new ApplicationModuleSourceContributions(factories, importer::importPackages, strategy, false);
assertThat(contributions.getRootPackages()).contains("contributed");
assertThat(contributions.getSources())
.extracting(ApplicationModuleSource::getModuleBasePackage)
.extracting(JavaPackage::getName)
.containsExactlyInAnyOrder("contributed.detected", "contributed.enumerated");
}
}

View File

@@ -110,6 +110,6 @@ public class TestUtils {
}
private static ApplicationModules of(ModulithMetadata metadata, DescribedPredicate<JavaClass> ignores) {
return new ApplicationModules(metadata, ignores, false, new ImportOption.OnlyIncludeTests()) {};
return new ApplicationModules(metadata, ignores, new ImportOption.OnlyIncludeTests()) {};
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contributed;
import java.util.List;
import org.springframework.modulith.core.ApplicationModuleDetectionStrategy;
import org.springframework.modulith.core.ApplicationModuleSourceFactory;
/**
* Example {@link ApplicationModuleSourceFactory}.
*
* @author Oliver Drotbohm
*/
public class ApplicationModuleSourceContribution implements ApplicationModuleSourceFactory {
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getRootPackages()
*/
@Override
public List<String> getRootPackages() {
return List.of("contributed");
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getApplicationModuleDetectionStrategy()
*/
@Override
public ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.core.ApplicationModuleSourceFactory#getModuleBasePackages()
*/
@Override
public List<String> getModuleBasePackages() {
return List.of("contributed.enumerated");
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contributed.detected;
/**
*
* @author Oliver Drotbohm
*/
public class DetectedContribution {
}

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule
package contributed.detected;

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contributed.enumerated;
/**
*
* @author Oliver Drotbohm
*/
public class EnumeratedContribution {
}

View File

@@ -0,0 +1 @@
package contributed.enumerated;

View File

@@ -0,0 +1 @@
org.springframework.modulith.core.ApplicationModuleSourceFactory=contributed.ApplicationModuleSourceContribution

View File

@@ -242,6 +242,25 @@ class ApplicationModulesIntegrationTest {
assertThatIllegalArgumentException().isThrownBy(() -> ApplicationModules.of("non.existant"));
}
@Test // GH-613
void detectsContributedApplicationModules() {
var location = ApplicationModuleSourceContributions.LOCATION;
ApplicationModuleSourceContributions.LOCATION = "META-INF/spring-test.factories";
try {
var modules = org.springframework.modulith.test.TestUtils.createApplicationModules(Application.class);
assertThat(modules.getModuleByName("detected")).isNotEmpty();
assertThat(modules.getModuleByName("enumerated")).isNotEmpty();
} finally {
ApplicationModuleSourceContributions.LOCATION = location;
}
}
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class<?>... types) {
Stream.of(types).forEach(type -> {

View File

@@ -23,6 +23,7 @@ import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextLoadException;
@@ -30,11 +31,25 @@ import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate;
import org.springframework.test.context.support.DefaultBootstrapContext;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* @author Oliver Drotbohm
*/
public class TestUtils {
/**
* Creates a fresh {@link ApplicationModules} instance no matter the caching state.
*
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
* @since 1.3
*/
public static ApplicationModules createApplicationModules(Class<?> type) {
return ApplicationModules.of(type, cacheInvalidator());
}
public static void assertDependencyMissing(Class<?> testClass, Class<?> expectedMissingDependency) {
CacheAwareContextLoaderDelegate delegate = new DefaultCacheAwareContextLoaderDelegate();
@@ -72,4 +87,15 @@ public class TestUtils {
private static RuntimeException asRuntimeException(Throwable o_O) {
return o_O instanceof RuntimeException ? (RuntimeException) o_O : new RuntimeException(o_O);
}
private static DescribedPredicate<JavaClass> cacheInvalidator() {
return new DescribedPredicate<JavaClass>("invalidator") {
@Override
public boolean test(JavaClass t) {
return false;
}
};
}
}

View File

@@ -1,5 +1,6 @@
[[fundamentals]]
= Fundamentals
:tabsize: 2
:toc:
:toclevels: 3
@@ -393,64 +394,6 @@ package example.inventory
----
======
[[customizing-modules]]
=== Customizing Module Detection
By default, application modules will be expected to be located in direct sub-packages of the package the Spring Boot application class resides in.
An alternative detection strategy can be activated to only consider package explicitly annotated, either via Spring Modulith's `@ApplicationModule` or jMolecules `@Module` annotation.
That strategy can be activated by configuring the `spring.modulith.detection-strategy` to `explicitly-annotated`.
.Switching the application module detection strategy to only consider annotated packages
[source, text]
----
spring.modulith.detection-strategy=explicitly-annotated
----
If the neither default application module detection strategy nor the manually annotated one 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:
.Implementing a custom `ApplicationModuleDetectionStrategy`
[tabs]
======
Java::
+
[source, java, role="primary"]
----
package example;
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
@Override
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
// Your module detection goes here
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
package example
class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {
override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
// Your module detection goes here
}
}
----
======
This class can now be registered as `spring.modulith.detection-strategy` as follows:
[source, text]
----
spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy
----
[[customizing-modules-arrangement]]
== Customizing the Application Modules Arrangement
@@ -514,3 +457,101 @@ The annotation exposes the following attributes to customize:
|Instructs Spring Modulith to treat the configured packages as additional root application packages. In other words, application module detection will be triggered for those as well.
|===
[[customizing-modules]]
=== Customizing Module Detection
By default, application modules will be expected to be located in direct sub-packages of the package the Spring Boot application class resides in.
An alternative detection strategy can be activated to only consider package explicitly annotated, either via Spring Modulith's `@ApplicationModule` or jMolecules `@Module` annotation.
That strategy can be activated by configuring the `spring.modulith.detection-strategy` to `explicitly-annotated`.
.Switching the application module detection strategy to only consider annotated packages
[source, text]
----
spring.modulith.detection-strategy=explicitly-annotated
----
If the neither default application module detection strategy nor the manually annotated one 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:
.Implementing a custom `ApplicationModuleDetectionStrategy`
[tabs]
======
Java::
+
[source, java, role="primary"]
----
package example;
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
@Override
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
// Your module detection goes here
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
package example
class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {
override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
// Your module detection goes here
}
}
----
======
This class can now be registered as `spring.modulith.detection-strategy` as follows:
[source, text]
----
spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy
----
=== Contributing Application Modules From Other Packages
While `@Modulithic` allows defining `additionalPackages` to trigger application module detection for packages other than the one of the annotated class, its usage requires knowing about those in advance.
As of version 1.3, Spring Modulith supports external contributions of application modules via the `ApplicationModuleSource` and `ApplicationModuleSourceFactory` abstractions.
An implementation of the latter can be registered in a `spring.factories` file located in `META-INF`.
[source, text]
----
org.springframework.modulith.core.ApplicationModuleSourceFactory=example.CustomApplicationModuleSourceFactory
----
Such a factory can either return arbitrary package names to get an `ApplicationModuleDetectionStrategy` applied, or explicitly return packages to create modules for.
[source, java]
----
package example;
public class CustomApplicationModuleSourceFactory implements ApplicationModuleDetectionStrategy {
@Override
public List<String> getRootPackages() {
return List.of("com.acme.toscan");
}
@Override
public ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
}
@Override
public List<String> getModuleBasePackages() {
return List.of("com.acme.module");
}
}
----
The above example would use `com.acme.toscan` to detect xref:fundamentals.adoc#customizing-modules[explicitly declared modules] within that and also create an application module from `com.acme.module`.
The package names returned from these will subsequently be translated into ``ApplicationModuleSource``s via the corresponding `getApplicationModuleSource(…)` flavors exposed in `ApplicationModuleDetectionStrategy`.