Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,10 @@
*/
package org.springframework.modulith.core;

import java.util.List;
import java.util.function.Supplier;
import org.springframework.modulith.core.config.StrategyLookup;

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.DefaultResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.function.Supplier;

/**
* A factory for the {@link ApplicationModuleDetectionStrategy} to be used when scanning code for
Expand All @@ -37,35 +29,6 @@
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
Expand All @@ -74,7 +37,7 @@ class ApplicationModuleDetectionStrategyLookup {
* <li>Use the prepared strategies if either {@code direct-sub-packages} or {@code explicitly-annotated} is configured
* for the {@code spring.modulith.detection-strategy} configuration property.</li>
* <li>Interpret the configured value as class if it doesn't match the predefined values just described.</li>
* <li>Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.properties}
* <li>Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.factories}
* (deprecated)</li>
* <li>A final fallback on the {@code direct-sub-packages}.</li>
* </ol>
Expand All @@ -83,33 +46,16 @@ class ApplicationModuleDetectionStrategyLookup {
*/
static ApplicationModuleDetectionStrategy getStrategy() {

var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment,
new DefaultResourceLoader(ApplicationModuleDetectionStrategyLookup.class.getClassLoader()), null);

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.explicitlyAnnotated();
}

try {
Map<String, Supplier<ApplicationModuleDetectionStrategy>> predefinedStrategies = Map.of(
"direct-sub-packages", ApplicationModuleDetectionStrategy::directSubPackage,
"explicitly-annotated", ApplicationModuleDetectionStrategy::explicitlyAnnotated);

// Lookup configured value as class
var strategyType = ClassUtils.forName(configuredStrategy, ApplicationModules.class.getClassLoader());
return BeanUtils.instantiateClass(strategyType, ApplicationModuleDetectionStrategy.class);
var lookup = new StrategyLookup<>(
DETECTION_STRATEGY_PROPERTY,
ApplicationModuleDetectionStrategy.class,
predefinedStrategies,
ApplicationModuleDetectionStrategy::directSubPackage);

} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException(o_O);
}
return lookup.lookup();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2024-2025 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.config;

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.DefaultResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
* Generic utility for looking up strategy implementations based on configuration. Supports property-based
* configuration, predefined strategies, custom class instantiation, and {@link SpringFactoriesLoader} fallback.
*
* @param <T> the strategy type
* @since 1.4
*/
public class StrategyLookup<T> {

private static final Logger LOG = LoggerFactory.getLogger(StrategyLookup.class);

private final String propertyName;
private final Class<T> strategyType;
private final Map<String, Supplier<T>> predefinedStrategies;
private final Supplier<T> fallbackSupplier;

/**
* Creates a new {@link StrategyLookup} instance.
*
* @param propertyName the configuration property name (e.g., "spring.modulith.detection-strategy")
* @param strategyType the strategy class
* @param predefinedStrategies map of predefined strategy names to their suppliers
* @param fallbackSupplier the fallback strategy supplier
*/
public StrategyLookup(String propertyName, Class<T> strategyType, Map<String, Supplier<T>> predefinedStrategies,
Supplier<T> fallbackSupplier) {

this.propertyName = propertyName;
this.strategyType = strategyType;
this.predefinedStrategies = predefinedStrategies;
this.fallbackSupplier = fallbackSupplier;
}

/**
* Looks up and returns the strategy implementation using the following algorithm:
* <ol>
* <li>Use the predefined strategies if the configured property value matches one of them.</li>
* <li>Interpret the configured value as a class name if it doesn't match the predefined values.</li>
* <li>Use the {@link SpringFactoriesLoader} if no property is configured (deprecated).</li>
* <li>A final fallback on the provided fallback supplier.</li>
* </ol>
*
* @return the strategy implementation, never {@literal null}
*/
public T lookup() {

var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment,
new DefaultResourceLoader(StrategyLookup.class.getClassLoader()), null);

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

// Nothing configured? Use SpringFactoriesLoader or fallback
if (!StringUtils.hasText(configuredStrategy)) {
return lookupViaSpringFactoriesOrFallback();
}

// Check predefined strategies
var predefined = predefinedStrategies.get(configuredStrategy);

if (predefined != null) {
return predefined.get();
}

// Try to load configured value as class
try {

var strategyClass = ClassUtils.forName(configuredStrategy, strategyType.getClassLoader());
return BeanUtils.instantiateClass(strategyClass, strategyType);

} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException("Unable to load strategy class: " + configuredStrategy, o_O);
}
}

