Skip to content

Commit

Permalink
GH-656 - Allow to configure the ApplicationModuleDetectionStrategy vi…
Browse files Browse the repository at this point in the history
…a a configuration property.

We now expose a configuration property spring.modulith.detection-strategy that can take either of the two prepared values "direct-sub-packages" (default) or "explicitly-annotated", or a fully qualified class name of the strategy to use.

Removed ApplicationModuleStrategies enum to avoid exposing the enum values as additional implementations. Those are now held as inline lambda expression in the factory methods on ApplicationModuleStrategy. Extracted the lookup of the strategy to use into ApplicationModuleDetectionStrategyLookup for easier testability.
  • Loading branch information
odrotbohm committed Jun 17, 2024
1 parent b37e39d commit 702fac5
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 102 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
*/
package org.springframework.modulith.core;

import java.util.Objects;
import java.util.stream.Stream;

import org.springframework.modulith.ApplicationModule;
import org.springframework.modulith.core.Types.JMoleculesTypes;

/**
* Strategy interface to customize which packages are considered module base packages.
*
Expand All @@ -34,22 +38,24 @@ public interface ApplicationModuleDetectionStrategy {
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);

/**
* A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package to be module
* base packages.
* A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package
* to be module base packages.
*
* @return will never be {@literal null}.
*/
static ApplicationModuleDetectionStrategy directSubPackage() {
return ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES;
return pkg -> pkg.getDirectSubPackages().stream();
}