/**
* Attempts to load strategy via {@link SpringFactoriesLoader} (deprecated), falling back to the fallback supplier
* if none found.
*
* @return the strategy implementation, never {@literal null}
*/
private T lookupViaSpringFactoriesOrFallback() {

List<T> loadFactories = SpringFactoriesLoader.loadFactories(strategyType, strategyType.getClassLoader());

var size = loadFactories.size();

if (size == 0) {
return fallbackSupplier.get();
}

if (size > 1) {
throw new IllegalStateException(
"Multiple strategies configured via spring.factories. Only one supported! %s".formatted(loadFactories));
}

LOG.warn(
"Configuring strategy via spring.factories is deprecated! Please configure {} instead.",
propertyName);

return loadFactories.get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,27 @@
*/
package org.springframework.modulith.docs;

import static java.util.stream.Collectors.*;
import static org.springframework.util.ClassUtils.*;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaModifier;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModuleDependency;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ArchitecturallyEvidentType;
import org.springframework.modulith.core.*;
import org.springframework.modulith.core.ArchitecturallyEvidentType.ReferenceMethod;
import org.springframework.modulith.core.DependencyType;
import org.springframework.modulith.core.EventType;
import org.springframework.modulith.core.FormattableType;
import org.springframework.modulith.core.Source;
import org.springframework.modulith.core.SpringBean;
import org.springframework.modulith.docs.ConfigurationProperties.ModuleProperty;
import org.springframework.modulith.docs.Documenter.CanvasOptions;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaModifier;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;
import static org.springframework.util.ClassUtils.convertClassNameToResourcePath;

/**
* @author Oliver Drotbohm
Expand All @@ -56,11 +47,9 @@ class Asciidoctor {
private static final Pattern LINE_BREAKS = Pattern.compile("\\<\\s*br\\s*\\>");
private static final Logger LOG = LoggerFactory.getLogger(Asciidoctor.class);

private static final Optional<DocumentationSource> DOC_SOURCE = getSpringModulithDocsSource();

private final ApplicationModules modules;
private final String javaDocBase;
private final Optional<DocumentationSource> docSource;
private final DocumentationSource docSource;

private Asciidoctor(ApplicationModules modules, String javaDocBase) {

Expand All @@ -69,13 +58,15 @@ private Asciidoctor(ApplicationModules modules, String javaDocBase) {

this.javaDocBase = javaDocBase;
this.modules = modules;
this.docSource = DOC_SOURCE.map(it -> new CodeReplacingDocumentationSource(it, this));

var rawSource = DocumentationSourceLookup.getDocumentationSource();
this.docSource = new CodeReplacingDocumentationSource(rawSource, this);
}

/**
* Creates a new {@link Asciidoctor} instance for the given {@link ApplicationModules} and Javadoc base URI.
*
* @param modules must not be {@literal null}.
* @param modules must not be {@literal null}.
* @param javadocBase can be {@literal null}.
* @return will never be {@literal null}.
*/
Expand Down Expand Up @@ -103,7 +94,7 @@ public String toInlineCode(String source) {

var parts = source.split("#");
var type = parts[0];
var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.<String> empty();
var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.<String>empty();

if (type.isBlank()) {
return methodSignature.map(Asciidoctor::toCode).orElse(source);
Expand Down Expand Up @@ -138,7 +129,7 @@ public String toInlineCode(SpringBean bean) {

private String withDocumentation(String base, JavaClass type) {

return docSource.flatMap(it -> it.getDocumentation(type))
return docSource.getDocumentation(type)
.map(it -> base + " -- " + it)
.orElse(base);
}
Expand Down Expand Up @@ -194,7 +185,7 @@ public String renderPublishedEvents(ApplicationModule module) {
continue;
}

var documentation = docSource.flatMap(it -> it.getDocumentation(eventType.getType()))
var documentation = docSource.getDocumentation(eventType.getType())
.map(" -- "::concat);

builder.append("* ")
Expand Down Expand Up @@ -333,7 +324,7 @@ private String renderReferenceMethod(ReferenceMethod it, int level) {
var isAsync = it.isAsync() ? "(async) " : "";
var indent = "*".repeat(level + 1);

return docSource.flatMap(source -> source.getDocumentation(method))
return docSource.getDocumentation(method)
.map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync, doc))
.orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync));
}
Expand Down Expand Up @@ -411,7 +402,7 @@ public String renderBeanReferences(ApplicationModule module) {
}

public String renderModuleDescription(ApplicationModule module) {
return docSource.flatMap(it -> it.getDocumentation(module.getBasePackage())).orElse("");
return docSource.getDocumentation(module.getBasePackage()).orElse("");
}

public String renderHeadline(int i, String modules) {
Expand All @@ -426,16 +417,6 @@ public String renderGeneralInclude(String componentsFilename) {
return "include::" + componentsFilename + "[]" + System.lineSeparator();
}

private static Optional<DocumentationSource> getSpringModulithDocsSource() {

return SpringModulithDocumentationSource.getInstance()
.map(it -> {
LOG.debug("Using Javadoc extracted by Spring Modulith in {}.",
SpringModulithDocumentationSource.getMetadataLocation());
return it;
});
}

private static final String wrap(String source, String chars) {
return chars + source + chars;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*
* @author Oliver Drotbohm
*/
interface DocumentationSource {
public interface DocumentationSource {

/**
* Returns the documentation to be used for the given {@link JavaMethod}.
Expand Down
Loading