/**
* A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with {@link ApplicationModule} module base
* packages.
* A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with
* {@link ApplicationModule} module base packages.
*
* @return will never be {@literal null}.
*/
static ApplicationModuleDetectionStrategy explictlyAnnotated() {
return ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED;
return pkg -> Stream.of(ApplicationModule.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent())
.filter(Objects::nonNull)
.flatMap(pkg::getSubPackagesAnnotatedWith);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.List;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
* A factory for the {@link ApplicationModuleDetectionStrategy} to be used when scanning code for
* {@link ApplicationModule}s.
*
* @author Oliver Drotbohm
*/
class ApplicationModuleDetectionStrategyLookup {

private static final String DETECTION_STRATEGY_PROPERTY = "spring.modulith.detection-strategy";
private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleDetectionStrategyLookup.class);
private static final Supplier<ApplicationModuleDetectionStrategy> FALLBACK_DETECTION_STRATEGY;

static {

FALLBACK_DETECTION_STRATEGY = () -> {

List<ApplicationModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(
ApplicationModuleDetectionStrategy.class, ApplicationModules.class.getClassLoader());

var size = loadFactories.size();

if (size == 0) {
return ApplicationModuleDetectionStrategy.directSubPackage();
}

if (size > 1) {

throw new IllegalStateException(
"Multiple module detection strategies configured. Only one supported! %s".formatted(loadFactories));
}

LOG.warn(
"Configuring the application module detection strategy via spring.factories is deprecated! Please configure {} instead.",
DETECTION_STRATEGY_PROPERTY);

return loadFactories.get(0);
};
}

/**
* Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModule}s. Will use
* the following algorithm:
* <ol>
* <li>Use the prepared strategies if
*
* @return
*/
static ApplicationModuleDetectionStrategy getStrategy() {

var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment);

var configuredStrategy = environment.getProperty(DETECTION_STRATEGY_PROPERTY, String.class);

// Nothing configured? Use fallback.
if (!StringUtils.hasText(configuredStrategy)) {
return FALLBACK_DETECTION_STRATEGY.get();
}

// Any of the prepared ones?
switch (configuredStrategy) {
case "direct-sub-packages":
return ApplicationModuleDetectionStrategy.directSubPackage();
case "explicitly-annotated":
return ApplicationModuleDetectionStrategy.explictlyAnnotated();
}

try {

// Lookup configured value as class
var strategyType = ClassUtils.forName(configuredStrategy, ApplicationModules.class.getClassLoader());
return BeanUtils.instantiateClass(strategyType, ApplicationModuleDetectionStrategy.class);

} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException(o_O);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import org.jmolecules.archunit.JMoleculesDddRules;
import org.springframework.aot.generate.Generated;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.modulith.core.Types.JMoleculesTypes;
import org.springframework.modulith.core.Violations.Violation;
Expand Down Expand Up @@ -65,29 +64,14 @@
public class ApplicationModules implements Iterable<ApplicationModule> {

private static final Map<CacheKey, ApplicationModules> CACHE = new ConcurrentHashMap<>();
private static final ApplicationModuleDetectionStrategy DETECTION_STRATEGY;

private static final ImportOption IMPORT_OPTION = new ImportOption.DoNotIncludeTests();
private static final boolean JGRAPHT_PRESENT = ClassUtils.isPresent("org.jgrapht.Graph",
ApplicationModules.class.getClassLoader());
private static final DescribedPredicate<CanBeAnnotated> IS_AOT_TYPE;
private static final DescribedPredicate<HasName> IS_SPRING_CGLIB_PROXY = nameContaining("$$SpringCGLIB$$");

static {

List<ApplicationModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(
ApplicationModuleDetectionStrategy.class,
ApplicationModules.class.getClassLoader());

if (loadFactories.size() > 1) {

throw new IllegalStateException(
String.format("Multiple module detection strategies configured. Only one supported! %s",
loadFactories));
}

DETECTION_STRATEGY = loadFactories.isEmpty() ? ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES
: loadFactories.get(0);

IS_AOT_TYPE = ClassUtils.isPresent("org.springframework.aot.generate.Generated",
ApplicationModules.class.getClassLoader()) ? getAtGenerated() : DescribedPredicate.alwaysFalse();
}
Expand Down Expand Up @@ -150,10 +134,11 @@ protected ApplicationModules(ModulithMetadata metadata, Collection<String> packa
Assert.notEmpty(allClasses, () -> "No classes found in packages %s!".formatted(packages));

Classes classes = Classes.of(allClasses);
var strategy = ApplicationModuleDetectionStrategyLookup.getStrategy();

this.modules = packages.stream() //
.map(it -> JavaPackage.of(classes, it))
.flatMap(DETECTION_STRATEGY::getModuleBasePackages) //
.flatMap(strategy::getModuleBasePackages) //
.map(it -> new ApplicationModule(it, useFullyQualifiedModuleNames)) //
.collect(toMap(ApplicationModule::getName, Function.identity()));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"properties": [
{
"name": "spring.modulith.detection-strategy",
"type": "java.lang.String",
"description": "The strategy how to detect application modules."
}
],
"hints": [
{
"name": "spring.modulith.detection-strategy",
"values": [
{
"value": "direct-sub-packages",
"description" : "Selects the direct sub-packages underneath the main application class as application module base interfaces."
},
{
"value": "explicitly-annotated",
"description" : "Only selects explicitly annotated packages as application module base packages (via @ApplicationModules or jMolecules' DDD @Module)."
}
],
"providers": [
{
"name": "class-reference",
"parameters": {
"target": "org.springframework.modulith.core.ApplicationModuleDetectionStrategy"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 java.util.stream.Stream;

import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link ApplicationModuleDetectionStrategy}.
*
* @author Oliver Drotbohm
*/
class ApplicationModuleDetectionStrategyLookupTests {

@Test // GH-656
void usesExplicitlyAnnotatedStrategyIfConfigured() {

System.setProperty("spring.config.additional-location", "classpath:detection/explicitly-annotated.properties");

assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
.isEqualTo(ApplicationModuleDetectionStrategy.explictlyAnnotated());
}

@Test // GH-656
void usesDirectSubPackagesStrategyIfConfigured() {

System.setProperty("spring.config.additional-location", "classpath:detection/direct-sub-packages.properties");

assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
.isEqualTo(ApplicationModuleDetectionStrategy.directSubPackage());
}

@Test // GH-656
void usesCustomStrategyIfConfigured() {

System.setProperty("spring.config.additional-location", "classpath:detection/custom-type.properties");

assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
.isInstanceOf(TestStrategy.class);
}

static class TestStrategy implements ApplicationModuleDetectionStrategy {

@Override
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,6 @@
*/
class ModuleDetectionStrategyUnitTest {

@Test
void usesExplicitlyAnnotatedConstant() {

assertThat(ApplicationModuleDetectionStrategy.explictlyAnnotated())
.isEqualTo(ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED);
}

@Test
void usesDirectSubPackages() {

assertThat(ApplicationModuleDetectionStrategy.directSubPackage())
.isEqualTo(ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES);
}

@Test
void detectsJMoleculesAnnotatedModule() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring.modulith.detection-strategy=org.springframework.modulith.core.ApplicationModuleDetectionStrategyLookupTests.TestStrategy
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring.modulith.detection-strategy=direct-sub-packages
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring.modulith.detection-strategy=explicitly-annotated
Loading

0 comments on commit 702fac5

Please sign in to comment.