GH-14 - Remove Lombok from production sources.

Polished a lot of Javadoc.
This commit is contained in:
Oliver Drotbohm
2023-01-12 00:54:04 +01:00
parent 20554c3af3
commit 9ce6bf23ae
87 changed files with 3546 additions and 1061 deletions

21
pom.xml
View File

@@ -120,24 +120,6 @@ limitations under the License.
<build>
<plugins>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<configuration>
<addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
@@ -154,7 +136,6 @@ limitations under the License.
<doclint>none</doclint>
<quiet>true</quiet>
<show>package</show>
<sourcepath>target/generated-sources/delombok</sourcepath>
</configuration>
</plugin>
@@ -433,7 +414,7 @@ limitations under the License.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -17,8 +17,6 @@ package org.springframework.modulith.actuator;
import static java.util.stream.Collectors.*;
import lombok.extern.slf4j.Slf4j;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
@@ -28,6 +26,8 @@ import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.modulith.model.ApplicationModule;
@@ -41,10 +41,11 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@Slf4j
@Endpoint(id = "applicationmodules")
public class ApplicationModulesEndpoint {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesEndpoint.class);
private static final Function<Set<DependencyType>, Set<DependencyType>> REMOVE_DEFAULT_DEPENDENCY_TYPE_IF_OTHERS_PRESENT = it -> {
if (it.stream().anyMatch(type -> type != DependencyType.DEFAULT)) {
@@ -69,7 +70,7 @@ public class ApplicationModulesEndpoint {
Assert.notNull(runtime, "ModulesRuntime must not be null!");
LOG.debug("Activating Spring Modulith actuator.");
LOGGER.debug("Activating Spring Modulith actuator.");
this.runtime = runtime;
}

View File

@@ -32,7 +32,7 @@ public @interface ApplicationModule {
/**
* The human readable name of the module to be used for display and documentation purposes.
*
* @return
* @return will never be {@literal null}.
*/
String displayName() default "";
@@ -43,7 +43,7 @@ public @interface ApplicationModule {
* {@link NamedInterface}s need to be separated by a double colon {@code ::}, e.g. {@code module::API} if
* {@code module} is the logical module name and {@code API} is the name of the named interface.
*
* @return
* @return will never be {@literal null}.
* @see NamedInterface
*/
String[] allowedDependencies() default {};

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -33,12 +30,26 @@ import org.springframework.util.StringUtils;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class AnnotationModulithMetadata implements ModulithMetadata {
private final Class<?> modulithType;
private final Modulithic annotation;
/**
* Creates a new {@link AnnotationModulithMetadata} for the given type and annotation.
*
* @param modulithType must not be {@literal null}.
* @param annotation must not be {@literal null}.
*/
private AnnotationModulithMetadata(Class<?> modulithType, Modulithic annotation) {
Assert.notNull(modulithType, "Type must not be null!");
Assert.notNull(annotation, "Annotation must not be null!");
this.modulithType = modulithType;
this.annotation = annotation;
}
/**
* Creates a {@link ModulithMetadata} inspecting {@link Modulithic} annotation or return {@link Optional#empty()} if
* the type given does not carry the annotation.
@@ -62,6 +73,15 @@ class AnnotationModulithMetadata implements ModulithMetadata {
*/
@Override
public Object getModulithSource() {
return getSource();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getSource() {
return modulithType;
}

View File

@@ -23,18 +23,11 @@ import static org.springframework.modulith.model.Types.JavaXTypes.*;
import static org.springframework.modulith.model.Types.SpringDataTypes.*;
import static org.springframework.modulith.model.Types.SpringTypes.*;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.Value;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -63,20 +56,19 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode(doNotUseGetters = true)
public class ApplicationModule {
/**
* The base package of the {@link ApplicationModule}.
*/
private final @Getter JavaPackage basePackage;
private final JavaPackage basePackage;
private final ApplicationModuleInformation information;
/**
* All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
* or implicitly.
*/
private final @Getter NamedInterfaces namedInterfaces;
private final NamedInterfaces namedInterfaces;
private final boolean useFullyQualifiedModuleNames;
private final Supplier<Classes> springBeans;
@@ -104,6 +96,24 @@ public class ApplicationModule {
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
}
/**
* Returns the module's base package.
*
* @return the basePackage
*/
public JavaPackage getBasePackage() {
return basePackage;
}
/**
* Returns all {@link NamedInterfaces} exposed by the module.
*
* @return the namedInterfaces will never be {@literal null}.
*/
public NamedInterfaces getNamedInterfaces() {
return namedInterfaces;
}
/**
* Returns the logical name of the module.
*
@@ -407,6 +417,41 @@ public class ApplicationModule {
return getType(candidate).isPresent();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ApplicationModule that)) {
return false;
}
return Objects.equals(this.basePackage, that.basePackage) //
&& Objects.equals(this.entities, that.entities) //
&& Objects.equals(this.information, that.information) //
&& Objects.equals(this.namedInterfaces, that.namedInterfaces) //
&& Objects.equals(this.publishedEvents, that.publishedEvents) //
&& Objects.equals(this.springBeans, that.springBeans) //
&& Objects.equals(this.useFullyQualifiedModuleNames, that.useFullyQualifiedModuleNames) //
&& Objects.equals(this.valueTypes, that.valueTypes);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(basePackage, entities, information, namedInterfaces, publishedEvents, springBeans,
useFullyQualifiedModuleNames, valueTypes);
}
private List<EventType> findPublishedEvents() {
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
@@ -529,15 +574,28 @@ public class ApplicationModule {
.toList();
}
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
static class DeclaredDependency {
private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'.";
private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s.";
@NonNull ApplicationModule target;
@NonNull NamedInterface namedInterface;
private final ApplicationModule target;
private final NamedInterface namedInterface;
/**
* Creates a new {@link DeclaredDependency} for the given {@link ApplicationModule} and {@link NamedInterface}.
*
* @param target must not be {@literal null}.
* @param namedInterface must not be {@literal null}.
*/
private DeclaredDependency(ApplicationModule target, NamedInterface namedInterface) {
Assert.notNull(target, "Target ApplicationModule must not be null!");
Assert.notNull(namedInterface, "NamedInterface must not be null!");
this.target = target;
this.namedInterface = namedInterface;
}
/**
* Creates an {@link DeclaredDependency} to the module and optionally named interface defined by the given
@@ -580,10 +638,22 @@ public class ApplicationModule {
* @return
*/
public static DeclaredDependency to(ApplicationModule module) {
Assert.notNull(module, "ApplicationModule must not be null!");
return new DeclaredDependency(module, module.getNamedInterfaces().getUnnamedInterface());
}
/**
* Returns whether the {@link DeclaredDependency} contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return namedInterface.contains(type);
}
@@ -595,6 +665,35 @@ public class ApplicationModule {
public String toString() {
return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DeclaredDependency that)) {
return false;
}
return Objects.equals(this.target, that.target) //
&& Objects.equals(this.namedInterface, that.namedInterface);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(target, namedInterface);
}
}
/**
@@ -602,16 +701,26 @@ public class ApplicationModule {
*
* @author Oliver Drotbohm
*/
@Value
static class DeclaredDependencies {
List<DeclaredDependency> dependencies;
private final List<DeclaredDependency> dependencies;
/**
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
*
* @param dependencies must not be {@literal null}.
*/
public DeclaredDependencies(List<DeclaredDependency> dependencies) {
Assert.notNull(dependencies, "Dependencies must not be null!");
this.dependencies = dependencies;
}
/**
* Returns whether any of the dependencies contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean contains(JavaClass type) {
@@ -621,10 +730,17 @@ public class ApplicationModule {
.anyMatch(it -> it.contains(type));
}
/**
* Returns whether the {@link DeclaredDependencies} are empty.
*/
public boolean isEmpty() {
return dependencies.isEmpty();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
@@ -632,18 +748,64 @@ public class ApplicationModule {
.map(DeclaredDependency::toString)
.collect(Collectors.joining(", "));
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DeclaredDependencies that)) {
return false;
}
return Objects.equals(this.dependencies, that.dependencies);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(dependencies);
}
}
@EqualsAndHashCode
@RequiredArgsConstructor
static class QualifiedDependency {
private static final List<String> INJECTION_TYPES = Arrays.asList(//
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private static final List<String> INJECTION_TYPES = Arrays.asList(AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private final @NonNull @Getter JavaClass source, target;
private final @NonNull String description;
private final @NonNull DependencyType type;
private final JavaClass source, target;
private final String description;
private final DependencyType type;
/**
* Creates a new {@link QualifiedDependency} from the given source and target {@link JavaClass}, description and
* {@link DependencyType}.
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
* @param description must not be {@literal null}.
* @param type must not be {@literal null}.
*/
public QualifiedDependency(JavaClass source, JavaClass target, String description, DependencyType type) {
Assert.notNull(source, "Source JavaClass must not be null!");
Assert.notNull(target, "Target JavaClass must not be null!");
Assert.notNull(description, "Description must not be null!");
Assert.notNull(type, "DependencyType must not be null!");
this.source = source;
this.target = target;
this.description = description;
this.type = type;
}
QualifiedDependency(Dependency dependency) {
this(dependency.getOriginClass(), //
@@ -652,6 +814,65 @@ public class ApplicationModule {
DependencyType.forDependency(dependency));
}
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
var description = createDescription(codeUnit, parameter, "parameter");
var type = DependencyType.forCodeUnit(codeUnit) //
.defaultOr(() -> DependencyType.forParameter(parameter));
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
}
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
DependencyType.DEFAULT);
}
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
var source = type.getType();
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
}
static Stream<QualifiedDependency> allFrom(JavaCodeUnit codeUnit) {
var parameterDependencies = codeUnit.getRawParameterTypes()//
.stream() //
.map(it -> fromCodeUnitParameter(codeUnit, it));
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
return Stream.concat(parameterDependencies, returnType);
}
/**
* Returns the source {@link JavaClass}.
*
* @return the source will never be {@literal null}.
*/
public JavaClass getSource() {
return source;
}
/**
* Returns the target {@link JavaClass}.
*
* @return the target must not be {@literal null}.
*/
public JavaClass getTarget() {
return target;
}
/**
* Returns whether the {@link QualifiedDependency} has the given {@link DependencyType}.
*
* @param type must not be {@literal null}.
* @return
*/
boolean hasType(DependencyType type) {
return this.type.equals(type);
}
@@ -704,39 +925,34 @@ public class ApplicationModule {
return type.format(FormatableType.of(source), FormatableType.of(target));
}
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
var description = createDescription(codeUnit, parameter, "parameter");
var type = DependencyType.forCodeUnit(codeUnit) //
.defaultOr(() -> DependencyType.forParameter(parameter));
if (this == obj) {
return true;
}
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
if (!(obj instanceof QualifiedDependency other)) {
return false;
}
return Objects.equals(this.source, other.source) //
&& Objects.equals(this.target, other.target) //
&& Objects.equals(this.description, other.description) //
&& Objects.equals(this.type, other.type); //
}
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
DependencyType.DEFAULT);
}
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
var source = type.getType();
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
}
static Stream<QualifiedDependency> allFrom(JavaCodeUnit codeUnit) {
var parameterDependencies = codeUnit.getRawParameterTypes()//
.stream() //
.map(it -> fromCodeUnitParameter(codeUnit, it));
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
return Stream.concat(parameterDependencies, returnType);
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(source, target, description, type);
}
private static Stream<QualifiedDependency> fromConstructorOf(ArchitecturallyEvidentType source) {
@@ -885,15 +1101,35 @@ public class ApplicationModule {
}
}
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency {
private final QualifiedDependency dependency;
private final ApplicationModule target;
static Stream<DefaultApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
/**
* Creates a new {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
* {@link ApplicationModules}.
*
* @param dependency must not be {@literal null}.
* @param target must not be {@literal null}.
*/
private DefaultApplicationModuleDependency(QualifiedDependency dependency, ApplicationModule target) {
Assert.notNull(dependency, "QualifiedDependency must not be null!");
Assert.notNull(target, "Target ApplicationModule must not be null!");
this.dependency = dependency;
this.target = target;
}
/**
* Creates a new {@link Stream} of {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
* {@link ApplicationModules}.
*
* @param dependency must not be {@literal null}.
* @param modules must not be {@literal null}.
*/
static Stream<ApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
return modules.getModuleByType(dependency.getTarget()).stream()
.map(it -> new DefaultApplicationModuleDependency(dependency, it));
@@ -934,5 +1170,42 @@ public class ApplicationModule {
public ApplicationModule getTargetModule() {
return target;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "DefaultApplicationModuleDependency [dependency=" + dependency + ", target=" + target + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DefaultApplicationModuleDependency other)) {
return false;
}
return Objects.equals(this.target, other.target) //
&& Objects.equals(this.dependency, other.dependency);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(target, dependency);
}
}
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.RequiredArgsConstructor;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
@@ -25,16 +23,45 @@ import java.util.stream.Stream;
import org.springframework.util.Assert;
/**
* The materialized, in other words actually present dependencies of the current module towards other modules.
* The materialized, in other words actually present, dependencies of the current module towards other modules.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(staticName = "of")
public class ApplicationModuleDependencies {
private final List<ApplicationModuleDependency> dependencies;
private final ApplicationModules modules;
/**
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
*
* @param dependencies must not be {@literal null}.
* @param modules must not be {@literal null}.
*/
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies, ApplicationModules modules) {
Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!");
Assert.notNull(modules, "ApplicationModules must not be null!");
this.dependencies = dependencies;
this.modules = modules;
}
/**
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
*
* @param dependencies must not be {@literal null}.
* @param modules must not be {@literal null}.
* @return will never be {@literal null}.
*/
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies,
ApplicationModules modules) {
return new ApplicationModuleDependencies(dependencies, modules);
}
/**
* Returns whether the dependencies contain the given {@link ApplicationModule}.
*

View File

@@ -19,12 +19,6 @@ import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static java.util.stream.Collectors.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -60,7 +54,6 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
* @author Oliver Drotbohm
* @author Peter Gafert
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApplicationModules implements Iterable<ApplicationModule> {
private static final Map<CacheKey, ApplicationModules> CACHE = new HashMap<>();
@@ -90,7 +83,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
private final Map<String, ApplicationModule> modules;
private final JavaClasses allClasses;
private final List<JavaPackage> rootPackages;
private final @With(AccessLevel.PRIVATE) @Getter Set<ApplicationModule> sharedModules;
private final Set<ApplicationModule> sharedModules;
private final List<String> orderedNames;
private boolean verified;
@@ -123,6 +116,39 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
: modules.values().stream().map(ApplicationModule::getName).toList();
}
/**
* Creates a new {@link ApplicationModules} for the given {@link ModulithMetadata}, {@link ApplicationModule}s,
* {@link JavaClasses}, {@link JavaPackage}s, shared {@link ApplicationModule}s, ordered module names and verified
* flag.
*
* @param metadata must not be {@literal null}.
* @param modules must not be {@literal null}.
* @param allClasses must not be {@literal null}.
* @param rootPackages must not be {@literal null}.
* @param sharedModules must not be {@literal null}.
* @param orderedNames must not be {@literal null}.
* @param verified
*/
private ApplicationModules(ModulithMetadata metadata, Map<String, ApplicationModule> modules, JavaClasses classes,
List<JavaPackage> rootPackages, Set<ApplicationModule> sharedModules, List<String> orderedNames,
boolean verified) {
Assert.notNull(metadata, "ModulithMetadata must not be null!");
Assert.notNull(modules, "Application modules must not be null!");
Assert.notNull(classes, "JavaClasses must not be null!");
Assert.notNull(rootPackages, "Root JavaPackages must not be null!");
Assert.notNull(sharedModules, "Shared ApplicationModules must not be null!");
Assert.notNull(orderedNames, "Ordered application module names must not be null!");
this.metadata = metadata;
this.modules = modules;
this.allClasses = classes;
this.rootPackages = rootPackages;
this.sharedModules = sharedModules;
this.orderedNames = orderedNames;
this.verified = verified;
}
/**
* Creates a new {@link ApplicationModules} relative to the given modulith type. Will inspect the {@link Modulith}
* annotation on the class given for advanced customizations of the module setup.
@@ -147,7 +173,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
public static ApplicationModules of(Class<?> modulithType, DescribedPredicate<JavaClass> ignored) {
CacheKey key = TypeKey.of(modulithType, ignored);
CacheKey key = new TypeKey(modulithType, ignored);
return CACHE.computeIfAbsent(key, it -> {
@@ -177,7 +203,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
public static ApplicationModules of(String javaPackage, DescribedPredicate<JavaClass> ignored) {
CacheKey key = PackageKey.of(javaPackage, ignored);
CacheKey key = new PackageKey(javaPackage, ignored);
return CACHE.computeIfAbsent(key, it -> {
@@ -189,33 +215,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
}
/**
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
*
* @param key must not be {@literal null}.
* @return will never be {@literal null}.
* @deprecated use {@link #getSource()} instead
*/
private static ApplicationModules of(CacheKey key) {
Assert.notNull(key, "Cache key must not be null!");
ModulithMetadata metadata = key.getMetadata();
Set<String> basePackages = new HashSet<>();
basePackages.add(key.getBasePackage());
basePackages.addAll(metadata.getAdditionalPackages());
ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
Set<ApplicationModule> sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
.collect(Collectors.toSet());
return modules.withSharedModules(sharedModules);
@Deprecated(forRemoval = true)
public Object getModulithSource() {
return metadata.getSource();
}
public Object getModulithSource() {
return metadata.getModulithSource();
/**
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
*
* @return will never be {@literal null}.
*/
public Object getSource() {
return metadata.getSource();
}
/**
* Returns all {@link ApplicationModule}s registered as shared ones.
*
* @return will never be {@literal null}.
*/
public Set<ApplicationModule> getSharedModules() {
return sharedModules;
}
/**
@@ -421,9 +446,13 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
return this.stream().map(ApplicationModule::toString).collect(Collectors.joining("\n"));
}
private ApplicationModules withSharedModules(Set<ApplicationModule> sharedModules) {
return new ApplicationModules(metadata, modules, allClasses, rootPackages, sharedModules, orderedNames, verified);
}
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
EvaluationResult result = SlicesRuleDefinition.slices() //
var result = SlicesRuleDefinition.slices() //
.matching(rootPackage.getName().concat(".(*)..")) //
.should().beFreeOfCycles() //
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
@@ -457,7 +486,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
private ApplicationModule getRequiredModule(String moduleName) {
ApplicationModule module = modules.get(moduleName);
var module = modules.get(moduleName);
if (module == null) {
throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName));
@@ -466,6 +495,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
return module;
}
/**
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
*
* @param key must not be {@literal null}.
* @return will never be {@literal null}.
*/
private static ApplicationModules of(CacheKey key) {
Assert.notNull(key, "Cache key must not be null!");
var metadata = key.getMetadata();
var basePackages = new HashSet<String>();
basePackages.add(key.getBasePackage());
basePackages.addAll(metadata.getAdditionalPackages());
var modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
var sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
.collect(Collectors.toSet());
return modules.withSharedModules(sharedModules);
}
public static class Filters {
public static DescribedPredicate<JavaClass> withoutModules(String... names) {
@@ -489,11 +544,22 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
ModulithMetadata getMetadata();
}
@Value(staticConstructor = "of")
private static final class TypeKey implements CacheKey {
Class<?> type;
DescribedPredicate<JavaClass> ignored;
private final Class<?> type;
private final DescribedPredicate<JavaClass> ignored;
/**
* Creates a new {@link TypeKey} for the given type and {@link DescribedPredicate} of ignored {@link JavaClass}es.
*
* @param type must not be {@literal null}.
* @param ignored must not be {@literal null}.
*/
TypeKey(Class<?> type, DescribedPredicate<JavaClass> ignored) {
this.type = type;
this.ignored = ignored;
}
/*
* (non-Javadoc)
@@ -512,13 +578,79 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(type);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
*/
@Override
public DescribedPredicate<JavaClass> getIgnored() {
return ignored;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof TypeKey other)) {
return false;
}
return Objects.equals(this.type, other.type) //
&& Objects.equals(this.ignored, other.ignored);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(type, ignored);
}
}
@Value(staticConstructor = "of")
private static final class PackageKey implements CacheKey {
String basePackage;
DescribedPredicate<JavaClass> ignored;
private final String basePackage;
private final DescribedPredicate<JavaClass> ignored;
/**
* Creates a new {@link PackageKey} for the given base package and {@link DescribedPredicate} of ignored
* {@link JavaClass}es.
*
* @param basePackage must not be {@literal null}.
* @param ignored must not be {@literal null}.
*/
PackageKey(String basePackage, DescribedPredicate<JavaClass> ignored) {
this.basePackage = basePackage;
this.ignored = ignored;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getBasePackage()
*/
@Override
public String getBasePackage() {
return basePackage;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
*/
public DescribedPredicate<JavaClass> getIgnored() {
return ignored;
}
/*
* (non-Javadoc)
@@ -528,6 +660,34 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(basePackage);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PackageKey that)) {
return false;
}
return Objects.equals(this.basePackage, that.basePackage) //
&& Objects.equals(this.ignored, that.ignored);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(basePackage, ignored);
}
}
/**

View File

@@ -17,11 +17,6 @@ package org.springframework.modulith.model;
import static org.springframework.modulith.model.Types.JavaXTypes.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@@ -51,12 +46,15 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class ArchitecturallyEvidentType {
private static Map<Key, ArchitecturallyEvidentType> CACHE = new HashMap<>();
private final @Getter JavaClass type;
private final JavaClass type;
protected ArchitecturallyEvidentType(JavaClass type) {
this.type = type;
}
/**
* Creates a new {@link ArchitecturallyEvidentType} for the given {@link JavaType} and {@link Classes} of Spring
@@ -68,7 +66,7 @@ public abstract class ArchitecturallyEvidentType {
*/
public static ArchitecturallyEvidentType of(JavaClass type, Classes beanTypes) {
return CACHE.computeIfAbsent(Key.of(type, beanTypes), it -> {
return CACHE.computeIfAbsent(new Key(type, beanTypes), it -> {
List<ArchitecturallyEvidentType> delegates = new ArrayList<>();
@@ -86,6 +84,15 @@ public abstract class ArchitecturallyEvidentType {
});
}
/**
* Returns the {@link JavaClass} backing the {@link ArchitecturallyEvidentType}.
*
* @return the type wnn
*/
public JavaClass getType() {
return type;
}
/**
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name.
*
@@ -620,18 +627,20 @@ public abstract class ArchitecturallyEvidentType {
}
}
@Value(staticConstructor = "of")
private static class Key {
private static record Key(JavaClass type, Classes beanTypes) {}
JavaClass type;
Classes beanTypes;
}
@Value
public final class ReferenceMethod {
public static class ReferenceMethod {
private final JavaMethod method;
public ReferenceMethod(JavaMethod method) {
this.method = method;
}
public JavaMethod getMethod() {
return method;
}
public boolean isAsync() {
return method.isAnnotatedWith(SpringTypes.AT_ASYNC) || method.isMetaAnnotatedWith(SpringTypes.AT_ASYNC);
}

View File

@@ -15,15 +15,13 @@
*/
package org.springframework.modulith.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collector;
@@ -46,8 +44,6 @@ import com.tngtech.archunit.core.domain.properties.HasName;
/**
* @author Oliver Drotbohm
*/
@ToString
@EqualsAndHashCode
class Classes implements DescribedIterable<JavaClass> {
public static Classes NONE = Classes.of(Collections.emptyList());
@@ -65,7 +61,7 @@ class Classes implements DescribedIterable<JavaClass> {
this.classes = classes.stream() //
.sorted(Comparator.comparing(JavaClass::getName)) //
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
.toList();
}
/**
@@ -190,35 +186,73 @@ class Classes implements DescribedIterable<JavaClass> {
return classes.iterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Classes [classes=" + classes + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Classes that)) {
return false;
}
return Objects.equals(classes, that.classes);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(classes);
}
String format() {
return classes.stream() //
.map(Classes::format) //
.collect(Collectors.joining("\n"));
}
String format(String basePackage) {
return classes.stream() //
.map(it -> Classes.format(it, basePackage)) //
.collect(Collectors.joining("\n"));
}
private static String format(JavaClass type) {
return format(type, "");
}
static String format(JavaClass type, String basePackage) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(basePackage, "Base package must not be null!");
String prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
String name = StringUtils.hasText(basePackage) //
var prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
var name = StringUtils.hasText(basePackage) //
? type.getName().replace(basePackage, "") //
: type.getName();
return String.format("%s %s", prefix, name);
}
private static String format(JavaClass type) {
return format(type, "");
}
private static class SameClass extends DescribedPredicate<JavaClass> {
private final JavaClass reference;

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
@@ -26,6 +22,7 @@ import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.NonNull;
import org.springframework.modulith.Modulith;
import org.springframework.modulith.Modulithic;
import org.springframework.modulith.model.Types.SpringTypes;
@@ -37,13 +34,24 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class DefaultModulithMetadata implements ModulithMetadata {
private static final Class<? extends Annotation> AT_SPRING_BOOT_APPLICATION = Types
.loadIfPresent(SpringTypes.AT_SPRING_BOOT_APPLICATION);
private final @NonNull Object modulithSource;
private final @NonNull Object source;
/**
* Creates a new {@link DefaultModulithMetadata} for the given source.
*
* @param source must not be {@literal null}.
*/
private DefaultModulithMetadata(Object source) {
Assert.notNull(source, "Source must not be null!");
this.source = source;
}
/**
* Creates a new {@link ModulithMetadata} representing the defaults of a class annotated but not customized with
@@ -80,7 +88,16 @@ class DefaultModulithMetadata implements ModulithMetadata {
*/
@Override
public Object getModulithSource() {
return modulithSource;
return getSource();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getSource() {
return source;
}
/*

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.modulith.model;
import lombok.Value;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.util.Assert;
@@ -30,15 +29,9 @@ import com.tngtech.archunit.core.domain.JavaModifier;
*
* @author Oliver Drotbohm
*/
@Value
public class EventType {
private final JavaClass type;
/**
* The sources that create that event. Includes static factory methods that return an instance of the event type
* itself as well as constructor invocations, except ones from the factory methods.
*/
private final List<Source> sources;
/**
@@ -66,7 +59,68 @@ public class EventType {
.toList();
}
/**
* The {@link JavaClass} of the {@link EventType}.
*
* @return will never be {@literal null}.
*/
public JavaClass getType() {
return type;
}
/**
* The sources that create that event. Includes static factory methods that return an instance of the event type
* itself as well as constructor invocations, except ones from the factory methods.
*
* @return will never be {@literal null}.
*/
public List<Source> getSources() {
return sources;
}
/**
* Whether any sources exist at all.
*
* @see #getSources()
*/
public boolean hasSources() {
return !this.sources.isEmpty();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "EventType [type=" + type + ", sources=" + sources + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof EventType thaType)) {
return false;
}
return Objects.equals(sources, thaType.sources) //
&& Objects.equals(type, thaType.type);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(sources, type);
}
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -37,7 +34,6 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class FormatableType {
private static final Map<String, FormatableType> CACHE = new ConcurrentHashMap<>();
@@ -45,6 +41,21 @@ public class FormatableType {
private final String type;
private final Supplier<String> abbreviatedName;
/**
* Creates a new {@link FormatableType} for the given source {@link String} and lazily computed abbreviated name.
*
* @param type must not be {@literal null} or empty.
* @param abbreviatedName must not be {@literal null}.
*/
private FormatableType(String type, Supplier<String> abbreviatedName) {
Assert.hasText(type, "Type string must not be null or empty!");
Assert.notNull(abbreviatedName, "Computed abbreviated name must not be null!");
this.type = type;
this.abbreviatedName = abbreviatedName;
}
/**
* Creates a new {@link FormatableType} for the given {@link JavaClass}.
*

View File

@@ -17,20 +17,17 @@ package org.springframework.modulith.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import com.tngtech.archunit.base.DescribedIterable;
import com.tngtech.archunit.base.DescribedPredicate;
@@ -42,19 +39,27 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JavaPackage implements DescribedIterable<JavaClass> {
private static final String PACKAGE_INFO_NAME = "package-info";
private final @Getter String name;
private final String name;
private final Classes classes;
private final Classes packageClasses;
private final Supplier<Set<JavaPackage>> directSubPackages;
/**
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
*
* @param classes must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @param includeSubPackages
*/
private JavaPackage(Classes classes, String name, boolean includeSubPackages) {
Assert.notNull(classes, "Classes must not be null!");
Assert.hasText(name, "Name must not be null or empty!");
this.classes = classes;
this.packageClasses = classes.that(resideInAPackage(includeSubPackages ? name.concat("..") : name));
this.name = name;
@@ -67,22 +72,61 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.collect(Collectors.toSet()));
}
/**
* Creates a new {@link JavaPackage} for the given classes and name.
*
* @param classes must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @return
*/
public static JavaPackage of(Classes classes, String name) {
return new JavaPackage(classes, name, true);
}
/**
* Returns whether the given type is the {@code package-info.java} one.
*
* @param type must not be {@literal null}.
*/
public static boolean isPackageInfoType(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
}
/**
* Returns the name of the package.
*
* @return will never be {@literal null}.
*/
public String getName() {
return name;
}
/**
* Reduces the {@link JavaPackage} to only its base package.
*
* @return will never be {@literal null}.
*/
public JavaPackage toSingle() {
return new JavaPackage(classes, name, false);
}
/**
* Returns the local name of the package, i.e. the last segment of the qualified package name.
*
* @return will never be {@literal null}.
*/
public String getLocalName() {
return name.substring(name.lastIndexOf(".") + 1);
}
/**
* Returns all direct sub-packages of the current one.
*
* @return will never be {@literal null}.
*/
public Collection<JavaPackage> getDirectSubPackages() {
return directSubPackages.get();
}
@@ -91,32 +135,22 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
* Returns all classes residing in the current package and potentially in sub-packages if the current package was
* created to include them.
*
* @return
* @return will never be {@literal null}.
*/
public Classes getClasses() {
return packageClasses;
}
/**
* Extract the direct sub-package name of the given candidate.
* Returns all sub-packages that carry the given annotation type.
*
* @param candidate
* @return
* @param annotation must not be {@literal null}.
* @return will never be {@literal null}.
*/
private String extractDirectSubPackage(String candidate) {
if (candidate.length() <= name.length()) {
return candidate;
}
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
return candidate.substring(0, endIndex);
}
public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotation> annotation) {
Assert.notNull(annotation, "Annotation must not be null!");
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
.map(JavaClass::getPackageName) //
@@ -124,22 +158,59 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.map(it -> of(classes, it));
}
/**
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
*
* @param predicate must not be {@literal null}.
* @return
*/
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
Assert.notNull(predicate, "Predicate must not be null!");
return packageClasses.that(predicate);
}
/**
* Return whether the {@link JavaPackage} contains the given type.
*
* @param type must not be {@literal null}.
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return packageClasses.contains(type);
}
public boolean contains(String className) {
return packageClasses.contains(className);
/**
* Returns whether the {@link JavaPackage} contains the type with the given name.
*
* @param typeName must not be {@literal null} or empty.
*/
public boolean contains(String typeName) {
Assert.hasText(typeName, "Type name must not be null or empty!");
return packageClasses.contains(typeName);
}
/**
* Returns a {@link Stream} of all {@link JavaClass}es contained in the {@link JavaPackage}.
*
* @return will never be {@literal null}.
*/
public Stream<JavaClass> stream() {
return packageClasses.stream();
}
/**
* Return the annotation of the given type declared on the package.
*
* @param <A> the annotation type.
* @param annotationType the annotation type to be found.
* @return will never be {@literal null}.
*/
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
@@ -180,4 +251,52 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.append('\n') //
.toString();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof JavaPackage that)) {
return false;
}
return Objects.equals(this.classes, that.classes) //
&& Objects.equals(this.getDirectSubPackages(), that.getDirectSubPackages()) //
&& Objects.equals(this.name, that.name) //
&& Objects.equals(this.packageClasses, that.packageClasses);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(classes, directSubPackages, name, packageClasses);
}
/**
* Extract the direct sub-package name of the given candidate.
*
* @param candidate
* @return will never be {@literal null}.
*/
private String extractDirectSubPackage(String candidate) {
if (candidate.length() <= name.length()) {
return candidate;
}
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
return candidate.substring(0, endIndex);
}
}

View File

@@ -61,11 +61,20 @@ public interface ModulithMetadata {
}
/**
* Returns the source of the Moduliths setup. Either a type or a package.
* Returns the source of the Spring Modulith setup. Either a type or a package.
*
* @return will never be {@literal null}.
* @deprecated use {@link #getSource()} instead.
*/
@Deprecated(forRemoval = true)
Object getModulithSource();
/**
* Returns the source of the Spring Modulith setup. Either a type or a package.
*
* @return will never be {@literal null}.
*/
Object getModulithSource();
Object getSource();
/**
* Returns the names of the packages that are supposed to be considered modulith base packages, i.e. for which to

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@@ -32,21 +28,40 @@ import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.domain.properties.HasModifiers;
/**
* A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with
* {@link org.springframework.modulith.NamedInterface} or a set of types annotated with the same annotation. Other
* {@link ApplicationModules} can define allowed dependencies to particular named interfaces via the
* {@code $moduleName::$namedInterfaceName} syntax.
*
* @author Oliver Drotbohm
* @see org.springframework.modulith.ApplicationModule#allowedDependencies()
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class NamedInterface implements Iterable<JavaClass> {
private static final String UNNAMED_NAME = "<<UNNAMED>>";
private static final String PACKAGE_INFO_NAME = "package-info";
protected final @Getter String name;
protected final String name;
static NamedInterface unnamed(JavaPackage javaPackage) {
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
/**
* Creates a new {@link NamedInterface} with the given name.
*
* @param name must not be {@literal null} or empty.
*/
protected NamedInterface(String name) {
Assert.hasText(name, "Name must not be null or empty!");
this.name = name;
}
public static List<PackageBasedNamedInterface> of(JavaPackage javaPackage) {
/**
* Returns all {@link PackageBasedNamedInterface}s for the given {@link JavaPackage}.
*
* @param javaPackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static List<NamedInterface> of(JavaPackage javaPackage) {
String[] name = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
.map(it -> it.name()) //
@@ -54,33 +69,81 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
return Arrays.stream(name) //
.map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
.<NamedInterface> map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
.toList();
}
/**
* Returns a {@link TypeBasedNamedInterface} with the given name, {@link Classes} and base {@link JavaPackage}.
*
* @param name must not be {@literal null} or empty.
* @param classes must not be {@literal null}.
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
return new TypeBasedNamedInterface(name, classes, basePackage);
}
/**
* Creates an unnamed {@link NamedInterface} for the given {@link JavaPackage}.
*
* @param javaPackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterface unnamed(JavaPackage javaPackage) {
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
}
/**
* Returns the {@link NamedInterface}'s name.
*
* @return will never be {@literal null} or empty.
*/
public String getName() {
return name;
}
/**
* Returns whether this is the unnamed (implicit) {@link NamedInterface}.
*/
public boolean isUnnamed() {
return name.equals(UNNAMED_NAME);
}
/**
* Returns whether the {@link NamedInterface} contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "JavaClass must not be null!");
return getClasses().contains(type);
}
/**
* Returns whether the {@link NamedInterface} contains the given type.
*
* @param type must not be {@literal null}.
*/
public boolean contains(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
}
/**
* Returns whether the given {@link NamedInterface} has the same name as the current one.
*
* @param other
* @return
* @param other must not be {@literal null}.
*/
boolean hasSameNameAs(NamedInterface other) {
Assert.notNull(other, "NamedInterface must not be null!");
return this.name.equals(other.name);
}
@@ -93,13 +156,24 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
return getClasses().iterator();
}
/**
* Returns all {@link Classes} making up this {@link NamedInterface}.
*
* @return will never be {@literal null}.
*/
protected abstract Classes getClasses();
/**
* Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}.
*
* @param other must not be {@literal null}.
* @return will never be {@literal null}.
*/
public abstract NamedInterface merge(TypeBasedNamedInterface other);
static class PackageBasedNamedInterface extends NamedInterface {
private static class PackageBasedNamedInterface extends NamedInterface {
private final @Getter Classes classes;
private final Classes classes;
private final JavaPackage javaPackage;
public PackageBasedNamedInterface(String name, JavaPackage pkg) {
@@ -123,6 +197,15 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
this.javaPackage = pkg;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#getClasses()
*/
@Override
public Classes getClasses() {
return classes;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
@@ -143,18 +226,38 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
}
}
static class TypeBasedNamedInterface extends NamedInterface {
public static class TypeBasedNamedInterface extends NamedInterface {
private final @Getter Classes classes;
private final Classes classes;
private final JavaPackage pkg;
/**
* Creates a new {@link TypeBasedNamedInterface} with the given name, {@link Classes} and {@link JavaPackage}.
*
* @param name must not be {@literal null} or empty.
* @param types must not be {@literal null}.
* @param pkg must not be {@literal null}.
*/
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
super(name);
Assert.notNull(types, "Classes must not be null!");
Assert.notNull(pkg, "JavaPackage must not be null!");
this.classes = types;
this.pkg = pkg;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#getClasses()
*/
@Override
public Classes getClasses() {
return classes;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -28,112 +25,101 @@ import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* A collection of {@link NamedInterface}s.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class NamedInterfaces implements Iterable<NamedInterface> {
public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList());
private final List<NamedInterface> namedInterfaces;
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
/**
* Creates a new {@link NamedInterfaces} for all {@link NamedInterface}s.
*
* @param namedInterfaces must not be {@literal null}.
*/
private NamedInterfaces(List<NamedInterface> namedInterfaces) {
Assert.notNull(namedInterfaces, "Named interfaces must not be null!");
this.namedInterfaces = namedInterfaces;
}
/**
* Discovers all {@link NamedInterfaces} declared for the given {@link JavaPackage}.
*
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
.and(NamedInterface.unnamed(basePackage));
}
public static NamedInterfaces of(List<NamedInterface> interfaces) {
/**
* Creates a new {@link NamedInterfaces} for the given {@link NamedInterface}s.
*
* @param interfaces must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces of(List<NamedInterface> interfaces) {
return interfaces.isEmpty() ? NONE : new NamedInterfaces(interfaces);
}
/**
* Creates a new {@link NamedInterfaces} for the given base {@link JavaPackage}.
*
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces ofAnnotatedPackages(JavaPackage basePackage) {
Assert.notNull(basePackage, "Base package must not be null!");
return basePackage //
.getSubPackagesAnnotatedWith(org.springframework.modulith.NamedInterface.class) //
.flatMap(it -> NamedInterface.of(it).stream()) //
.collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of));
}
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
MultiValueMap<String, JavaClass> mappings = new LinkedMultiValueMap<>();
basePackage.stream() //
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
.forEach(it -> {
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
return;
}
org.springframework.modulith.NamedInterface annotation = AnnotatedElementUtils
.getMergedAnnotation(it.reflect(), org.springframework.modulith.NamedInterface.class);
for (String name : annotation.name()) {
mappings.add(name, it);
}
});
return mappings.entrySet().stream() //
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
.toList();
}
private NamedInterfaces and(NamedInterface namedInterface) {
List<NamedInterface> result = new ArrayList<>(namedInterfaces.size() + 1);
result.addAll(namedInterfaces);
result.add(namedInterface);
return new NamedInterfaces(result);
}
/**
* Returns whether at least one explicit {@link NamedInterface} is declared.
*
* @return will never be {@literal null}.
*/
public boolean hasExplicitInterfaces() {
return namedInterfaces.size() > 1 || !namedInterfaces.get(0).isUnnamed();
}
/**
* Create a {@link Stream} of {@link NamedInterface}s.
*
* @return will never be {@literal null}.
*/
public Stream<NamedInterface> stream() {
return namedInterfaces.stream();
}
public NamedInterfaces and(List<TypeBasedNamedInterface> others) {
List<NamedInterface> namedInterfaces = new ArrayList<>();
List<NamedInterface> unmergedInterface = this.namedInterfaces;
for (TypeBasedNamedInterface candidate : others) {
Optional<NamedInterface> existing = namedInterfaces.stream() //
.filter(it -> it.hasSameNameAs(candidate)) //
.findFirst();
// Merge existing with new and add to result
existing.ifPresent(it -> {
namedInterfaces.add(it.merge(candidate));
namedInterfaces.add(it);
unmergedInterface.remove(it);
});
// Simply add candidate
if (!existing.isPresent()) {
namedInterfaces.add(candidate);
}
}
namedInterfaces.addAll(unmergedInterface);
return new NamedInterfaces(namedInterfaces);
}
/**
* Returns the {@link NamedInterface} with the given name if present.
*
* @param name must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public Optional<NamedInterface> getByName(String name) {
Assert.hasText(name, "Named interface name must not be null or empty!");
return namedInterfaces.stream().filter(it -> it.getName().equals(name)).findFirst();
}
@@ -158,4 +144,79 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
public Iterator<NamedInterface> iterator() {
return namedInterfaces.iterator();
}
/**
* Creates a new {@link NamedInterfaces} instance with the given {@link TypeBasedNamedInterface}s added.
*
* @param others must not be {@literal null}.
* @return will never be {@literal null}.
*/
NamedInterfaces and(List<TypeBasedNamedInterface> others) {
Assert.notNull(others, "Other TypeBasedNamedInterfaces must not be null!");
var namedInterfaces = new ArrayList<NamedInterface>();
var unmergedInterface = this.namedInterfaces;
if (others.isEmpty()) {
return this;
}
for (TypeBasedNamedInterface candidate : others) {
var existing = namedInterfaces.stream() //
.filter(it -> it.hasSameNameAs(candidate)) //
.findFirst();
// Merge existing with new and add to result
existing.ifPresent(it -> {
namedInterfaces.add(it.merge(candidate));
namedInterfaces.add(it);
unmergedInterface.remove(it);
});
// Simply add candidate
if (!existing.isPresent()) {
namedInterfaces.add(candidate);
}
}
namedInterfaces.addAll(unmergedInterface);
return new NamedInterfaces(namedInterfaces);
}
private NamedInterfaces and(NamedInterface namedInterface) {
var result = new ArrayList<NamedInterface>(namedInterfaces.size() + 1);
result.addAll(namedInterfaces);
result.add(namedInterface);
return new NamedInterfaces(result);
}
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
var mappings = new LinkedMultiValueMap<String, JavaClass>();
basePackage.stream() //
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
.forEach(it -> {
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
return;
}
var annotation = AnnotatedElementUtils.getMergedAnnotation(it.reflect(),
org.springframework.modulith.NamedInterface.class);
for (String name : annotation.name()) {
mappings.add(name, it);
}
});
return mappings.entrySet().stream() //
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
.toList();
}
}

View File

@@ -15,12 +15,10 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Objects;
import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.JavaClass;
@@ -29,17 +27,49 @@ import com.tngtech.archunit.core.domain.JavaClass;
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE)
public class SpringBean {
private final @Getter JavaClass type;
private final JavaClass type;
private final ApplicationModule module;
/**
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
*
* @param type must not be {@literal null}.
* @param module must not be {@literal null}.
*/
private SpringBean(JavaClass type, ApplicationModule module) {
Assert.notNull(type, "JavaClass must not be null!");
Assert.notNull(module, "ApplicationModule must not be null!");
this.type = type;
this.module = module;
}
/**
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
*
* @param type must not be {@literal null}.
* @param module must not be {@literal null}.
*/
static SpringBean of(JavaClass type, ApplicationModule module) {
return new SpringBean(type, module);
}
/**
* Returns the {@link JavaClass} of the {@link SpringBean}.
*
* @return will never be {@literal null}.
*/
public JavaClass getType() {
return type;
}
/**
* Returns the fully-qualified name of the Spring bean type.
*
* @return
* @return will never be {@literal null} or empty.
*/
public String getFullyQualifiedTypeName() {
return type.getFullName();
@@ -56,9 +86,9 @@ public class SpringBean {
}
/**
* Returns all interfaces implemented by the bean that are part of the same module.
* Returns all interfaces implemented by the bean that are part of the same application module.
*
* @return
* @return will never be {@literal null}.
*/
public List<JavaClass> getInterfacesWithinModule() {
@@ -67,11 +97,40 @@ public class SpringBean {
.toList();
}
public boolean isAnnotatedWith(Class<?> type) {
return Types.isAnnotatedWith(type).test(this.type);
}
/**
* Creates a new {@link ArchitecturallyEvidentType} from the current {@link SpringBean}.
*
* @return
*/
public ArchitecturallyEvidentType toArchitecturallyEvidentType() {
return ArchitecturallyEvidentType.of(type, module.getSpringBeansInternal());
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SpringBean that)) {
return false;
}
return Objects.equals(this.module, that.module) //
&& Objects.equals(this.type, that.type);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(type, module);
}
}

View File

@@ -17,8 +17,6 @@ package org.springframework.modulith.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.experimental.UtilityClass;
import java.lang.annotation.Annotation;
import org.springframework.lang.Nullable;
@@ -33,12 +31,11 @@ import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
/**
* @author Oliver Drotbohm
*/
@UtilityClass
class Types {
@Nullable
@SuppressWarnings("unchecked")
<T> Class<T> loadIfPresent(String name) {
static <T> Class<T> loadIfPresent(String name) {
ClassLoader loader = Types.class.getClassLoader();
@@ -79,7 +76,6 @@ class Types {
}
}
@UtilityClass
static class JavaXTypes {
private static final String BASE_PACKAGE = "jakarta";
@@ -93,7 +89,6 @@ class Types {
}
}
@UtilityClass
static class SpringTypes {
private static final String BASE_PACKAGE = "org.springframework";
@@ -129,7 +124,6 @@ class Types {
}
}
@UtilityClass
static class SpringDataTypes {
private static final String BASE_PACKAGE = SpringTypes.BASE_PACKAGE + ".data";
@@ -147,11 +141,11 @@ class Types {
}
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
return isAnnotatedWith(type.getName());
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.annotatedWith(type) //
.or(Predicates.metaAnnotatedWith(type));
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -32,7 +29,6 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
public class Violations extends RuntimeException {
private static final long serialVersionUID = 6863781504675034691L;
@@ -41,13 +37,25 @@ public class Violations extends RuntimeException {
private final List<RuntimeException> exceptions;
/**
* Creates a new {@link Violations} from the given {@link RuntimeException}s.
*
* @param exceptions must not be {@literal null}.
*/
private Violations(List<RuntimeException> exceptions) {
Assert.notNull(exceptions, "Exceptions must not be null!");
this.exceptions = exceptions;
}
/**
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
*
* @return will never be {@literal null}.
*/
static Collector<RuntimeException, ?, Violations> toViolations() {
return Collectors.collectingAndThen(Collectors.toList(), Violations::of);
return Collectors.collectingAndThen(Collectors.toList(), Violations::new);
}
/*

View File

@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
@@ -47,11 +46,11 @@ class ModuleDetectionStrategyUnitTest {
@Test
void detectsJMoleculesAnnotatedModule() {
JavaClasses classes = new ClassFileImporter() //
var classes = new ClassFileImporter() //
.withImportOption(new ImportOption.OnlyIncludeTests()) //
.importPackages("jmolecules");
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
var javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
assertThat(ApplicationModuleDetectionStrategy.explictlyAnnotated().getModuleBasePackages(javaPackage))
.containsExactly(javaPackage);

View File

@@ -27,7 +27,6 @@ import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.modulith.docs.ConfigurationProperties.ModuleProperty;
import org.springframework.modulith.docs.Documenter.CanvasOptions;
import org.springframework.modulith.docs.Documenter.CanvasOptions.Groupings;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModuleDependency;
import org.springframework.modulith.model.ApplicationModules;
@@ -42,7 +41,6 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaModifier;
/**
@@ -99,10 +97,9 @@ class Asciidoctor {
*/
public String toInlineCode(String source) {
String[] parts = source.split("#");
String type = parts[0];
Optional<String> methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.empty();
var parts = source.split("#");
var type = parts[0];
var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.<String> empty();
return modules.getModuleByType(type)
.flatMap(it -> it.getType(type))
@@ -116,15 +113,14 @@ class Asciidoctor {
public String toInlineCode(SpringBean bean) {
String base = toInlineCode(bean.toArchitecturallyEvidentType());
List<JavaClass> interfaces = bean.getInterfacesWithinModule();
var base = toInlineCode(bean.toArchitecturallyEvidentType());
var interfaces = bean.getInterfacesWithinModule();
if (interfaces.isEmpty()) {
return base;
}
String interfacesAsString = interfaces.stream() //
var interfacesAsString = interfaces.stream() //
.map(this::toInlineCode) //
.collect(Collectors.joining(", "));
@@ -133,8 +129,8 @@ class Asciidoctor {
public String renderSpringBeans(ApplicationModule module, CanvasOptions options) {
StringBuilder builder = new StringBuilder();
Groupings groupings = options.groupBeans(module);
var builder = new StringBuilder();
var groupings = options.groupBeans(module);
if (groupings.hasOnlyFallbackGroup()) {
return toBulletPoints(groupings.byGrouping(CanvasOptions.FALLBACK_GROUP));
@@ -166,13 +162,13 @@ class Asciidoctor {
public String renderEvents(ApplicationModule module) {
List<EventType> events = module.getPublishedEvents();
var events = module.getPublishedEvents();
if (events.isEmpty()) {
return "none";
}
StringBuilder builder = new StringBuilder();
var builder = new StringBuilder();
for (EventType eventType : events) {
@@ -205,12 +201,12 @@ class Asciidoctor {
Stream<String> stream = properties.stream()
.map(it -> {
StringBuilder builder = new StringBuilder()
.append(toCode(it.getName()))
var builder = new StringBuilder()
.append(toCode(it.name()))
.append(" -- ")
.append(toInlineCode(it.getType()));
.append(toInlineCode(it.type()));
String defaultValue = it.getDefaultValue();
var defaultValue = it.defaultValue();
if (defaultValue != null && StringUtils.hasText(defaultValue)) {
@@ -219,7 +215,7 @@ class Asciidoctor {
.append("");
}
String description = it.getDescription();
var description = it.description();
if (description != null && StringUtils.hasText(description)) {
builder = builder.append(". ")
@@ -257,8 +253,8 @@ class Asciidoctor {
private String toOptionalLink(JavaClass source, Optional<String> methodSignature) {
ApplicationModule module = modules.getModuleByType(source).orElse(null);
String typeAndMethod = toCode(
var module = modules.getModuleByType(source).orElse(null);
var typeAndMethod = toCode(
toTypeAndMethod(FormatableType.of(source).getAbbreviatedFullName(module), methodSignature));
if (module == null
@@ -267,7 +263,7 @@ class Asciidoctor {
return typeAndMethod;
}
String classPath = convertClassNameToResourcePath(source.getFullName()) //
var classPath = convertClassNameToResourcePath(source.getFullName()) //
.replace('$', '.');
return Optional.ofNullable(javaDocBase == PLACEHOLDER ? null : javaDocBase) //
@@ -288,7 +284,7 @@ class Asciidoctor {
if (!docSource.isPresent()) {
Stream<JavaClass> referenceTypes = type.getReferenceTypes();
var referenceTypes = type.getReferenceTypes();
return String.format("%s listening to %s", //
toInlineCode(type.getType()), //
@@ -299,14 +295,14 @@ class Asciidoctor {
return header + type.getReferenceMethods().map(it -> {
JavaMethod method = it.getMethod();
var method = it.getMethod();
Assert.isTrue(method.getRawParameterTypes().size() > 0,
() -> String.format("Method %s must have at least one parameter!", method));
JavaClass parameterType = it.getMethod().getRawParameterTypes().get(0);
String isAsync = it.isAsync() ? "(async) " : "";
var parameterType = method.getRawParameterTypes().get(0);
var isAsync = it.isAsync() ? "(async) " : "";
return docSource.flatMap(source -> source.getDocumentation(it.getMethod()))
return docSource.flatMap(source -> source.getDocumentation(method))
.map(doc -> String.format("** %s %s-- %s", toInlineCode(parameterType), isAsync, doc))
.orElseGet(() -> String.format("** %s %s", toInlineCode(parameterType), isAsync));

View File

@@ -15,10 +15,10 @@
*/
package org.springframework.modulith.docs;
import lombok.RequiredArgsConstructor;
import java.util.Optional;
import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.JavaMethod;
/**
@@ -27,12 +27,27 @@ import com.tngtech.archunit.core.domain.JavaMethod;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
class CodeReplacingDocumentationSource implements DocumentationSource {
private final DocumentationSource delegate;
private final Asciidoctor codeSource;
/**
* Creates a new {@link CodeReplacingDocumentationSource} for the given delegate {@link DocumentationSource} and
* {@link Asciidoctor} instance.
*
* @param delegate must not be {@literal null}.
* @param asciidoctor must not be {@literal null}.
*/
CodeReplacingDocumentationSource(DocumentationSource delegate, Asciidoctor asciidoctor) {
Assert.notNull(delegate, "Delegate DocumentationSource must not be null!");
Assert.notNull(asciidoctor, "Asciidoctor must not be null!");
this.delegate = delegate;
this.codeSource = asciidoctor;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.docs.DocumentationSource#getDocumentation(com.tngtech.archunit.core.domain.JavaMethod)

View File

@@ -15,15 +15,12 @@
*/
package org.springframework.modulith.docs;
import lombok.Value;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.io.Resource;
@@ -34,7 +31,6 @@ import org.springframework.modulith.model.ApplicationModule;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.tngtech.archunit.core.domain.JavaType;
@@ -55,10 +51,11 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
*/
ConfigurationProperties() {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
var resolver = new PathMatchingResourcePatternResolver();
try {
Resource[] resources = resolver.getResources(METADATA_PATH);
var resources = resolver.getResources(METADATA_PATH);
this.properties = Arrays.stream(resources)
.flatMap(ConfigurationProperties::parseProperties)
@@ -96,9 +93,9 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
private Stream<ModuleProperty> getModuleProperty(ApplicationModule module,
ConfigurationProperty property) {
return module.getType(property.getSourceType())
.map(it -> new ModuleProperty(property.getName(), property.getDescription(), property.getType(), it,
property.getDefaultValue()))
return module.getType(property.sourceType)
.map(it -> new ModuleProperty(property.name(), property.description(), property.type(), it,
property.defaultValue()))
.map(Stream::of)
.orElseGet(Stream::empty);
}
@@ -112,7 +109,7 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
try (InputStream stream = source.getInputStream()) {
DocumentContext context = JsonPath.parse(stream);
var context = JsonPath.parse(stream);
List<Object> read = context.read(PATH, List.class);
return read.stream()
@@ -124,13 +121,8 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
}
}
@Value
static class ConfigurationProperty {
String name;
@Nullable String description;
String type, sourceType;
@Nullable String defaultValue;
static record ConfigurationProperty(String name, @Nullable String description, String type, String sourceType,
@Nullable String defaultValue) {
@SuppressWarnings("null")
static Stream<ConfigurationProperty> of(Map<String, Object> source) {
@@ -162,12 +154,6 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
}
}
@Value
static class ModuleProperty {
String name;
@Nullable String description;
String type;
JavaType sourceType;
@Nullable String defaultValue;
}
static record ModuleProperty(String name, @Nullable String description, String type, JavaType sourceType,
@Nullable String defaultValue) {}
}

View File

@@ -17,13 +17,6 @@ package org.springframework.modulith.docs;
import static org.springframework.modulith.docs.Asciidoctor.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.With;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
@@ -77,11 +70,9 @@ import com.tngtech.archunit.core.domain.JavaClass;
*
* @author Oliver Drotbohm
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Documenter {
private static final Map<DependencyType, String> DEPENDENCY_DESCRIPTIONS = new LinkedHashMap<>();
private static final String INVALID_FILE_NAME_PATTERN = "Configured file name pattern does not include a '%s' placeholder for the module name!";
static {
@@ -89,7 +80,7 @@ public class Documenter {
DEPENDENCY_DESCRIPTIONS.put(DependencyType.DEFAULT, "depends on");
}
private final @Getter ApplicationModules modules;
private final ApplicationModules modules;
private final Workspace workspace;
private final Container container;
private final ConfigurationProperties properties;
@@ -98,7 +89,8 @@ public class Documenter {
private Map<ApplicationModule, Component> components;
/**
* Creates a new {@link Documenter} for the {@link ApplicationModules} created for the given modulith type.
* Creates a new {@link Documenter} for the {@link ApplicationModules} created for the given modulith type in the
* default output folder ({@code spring-modulith-docs}).
*
* @param modulithType must not be {@literal null}.
*/
@@ -107,7 +99,8 @@ public class Documenter {
}
/**
* Creates a new {@link Documenter} for the given {@link ApplicationModules} instance.
* Creates a new {@link Documenter} for the given {@link ApplicationModules} instance in the default output folder
* ({@code spring-modulith-docs}).
*
* @param modules must not be {@literal null}.
*/
@@ -115,7 +108,13 @@ public class Documenter {
this(modules, getDefaultOutputDirectory());
}
private Documenter(ApplicationModules modules, String outputFolder) {
/**
* Creates a new {@link Documenter} for the given {@link ApplicationModules} and output folder.
*
* @param modules must not be {@literal null}.
* @param outputFolder must not be {@literal null} or empty.
*/
public Documenter(ApplicationModules modules, String outputFolder) {
Assert.notNull(modules, "Modules must not be null!");
Assert.hasText(outputFolder, "Output folder must not be null or empty!");
@@ -142,11 +141,12 @@ public class Documenter {
* Customize the output folder to write the generated files to. Defaults to {@value #DEFAULT_LOCATION}.
*
* @param outputFolder must not be {@literal null} or empty.
* @return the current instance, will never be {@literal null}.
* @see #DEFAULT_LOCATION
* @return will never be {@literal null}.
* @deprecated use {@link Documenter(ApplicationModules, String)} directly.
*/
@Deprecated(forRemoval = true)
public Documenter withOutputFolder(String outputFolder) {
return new Documenter(modules, workspace, container, properties, outputFolder, components);
return new Documenter(modules, outputFolder);
}
/**
@@ -258,12 +258,12 @@ public class Documenter {
Assert.notNull(module, "Module must not be null!");
Assert.notNull(options, "Options must not be null!");
ComponentView view = createComponentView(options, module);
view.setTitle(options.getDefaultDisplayName().apply(module));
var view = createComponentView(options, module);
view.setTitle(options.defaultDisplayName.apply(module));
addComponentsToView(module, view, options);
String fileNamePattern = options.getTargetFileName().orElse("module-%s.uml");
var fileNamePattern = options.getTargetFileName().orElse("module-%s.uml");
Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern));
@@ -291,8 +291,8 @@ public class Documenter {
modules.forEach(module -> {
String filename = String.format(options.getTargetFileName().orElse("module-%s.adoc"), module.getName());
Path file = recreateFile(filename);
var filename = options.getTargetFileName(module.getName());
var file = recreateFile(filename);
try (FileWriter writer = new FileWriter(file.toFile())) {
@@ -347,16 +347,8 @@ public class Documenter {
.toString();
}
private static String addTableRow(String title, String content, CanvasOptions options) {
return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none"))
? ""
: writeTableRow(title, content);
}
private static <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper,
CanvasOptions options) {
return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
ApplicationModules getModules() {
return modules;
}
String toPlantUml() {
@@ -387,7 +379,7 @@ public class Documenter {
this.components = modules.stream() //
.collect(Collectors.toMap(Function.identity(),
it -> container.addComponent(options.getDefaultDisplayName().apply(it), "", "Module")));
it -> container.addComponent(options.defaultDisplayName.apply(it), "", "Module")));
this.components.forEach((key, value) -> addDependencies(key, value, options));
}
@@ -398,7 +390,7 @@ public class Documenter {
private void addComponentsToView(ApplicationModule module, ComponentView view, DiagramOptions options) {
Supplier<Stream<ApplicationModule>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
options.getDependencyDepth());
options.dependencyDepth);
Supplier<Stream<ApplicationModule>> otherDependencies = () -> options.getDependencyTypes()
.flatMap(it -> module.getDependencies(modules, it).stream()
.map(ApplicationModuleDependency::getTargetModule));
@@ -413,14 +405,14 @@ public class Documenter {
DiagramOptions options,
Consumer<ComponentView> afterCleanup) {
Styles styles = view.getViewSet().getConfiguration().getStyles();
Map<ApplicationModule, Component> components = getComponents(options);
var styles = view.getViewSet().getConfiguration().getStyles();
var components = getComponents(options);
modules.get() //
.distinct()
.filter(options.getExclusions().negate()) //
.filter(options.exclusions.negate()) //
.map(it -> applyBackgroundColor(it, components, options, styles)) //
.filter(options.getComponentFilter()) //
.filter(options.componentFilter) //
.forEach(view::add);
// Remove filtered dependency types
@@ -431,7 +423,7 @@ public class Documenter {
afterCleanup.accept(view);
// Filter outgoing relationships of target-only modules
modules.get().filter(options.getTargetOnly()) //
modules.get().filter(options.targetOnly) //
.forEach(module -> {
Component component = components.get(module);
@@ -467,31 +459,6 @@ public class Documenter {
.findFirst().ifPresent(view::remove);
}
private static Component applyBackgroundColor(ApplicationModule module,
Map<ApplicationModule, Component> components,
DiagramOptions options,
Styles styles) {
Component component = components.get(module);
Function<ApplicationModule, Optional<String>> selector = options.getColorSelector();
// Apply custom color if configured
selector.apply(module).ifPresent(color -> {
String tag = module.getName() + "-" + color;
component.addTags(tag);
// Add or update background color
styles.getElements().stream()
.filter(it -> it.getTag().equals(tag))
.findFirst()
.orElseGet(() -> styles.addElementStyle(tag))
.background(color);
});
return component;
}
private Documenter writeViewAsPlantUml(ComponentView view, String filename, DiagramOptions options) {
Path file = recreateFile(filename);
@@ -566,6 +533,43 @@ public class Documenter {
}
}
private static Component applyBackgroundColor(ApplicationModule module,
Map<ApplicationModule, Component> components,
DiagramOptions options,
Styles styles) {
var component = components.get(module);
var selector = options.colorSelector;
// Apply custom color if configured
selector.apply(module).ifPresent(color -> {
var tag = module.getName() + "-" + color;
component.addTags(tag);
// Add or update background color
styles.getElements().stream()
.filter(it -> it.getTag().equals(tag))
.findFirst()
.orElseGet(() -> styles.addElementStyle(tag))
.background(color);
});
return component;
}
private static String addTableRow(String title, String content, CanvasOptions options) {
return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none"))
? ""
: writeTableRow(title, content);
}
private static <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper,
CanvasOptions options) {
return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
}
/**
* Returns the default output directory based on the detected build system.
*
@@ -575,11 +579,7 @@ public class Documenter {
return (new File("pom.xml").exists() ? "target" : "build").concat("/spring-modulith-docs");
}
@Value
private static class Connection {
Element source, target;
private static record Connection(Element source, Element target) {
public static Connection of(Relationship relationship) {
return new Connection(relationship.getSource(), relationship.getDestination());
}
@@ -590,58 +590,130 @@ public class Documenter {
*
* @author Oliver Drotbohm
*/
@Getter(AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class DiagramOptions {
private static final Set<DependencyType> ALL_TYPES = Arrays.stream(DependencyType.values())
.collect(Collectors.toSet());
private final Set<DependencyType> dependencyTypes;
private final DependencyDepth dependencyDepth;
private final Predicate<ApplicationModule> exclusions;
private final Predicate<Component> componentFilter;
private final Predicate<ApplicationModule> targetOnly;
private final @Nullable String targetFileName;
private final Function<ApplicationModule, Optional<String>> colorSelector;
private final Function<ApplicationModule, String> defaultDisplayName;
private final DiagramStyle style;
private final ElementsWithoutRelationships elementsWithoutRelationships;
/**
* @param dependencyTypes must not be {@literal null}.
* @param dependencyDepth must not be {@literal null}.
* @param exclusions must not be {@literal null}.
* @param componentFilter must not be {@literal null}.
* @param targetOnly must not be {@literal null}.
* @param targetFileName can be {@literal null}.
* @param colorSelector must not be {@literal null}.
* @param defaultDisplayName must not be {@literal null}.
* @param style must not be {@literal null}.
* @param elementsWithoutRelationships must not be {@literal null}.
*/
DiagramOptions(Set<DependencyType> dependencyTypes, DependencyDepth dependencyDepth,
Predicate<ApplicationModule> exclusions, Predicate<Component> componentFilter,
Predicate<ApplicationModule> targetOnly, @Nullable String targetFileName,
Function<ApplicationModule, Optional<String>> colorSelector,
Function<ApplicationModule, String> defaultDisplayName, DiagramStyle style,
ElementsWithoutRelationships elementsWithoutRelationships) {
Assert.notNull(dependencyTypes, "Dependency types must not be null!");
Assert.notNull(dependencyDepth, "Dependency depth must not be null!");
Assert.notNull(exclusions, "Exclusions must not be null!");
Assert.notNull(componentFilter, "Component filter must not be null!");
Assert.notNull(targetOnly, "Target only must not be null!");
Assert.notNull(colorSelector, "Color selector must not be null!");
Assert.notNull(defaultDisplayName, "Default display name must not be null!");
Assert.notNull(style, "DiagramStyle must not be null!");
Assert.notNull(elementsWithoutRelationships, "ElementsWithoutRelationships must not be null!");
this.dependencyTypes = dependencyTypes;
this.dependencyDepth = dependencyDepth;
this.exclusions = exclusions;
this.componentFilter = componentFilter;
this.targetOnly = targetOnly;
this.targetFileName = targetFileName;
this.colorSelector = colorSelector;
this.defaultDisplayName = defaultDisplayName;
this.style = style;
this.elementsWithoutRelationships = elementsWithoutRelationships;
}
/**
* The {@link DependencyDepth} to define which other modules to be included in the diagram to be created.
*/
private final @With DependencyDepth dependencyDepth;
public DiagramOptions withDependencyDepth(DependencyDepth dependencyDepth) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* A {@link Predicate} to define the which modules to exclude from the diagram to be created.
*/
private final @With Predicate<ApplicationModule> exclusions;
public DiagramOptions withExcusions(Predicate<ApplicationModule> exclusions) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be created.
*/
private final @With Predicate<Component> componentFilter;
public DiagramOptions withComponentFilter(Predicate<Component> componentFilter) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* A {@link Predicate} to define which of the modules shall only be considered targets, i.e. all efferent
* relationships are going to be hidden from the rendered view. Modules that have no incoming relationships will
* entirely be removed from the view.
*/
private final @With Predicate<ApplicationModule> targetOnly;
public DiagramOptions withTargetOnly(Predicate<ApplicationModule> targetOnly) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* The target file name to be used for the diagram to be created. For individual module diagrams this needs to
* include a {@code %s} placeholder for the module names.
*/
private final @With @Nullable String targetFileName;
public DiagramOptions withTargetFileName(String targetFileName) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* A callback to return a hex-encoded color per {@link ApplicationModule}.
*/
private final @With Function<ApplicationModule, Optional<String>> colorSelector;
public DiagramOptions withColorSelector(Function<ApplicationModule, Optional<String>> colorSelector) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* A callback to return a default display names for a given {@link ApplicationModule}. Default implementation just
* forwards to {@link ApplicationModule#getDisplayName()}.
*/
private final @With Function<ApplicationModule, String> defaultDisplayName;
public DiagramOptions withDefaultDisplayName(Function<ApplicationModule, String> defaultDisplayName) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* Which style to render the diagram in. Defaults to {@value DiagramStyle#UML}.
*/
private final @With DiagramStyle style;
public DiagramOptions withStyle(DiagramStyle style) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* Configuration setting to define whether modules that do not have a relationship to any other module shall be
@@ -651,7 +723,10 @@ public class Documenter {
*
* @see #withExclusions(Predicate)
*/
private final @With ElementsWithoutRelationships elementsWithoutRelationships;
public DiagramOptions withElementsWithoutRelationships(ElementsWithoutRelationships elementsWithoutRelationships) {
return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly,
targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
}
/**
* Creates a new default {@link DiagramOptions} instance configured to use all dependency types, list immediate
@@ -729,17 +804,23 @@ public class Documenter {
}
}
// Prefix required for javac 🤔
@lombok.RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public static class CanvasOptions {
static final Grouping FALLBACK_GROUP = Grouping.of("Others", null, __ -> true);
static final Grouping FALLBACK_GROUP = new Grouping("Others", __ -> true, null);
private final List<Grouping> groupers;
private final @With @Getter @Nullable String apiBase;
private final @With @Nullable String targetFileName;
private final boolean hideInternals;
private final boolean hideEmptyLines;
private final @Nullable String apiBase, targetFileName;
private final boolean hideInternals, hideEmptyLines;
CanvasOptions(List<Grouping> groupers, @Nullable String apiBase, @Nullable String targetFileName,
boolean hideInternals, boolean hideEmptyLines) {
this.groupers = groupers;
this.apiBase = apiBase;
this.targetFileName = targetFileName;
this.hideInternals = hideInternals;
this.hideEmptyLines = hideEmptyLines;
}
public static CanvasOptions defaults() {
@@ -758,14 +839,38 @@ public class Documenter {
public CanvasOptions groupingBy(Grouping... groupings) {
List<Grouping> result = new ArrayList<>(groupers);
result.addAll(Arrays.asList(groupings));
var result = new ArrayList<>(groupers);
result.addAll(List.of(groupings));
return new CanvasOptions(result, apiBase, targetFileName, hideInternals, hideEmptyLines);
}
/**
* Registers a component grouping with the given name and selecting filter.
*
* @param name must not be {@literal null} or empty.
* @param filter must not be {@literal null}.
* @return will never be {@literal null}.
*/
public CanvasOptions groupingBy(String name, Predicate<SpringBean> filter) {
return groupingBy(Grouping.of(name, null, filter));
return groupingBy(Grouping.of(name, filter, null));
}
/**
* Registers a component grouping with the given name, selecting filter and description.
*
* @param name must not be {@literal null} or empty.
* @param filter must not be {@literal null}.
* @param description must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public CanvasOptions groupingBy(String name, Predicate<SpringBean> filter, String description) {
Assert.hasText(name, "Name must not be null!");
Assert.notNull(filter, "Filter must not be null!");
Assert.hasText(description, "Description must not be null!");
return groupingBy(Grouping.of(name, filter, description));
}
/**
@@ -777,21 +882,55 @@ public class Documenter {
return new CanvasOptions(groupers, apiBase, targetFileName, false, hideEmptyLines);
}
/**
* Enables table rows not containing any values to be retained in the output. By default, no table rows for e.g.
* aggregates will be rendered if none are found in the {@link ApplicationModule}.
*
* @return will never be {@literal null}.
*/
public CanvasOptions revealEmptyLines() {
return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, false);
}
/**
* Configures a URI string to act as the base of the Javadoc accessible for the types contained in the canvas. If
* set, the output will add links to the Javadoc for those types.
*
* @param apiBase must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public CanvasOptions withApiBase(String apiBase) {
Assert.hasText(apiBase, "API base must not be null or empty!");
return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, hideEmptyLines);
}
/**
* Configures the target file name for the canvas to be written. Defaults to {@code module-$moduleName.adoc}.
*
* @param targetFileName must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public CanvasOptions withTargetFileName(String targetFileName) {
return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, hideEmptyLines);
}
String getApiBase() {
return apiBase;
}
Groupings groupBeans(ApplicationModule module) {
List<Grouping> sources = new ArrayList<>(groupers);
var sources = new ArrayList<Grouping>(groupers);
sources.add(FALLBACK_GROUP);
MultiValueMap<Grouping, SpringBean> result = new LinkedMultiValueMap<>();
List<SpringBean> alreadyMapped = new ArrayList<>();
var result = new LinkedMultiValueMap<Grouping, SpringBean>();
var alreadyMapped = new ArrayList<SpringBean>();
sources.forEach(it -> {
List<SpringBean> matchingBeans = getMatchingBeans(module, it, alreadyMapped);
var matchingBeans = getMatchingBeans(module, it, alreadyMapped);
result.addAll(it, matchingBeans);
alreadyMapped.addAll(matchingBeans);
@@ -804,15 +943,15 @@ public class Documenter {
}
});
return Groupings.of(result);
return new Groupings(result);
}
Predicate<JavaClass> hideInternalFilter(ApplicationModule module) {
return hideInternals ? module::isExposed : __ -> true;
}
private Optional<String> getTargetFileName() {
return Optional.ofNullable(targetFileName);
private String getTargetFileName(String moduleName) {
return (targetFileName == null ? "module-%s.adoc" : targetFileName).formatted(moduleName);
}
private List<SpringBean> getMatchingBeans(ApplicationModule module, Grouping filter,
@@ -825,45 +964,149 @@ public class Documenter {
.toList();
}
@Value(staticConstructor = "of")
@Getter(AccessLevel.PACKAGE)
public static class Grouping {
String name;
@Nullable String description;
Predicate<SpringBean> predicate;
private final String name;
private final Predicate<SpringBean> predicate;
private final @Nullable String description;
/**
* Creates a new {@link Grouping} for the given {@link Predicate} and description.
*
* @param name must not be {@literal null} or empty.
* @param predicate must not be {@literal null}.
* @param description can be {@literal null}.
*/
private Grouping(String name, Predicate<SpringBean> predicate, @Nullable String description) {
Assert.hasText(name, "Name must not be null or empty!");
Assert.notNull(predicate, "Predicate must not be null!");
Assert.isTrue(description == null || !description.isBlank(), "Description must not be empty or null!");
this.name = name;
this.predicate = predicate;
this.description = description;
}
/**
* Creates a {@link Grouping} with the given name.
*
* @param name must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @deprecated no replacement as a name-only {@link Grouping} doesn't make any sense in the first place.
*/
@Deprecated
public static Grouping of(String name) {
return new Grouping(name, null, __ -> false);
return new Grouping(name, __ -> false, null);
}
/**
* Creates a {@link Grouping} with the given name and selecting {@link Predicate}.
*
* @param name must not be {@literal null} or empty.
* @param predicate must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static Grouping of(String name, Predicate<SpringBean> predicate) {
return new Grouping(name, null, predicate);
return new Grouping(name, predicate, null);
}
public boolean matches(SpringBean candidate) {
return predicate.test(candidate);
/**
* Creates a {@link Grouping} with the given name, selecting {@link Predicate} and description.
*
* @param name must not be {@literal null} or empty.
* @param predicate must not be {@literal null}.
* @param description must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public static Grouping of(String name, Predicate<SpringBean> predicate, String description) {
return new Grouping(name, predicate, description);
}
/**
* Helper method to create a {@link Predicate} for {@link SpringBean}s matching the given name pattern.
*
* @param pattern must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public static Predicate<SpringBean> nameMatching(String pattern) {
Assert.hasText(pattern, "Pattern must not be null or empty!");
return bean -> bean.getFullyQualifiedTypeName().matches(pattern);
}
/**
* Helper method to create a {@link Predicate} for {@link SpringBean}s implementing the given interface.
*
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static Predicate<SpringBean> implementing(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return bean -> bean.getType().isAssignableTo(type);
}
/**
* Helper method to create a {@link Predicate} for {@link SpringBean}s that are a subtype of the given one. In
* other words, implement or extend it but are not the type itself.
*
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static Predicate<SpringBean> subtypeOf(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return implementing(type) //
.and(bean -> !bean.getType().isEquivalentTo(type));
}
/**
* Returns the name of the {@link Grouping}.
*
* @return will never be {@literal null} or empty.
*/
public String getName() {
return name;
}
/**
* Returns the description of the {@link Grouping}.
*
* @return can be {@literal null}.
*/
@Nullable
public String getDescription() {
return description;
}
/**
* Returns whether the given {@link SpringBean} matches the {@link Grouping}.
*
* @param candidate must not be {@literal null}.
*/
public boolean matches(SpringBean candidate) {
Assert.notNull(candidate, "Candidate Spring bean must not be null!");
return predicate.test(candidate);
}
}
@RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
static class Groupings {
private final MultiValueMap<Grouping, SpringBean> groupings;
Groupings(MultiValueMap<Grouping, SpringBean> groupings) {
Assert.notNull(groupings, "Groupings must not be null!");
this.groupings = groupings;
}
Set<Grouping> keySet() {
return groupings.keySet();
}
@@ -873,7 +1116,7 @@ public class Documenter {
}
List<SpringBean> byGroupName(String name) {
return byFilter(it -> it.getName().equals(name));
return byFilter(it -> it.name.equals(name));
}
void forEach(BiConsumer<Grouping, List<SpringBean>> consumer) {

View File

@@ -56,6 +56,6 @@ public interface CompletableEventPublication extends EventPublication {
* @return
*/
static CompletableEventPublication of(Object event, PublicationTargetIdentifier id) {
return DefaultEventPublication.of(event, id);
return new DefaultEventPublication(event, id);
}
}

View File

@@ -15,36 +15,125 @@
*/
package org.springframework.modulith.events;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import org.springframework.util.Assert;
/**
* Default {@link CompletableEventPublication} implementation.
*
* @author Oliver Drotbohm
*/
@Getter
@RequiredArgsConstructor(staticName = "of")
@EqualsAndHashCode
@ToString
class DefaultEventPublication implements CompletableEventPublication {
private final @NonNull Object event;
private final @NonNull PublicationTargetIdentifier targetIdentifier;
private final Instant publicationDate = Instant.now();
private final Object event;
private final PublicationTargetIdentifier targetIdentifier;
private final Instant publicationDate;
private Optional<Instant> completionDate = Optional.empty();
private Optional<Instant> completionDate;
/**
* Creates a new {@link DefaultEventPublication} for the given event and {@link PublicationTargetIdentifier}.
*
* @param event must not be {@literal null}.
* @param targetIdentifier must not be {@literal null}.
*/
DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier) {
Assert.notNull(event, "Event must not be null!");
Assert.notNull(targetIdentifier, "PublicationTargetIdentifier must not be null!");
this.event = event;
this.targetIdentifier = targetIdentifier;
this.publicationDate = Instant.now();
this.completionDate = Optional.empty();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getEvent()
*/
@Override
public Object getEvent() {
return event;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getTargetIdentifier()
*/
@Override
public PublicationTargetIdentifier getTargetIdentifier() {
return targetIdentifier;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getPublicationDate()
*/
public Instant getPublicationDate() {
return publicationDate;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate()
*/
public Optional<Instant> getCompletionDate() {
return completionDate;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#markCompleted()
*/
@Override
public CompletableEventPublication markCompleted() {
this.completionDate = Optional.of(Instant.now());
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "DefaultEventPublication [event=" + event + ", targetIdentifier=" + targetIdentifier + ", publicationDate="
+ publicationDate + ", completionDate=" + completionDate + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DefaultEventPublication that)) {
return false;
}
return Objects.equals(this.completionDate, that.completionDate) //
&& Objects.equals(this.event, that.event) //
&& Objects.equals(this.publicationDate, that.publicationDate) //
&& Objects.equals(this.targetIdentifier, that.targetIdentifier);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(completionDate, event, publicationDate, targetIdentifier);
}
}

View File

@@ -15,13 +15,11 @@
*/
package org.springframework.modulith.events;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationListener;
import org.springframework.transaction.annotation.Propagation;
@@ -36,12 +34,28 @@ import org.springframework.util.Assert;
* @author Björn Kieling
* @author Dmitry Belyaev
*/
@Slf4j
@RequiredArgsConstructor
public class DefaultEventPublicationRegistry implements DisposableBean, EventPublicationRegistry {
private final @NonNull EventPublicationRepository events;
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultEventPublicationRegistry.class);
private final EventPublicationRepository events;
/**
* Creates a new {@link DefaultEventPublicationRegistry} for the given {@link EventPublicationRepository}.
*
* @param events must not be {@literal null}.
*/
public DefaultEventPublicationRegistry(EventPublicationRepository events) {
Assert.notNull(events, "EventPublicationRepository must not be null!");
this.events = events;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRegistry#store(java.lang.Object, java.util.stream.Stream)
*/
@Override
public void store(Object event, Stream<PublicationTargetIdentifier> listeners) {
@@ -49,11 +63,19 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
.forEach(events::create);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRegistry#findIncompletePublications()
*/
@Override
public Iterable<EventPublication> findIncompletePublications() {
return events.findIncompletePublications();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRegistry#markCompleted(java.lang.Object, org.springframework.modulith.events.PublicationTargetIdentifier)
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void markCompleted(Object event, PublicationTargetIdentifier targetIdentifier) {
@@ -67,6 +89,10 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
.ifPresent(it -> events.update(it.markCompleted()));
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
@Override
public void destroy() {
@@ -74,18 +100,18 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
if (publications.isEmpty()) {
LOG.info("No publications outstanding!");
LOGGER.info("No publications outstanding!");
return;
}
LOG.info("Shutting down with the following publications left unfinished:");
LOGGER.info("Shutting down with the following publications left unfinished:");
for (int i = 0; i < publications.size(); i++) {
String prefix = i + 1 == publications.size() ? "└─" : "├─";
EventPublication it = publications.get(i);
LOG.info("{} {} - {}", prefix, it.getEvent().getClass().getName(), it.getTargetIdentifier().getValue());
LOGGER.info("{} {} - {}", prefix, it.getEvent().getClass().getName(), it.getTargetIdentifier().getValue());
}
}
@@ -93,7 +119,7 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
EventPublication result = CompletableEventPublication.of(event, targetIdentifier);
LOG.debug("Registering publication of {} for {}.", //
LOGGER.debug("Registering publication of {} for {}.", //
result.getEvent().getClass().getName(), result.getTargetIdentifier().getValue());
return result;
@@ -101,9 +127,10 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
private static EventPublication logCompleted(EventPublication publication) {
LOG.debug("Marking publication of event {} to listener {} completed.", //
LOGGER.debug("Marking publication of event {} to listener {} completed.", //
publication.getEvent().getClass().getName(), publication.getTargetIdentifier().getValue());
return publication;
}
}

View File

@@ -79,6 +79,10 @@ public interface EventPublication extends Comparable<EventPublication> {
return this.getTargetIdentifier().equals(identifier);
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public default int compareTo(EventPublication that) {
return this.getPublicationDate().compareTo(that.getPublicationDate());

View File

@@ -15,19 +15,49 @@
*/
package org.springframework.modulith.events;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.Objects;
import org.springframework.util.Assert;
/**
* Identifier for a publication target.
*
* @author Oliver Drotbohm
*/
@Value
@RequiredArgsConstructor(staticName = "of")
public class PublicationTargetIdentifier {
String value;
private final String value;
/**
* Creates a new {@link PublicationTargetIdentifier} for the given value.
*
* @param value must not be {@literal null}.
*/
private PublicationTargetIdentifier(String value) {
Assert.hasText(value, "Value must not be null or empty!");
this.value = value;
}
/**
* Returns the {@link PublicationTargetIdentifier} for the given value.
*
* @param value must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public static PublicationTargetIdentifier of(String value) {
return new PublicationTargetIdentifier(value);
}
/**
* Returns the raw String value of the identifier.
*
* @return the value
*/
public String getValue() {
return value;
}
/*
* (non-Javadoc)
@@ -37,4 +67,32 @@ public class PublicationTargetIdentifier {
public String toString() {
return value;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(value);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PublicationTargetIdentifier that)) {
return false;
}
return Objects.equals(value, that.value);
}
}

View File

@@ -17,8 +17,6 @@ package org.springframework.modulith.events.config;
import static org.springframework.core.io.support.SpringFactoriesLoader.*;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
@@ -45,12 +43,15 @@ import org.springframework.modulith.events.config.EnablePersistentDomainEvents.P
@Import(PersistentDomainEventsImportSelector.class)
public @interface EnablePersistentDomainEvents {
@RequiredArgsConstructor
static class PersistentDomainEventsImportSelector implements ImportSelector, ResourceLoaderAware {
private ResourceLoader resourceLoader;
/*
PersistentDomainEventsImportSelector(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/*
* (non-Javadoc)
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
*/
@@ -59,7 +60,7 @@ public @interface EnablePersistentDomainEvents {
this.resourceLoader = resourceLoader;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
*/

View File

@@ -15,19 +15,14 @@
*/
package org.springframework.modulith.events.support;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.framework.ProxyFactory;
@@ -35,6 +30,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.NonNull;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.transaction.event.TransactionPhase;
@@ -47,17 +43,28 @@ import org.springframework.util.ReflectionUtils.MethodCallback;
/**
* {@link BeanPostProcessor} that will add a
* {@link CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor}
* to the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of
* {@link CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor} to
* the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of
* those methods mark the event publication to those listeners as completed.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor {
private final Supplier<EventPublicationRegistry> registry;
/**
* Creates a new {@link CompletionRegisteringBeanPostProcessor} for the given {@link EventPublicationRegistry}.
*
* @param registry must not be {@literal null}.
*/
public CompletionRegisteringBeanPostProcessor(Supplier<EventPublicationRegistry> registry) {
Assert.notNull(registry, "EventPublicationRegistry must not be null!");
this.registry = registry;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
@@ -65,11 +72,11 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(registry, beanName, bean, false);
ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(registry, beanName, bean);
ReflectionUtils.doWithMethods(AopProxyUtils.ultimateTargetClass(bean), callback);
return callback.methodFound ? callback.getBean() : bean;
return callback.methodFound ? callback.bean : bean;
}
@@ -79,14 +86,33 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
*
* @author Oliver Drotbohm
*/
@AllArgsConstructor
private static class ProxyCreatingMethodCallback implements MethodCallback {
private @NonNull final Supplier<EventPublicationRegistry> registry;
private @NonNull final String beanName;
private @NonNull @Getter Object bean;
private final Supplier<EventPublicationRegistry> registry;
private final String beanName;
private Object bean;
private boolean methodFound;
/**
* Creates a new {@link ProxyCreatingMethodCallback} for the given {@link EventPublicationRegistry}, bean name, bean
* and whether a completing method has been found.
*
* @param registry must not be {@literal null}.
* @param beanName must not be {@literal null} or empty.
* @param bean must not be {@literal null}.
*/
ProxyCreatingMethodCallback(Supplier<EventPublicationRegistry> registry, String beanName, Object bean) {
Assert.notNull(registry, "EventPublicationRegistry must not be null!");
Assert.hasText(beanName, "Bean name must not be null or empty!");
Assert.notNull(bean, "Bean must not be null!");
this.registry = registry;
this.beanName = beanName;
this.bean = bean;
this.methodFound = false;
}
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method)
@@ -127,10 +153,9 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
*
* @author Oliver Drotbohm
*/
@Slf4j
@RequiredArgsConstructor
private static class CompletionRegisteringMethodInterceptor implements MethodInterceptor, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(CompletionRegisteringMethodInterceptor.class);
private static final ConcurrentLruCache<Method, Boolean> COMPLETING_METHOD = new ConcurrentLruCache<>(100,
CompletionRegisteringMethodInterceptor::calculateIsCompletingMethod);
private static final ConcurrentLruCache<CacheKey, TransactionalApplicationListenerMethodAdapter> ADAPTERS = new ConcurrentLruCache<>(
@@ -139,6 +164,19 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
private final @NonNull Supplier<EventPublicationRegistry> registry;
private final @NonNull String beanName;
/**
* @param registry
* @param beanName
*/
CompletionRegisteringMethodInterceptor(Supplier<EventPublicationRegistry> registry, String beanName) {
Assert.notNull(registry, "EventPublicationRegistry must not be null!");
Assert.hasText(beanName, "Bean name must not be null or empty!");
this.registry = registry;
this.beanName = beanName;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
@@ -172,7 +210,7 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
}
// Mark publication complete if the method is a transactional event listener.
String adapterId = ADAPTERS.get(CacheKey.of(beanName, method)).getListenerId();
String adapterId = ADAPTERS.get(new CacheKey(beanName, method)).getListenerId();
PublicationTargetIdentifier identifier = PublicationTargetIdentifier.of(adapterId);
registry.get().markCompleted(invocation.getArguments()[0], identifier);
@@ -213,12 +251,8 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
return new TransactionalApplicationListenerMethodAdapter(key.beanName, key.method.getDeclaringClass(),
key.method);
}
}
@Value(staticConstructor = "of")
static class CacheKey {
String beanName;
Method method;
}
static record CacheKey(String beanName, Method method) {}
}

View File

@@ -15,16 +15,14 @@
*/
package org.springframework.modulith.events.support;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
@@ -33,6 +31,7 @@ import org.springframework.context.event.AbstractApplicationEventMulticaster;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.NonNull;
import org.springframework.modulith.events.EventPublication;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.events.PublicationTargetIdentifier;
@@ -52,13 +51,25 @@ import org.springframework.util.Assert;
* @author Oliver Drotbohm
* @see CompletionRegisteringBeanPostProcessor
*/
@Slf4j
@RequiredArgsConstructor
public class PersistentApplicationEventMulticaster extends AbstractApplicationEventMulticaster
implements SmartInitializingSingleton {
private static final Logger LOGGER = LoggerFactory.getLogger(PersistentApplicationEventMulticaster.class);
private final @NonNull Supplier<EventPublicationRegistry> registry;
/**
* Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}.
*
* @param registry must not be {@literal null}.
*/
public PersistentApplicationEventMulticaster(Supplier<EventPublicationRegistry> registry) {
Assert.notNull(registry, "EventPublicationRegistry must not be null!");
this.registry = registry;
}
/*
* (non-Javadoc)
* @see org.springframework.context.event.ApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent)
@@ -76,15 +87,16 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
@SuppressWarnings({ "unchecked", "rawtypes" })
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = eventType == null ? ResolvableType.forInstance(event) : eventType;
Collection<ApplicationListener<?>> listeners = getApplicationListeners(event, type);
var type = eventType == null ? ResolvableType.forInstance(event) : eventType;
var listeners = getApplicationListeners(event, type);
if (listeners.isEmpty()) {
return;
}
TransactionalEventListeners txListeners = new TransactionalEventListeners(listeners);
Object eventToPersist = getEventToPersist(event);
var txListeners = new TransactionalEventListeners(listeners);
var eventToPersist = getEventToPersist(event);
registry.get().store(eventToPersist, txListeners.stream() //
.map(TransactionalApplicationListener::getListenerId) //
.map(PublicationTargetIdentifier::of));
@@ -108,7 +120,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
private void invokeTargetListener(EventPublication publication) {
TransactionalEventListeners listeners = new TransactionalEventListeners(
var listeners = new TransactionalEventListeners(
getApplicationListeners());
listeners.stream() //
@@ -117,7 +129,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
.map(it -> executeListenerWithCompletion(publication, it)) //
.orElseGet(() -> {
LOG.debug("Listener {} not found!", publication.getTargetIdentifier());
LOGGER.debug("Listener {} not found!", publication.getTargetIdentifier());
return null;
});
}

View File

@@ -31,7 +31,7 @@ class CompletableEventPublicationUnitTests {
assertThatExceptionOfType(IllegalArgumentException.class)//
.isThrownBy(() -> CompletableEventPublication.of(null, PublicationTargetIdentifier.of("foo")))//
.withMessageContaining("event");
.withMessageContaining("Event");
}
@Test
@@ -39,7 +39,7 @@ class CompletableEventPublicationUnitTests {
assertThatExceptionOfType(IllegalArgumentException.class)//
.isThrownBy(() -> CompletableEventPublication.of(new Object(), null))//
.withMessageContaining("targetIdentifier");
.withMessageContaining("TargetIdentifier");
}
@Test

View File

@@ -15,14 +15,13 @@
*/
package org.springframework.modulith.events.jackson;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.config.EventSerializationConfigurationExtension;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -34,12 +33,27 @@ import com.fasterxml.jackson.databind.SerializationFeature;
* @author Oliver Drotbohm
*/
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
class JacksonEventSerializationConfiguration implements EventSerializationConfigurationExtension {
private final ObjectProvider<ObjectMapper> mapper;
private final ApplicationContext context;
/**
* Creates a new {@link JacksonEventSerializationConfiguration} for the given {@link ObjectMapper} and
* {@link ApplicationContext}.
*
* @param mapper must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public JacksonEventSerializationConfiguration(ObjectProvider<ObjectMapper> mapper, ApplicationContext context) {
Assert.notNull(mapper, "ObjectMapper must not be null!");
Assert.notNull(context, "ApplicationContext must not be null!");
this.mapper = mapper;
this.context = context;
}
@Bean
public JacksonEventSerializer jacksonEventSerializer() {
return new JacksonEventSerializer(() -> mapper.getIfAvailable(() -> defaultObjectMapper()));
@@ -47,7 +61,7 @@ class JacksonEventSerializationConfiguration implements EventSerializationConfig
private ObjectMapper defaultObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
var mapper = new ObjectMapper();
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.registerModules(context.getBeansOfType(Module.class).values());

View File

@@ -15,24 +15,36 @@
*/
package org.springframework.modulith.events.jackson;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.function.Supplier;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A Jackson-based {@link EventSerializer}.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
class JacksonEventSerializer implements EventSerializer {
private final Supplier<ObjectMapper> mapper;
/**
* Creates a new {@link JacksonEventSerializer} for the given {@link ObjectMapper}.
*
* @param mapper must not be {@literal null}.
*/
public JacksonEventSerializer(Supplier<ObjectMapper> mapper) {
Assert.notNull(mapper, "ObjectMapper must not be null!");
this.mapper = mapper;
}
/*
* (non-Javadoc)
* @see de.oliverDrotbohm.events.EventSerializer#serialize(java.lang.Object)

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.events.jdbc;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
@@ -24,7 +22,7 @@ import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.util.StreamUtils;
/**
@@ -34,13 +32,32 @@ import org.springframework.util.StreamUtils;
* @author Björn Kieling
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
class DatabaseSchemaInitializer implements InitializingBean {
private final JdbcTemplate jdbcTemplate;
private final JdbcOperations jdbcOperations;
private final ResourceLoader resourceLoader;
private final DatabaseType databaseType;
/**
* Creates a new {@link DatabaseSchemaInitializer} for the given {@link JdbcOperations}, {@link ResourceLoader} and
* {@link DatabaseType}.
*
* @param jdbcOperations must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
* @param databaseType must not be {@literal null}.
*/
public DatabaseSchemaInitializer(JdbcOperations jdbcOperations, ResourceLoader resourceLoader,
DatabaseType databaseType) {
this.jdbcOperations = jdbcOperations;
this.resourceLoader = resourceLoader;
this.databaseType = databaseType;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
@@ -48,10 +65,10 @@ class DatabaseSchemaInitializer implements InitializingBean {
var schemaDdlResource = resourceLoader.getResource(schemaResourceFilename);
var schemaDdl = asString(schemaDdlResource);
jdbcTemplate.execute(schemaDdl);
jdbcOperations.execute(schemaDdl);
}
private String asString(Resource resource) {
private static String asString(Resource resource) {
try {
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);

View File

@@ -15,20 +15,18 @@
*/
package org.springframework.modulith.events.jdbc;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
@@ -39,6 +37,7 @@ import org.springframework.modulith.events.EventPublicationRepository;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
/**
* JDBC-based repository to store {@link EventPublication}s.
@@ -47,10 +46,10 @@ import org.springframework.transaction.annotation.Transactional;
* @author Björn Kieling
* @author Oliver Drotbohm
*/
@Slf4j
@RequiredArgsConstructor
class JdbcEventPublicationRepository implements EventPublicationRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(JdbcEventPublicationRepository.class);
private static final String SQL_STATEMENT_INSERT = """
INSERT INTO EVENT_PUBLICATION (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT)
VALUES (?, ?, ?, ?, ?)
@@ -89,6 +88,30 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
private final EventSerializer serializer;
private final DatabaseType databaseType;
/**
* Creates a new {@link JdbcEventPublicationRepository} for the given {@link JdbcOperations}, {@link EventSerializer}
* and {@link DatabaseType}.
*
* @param operations must not be {@literal null}.
* @param serializer must not be {@literal null}.
* @param databaseType must not be {@literal null}.
*/
public JdbcEventPublicationRepository(JdbcOperations operations, EventSerializer serializer,
DatabaseType databaseType) {
Assert.notNull(operations, "JdbcOperations must not be null!");
Assert.notNull(serializer, "EventSerializer must not be null!");
Assert.notNull(databaseType, "DatabaseType must not be null!");
this.operations = operations;
this.serializer = serializer;
this.databaseType = databaseType;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#create(org.springframework.modulith.events.EventPublication)
*/
@Override
@Transactional
public EventPublication create(EventPublication publication) {
@@ -218,14 +241,12 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
}
var completionDate = rs.getTimestamp("COMPLETION_DATE");
var publicationDate = rs.getTimestamp("PUBLICATION_DATE").toInstant();
var listenerId = rs.getString("LISTENER_ID");
var serializedEvent = rs.getString("SERIALIZED_EVENT");
return JdbcEventPublication.builder().completionDate(completionDate == null ? null : completionDate.toInstant())
.eventType(eventClass) //
.listenerId(rs.getString("LISTENER_ID")) //
.publicationDate(rs.getTimestamp("PUBLICATION_DATE").toInstant()) //
.serializedEvent(rs.getString("SERIALIZED_EVENT")) //
.serializer(serializer) //
.build();
return new JdbcEventPublication(id, publicationDate, listenerId, serializedEvent, eventClass, serializer,
completionDate == null ? null : completionDate.toInstant());
}
private Object uuidToDatabase(UUID id) {
@@ -242,13 +263,11 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
LOG.warn("Event '{}' of unknown type '{}' found", id, className);
LOGGER.warn("Event '{}' of unknown type '{}' found", id, className);
return null;
}
}
@EqualsAndHashCode
@Builder
private static class JdbcEventPublication implements CompletableEventPublication {
private final UUID id;
@@ -260,35 +279,122 @@ class JdbcEventPublicationRepository implements EventPublicationRepository {
private final EventSerializer serializer;
private @Nullable Instant completionDate;
/**
* @param id must not be {@literal null}.
* @param publicationDate must not be {@literal null}.
* @param listenerId must not be {@literal null} or empty.
* @param serializedEvent must not be {@literal null} or empty.
* @param eventType must not be {@literal null}.
* @param serializer must not be {@literal null}.
* @param completionDate can be {@literal null}.
*/
public JdbcEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType, EventSerializer serializer, @Nullable Instant completionDate) {
Assert.notNull(id, "Id must not be null!");
Assert.notNull(publicationDate, "Publication date must not be null!");
Assert.hasText(listenerId, "Listener id must not be null or empty!");
Assert.hasText(serializedEvent, "Serialized event must not be null or empty!");
Assert.notNull(eventType, "Event type must not be null!");
Assert.notNull(serializer, "EventSerializer must not be null!");
this.id = id;
this.publicationDate = publicationDate;
this.listenerId = listenerId;
this.serializedEvent = serializedEvent;
this.eventType = eventType;
this.serializer = serializer;
this.completionDate = completionDate;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getEvent()
*/
@Override
public Object getEvent() {
return serializer.deserialize(serializedEvent, eventType);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getTargetIdentifier()
*/
@Override
public PublicationTargetIdentifier getTargetIdentifier() {
return PublicationTargetIdentifier.of(listenerId);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getPublicationDate()
*/
@Override
public Instant getPublicationDate() {
return publicationDate;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate()
*/
@Override
public Optional<Instant> getCompletionDate() {
return Optional.ofNullable(completionDate);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#isPublicationCompleted()
*/
@Override
public boolean isPublicationCompleted() {
return completionDate != null;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#markCompleted()
*/
@Override
public CompletableEventPublication markCompleted() {
this.completionDate = Instant.now();
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof JdbcEventPublication that)) {
return false;
}
return Objects.equals(completionDate, that.completionDate) //
&& Objects.equals(eventType, that.eventType) //
&& Objects.equals(id, that.id) //
&& Objects.equals(listenerId, that.listenerId) //
&& Objects.equals(publicationDate, that.publicationDate) //
&& Objects.equals(serializedEvent, that.serializedEvent) //
&& Objects.equals(serializer, that.serializer);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(completionDate, eventType, id, listenerId, publicationDate, serializedEvent, serializer);
}
}
}

View File

@@ -18,15 +18,12 @@ package org.springframework.modulith.events.jpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.UUID;
import org.springframework.util.Assert;
/**
* JPA entity to represent event publications.
*
@@ -34,25 +31,47 @@ import java.util.UUID;
* @author Dmitry Belyaev
* @author Björn Kieling
*/
@Data
@Entity
@NoArgsConstructor(force = true)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class JpaEventPublication {
private final @Id @Column(length = 16) UUID id;
private final Instant publicationDate;
private final String listenerId;
private final String serializedEvent;
private final Class<?> eventType;
final @Id @Column(length = 16) UUID id;
final Instant publicationDate;
final String listenerId;
final String serializedEvent;
final Class<?> eventType;
private Instant completionDate;
Instant completionDate;
@Builder
static JpaEventPublication of(Instant publicationDate, String listenerId, Object serializedEvent,
Class<?> eventType) {
return new JpaEventPublication(UUID.randomUUID(), publicationDate, listenerId, serializedEvent.toString(),
eventType);
/**
* Creates a new {@link JpaEventPublication} for the given publication date, listener id, serialized event and event
* type.
*
* @param publicationDate must not be {@literal null}.
* @param listenerId must not be {@literal null} or empty.
* @param serializedEvent must not be {@literal null} or empty.
* @param eventType must not be {@literal null}.
*/
JpaEventPublication(Instant publicationDate, String listenerId, String serializedEvent, Class<?> eventType) {
Assert.notNull(publicationDate, "Publication date must not be null!");
Assert.notNull(listenerId, "Listener id must not be null or empty!");
Assert.notNull(serializedEvent, "Serialized event must not be null or empty!");
Assert.notNull(eventType, "Event type must not be null!");
this.id = UUID.randomUUID();
this.publicationDate = publicationDate;
this.listenerId = listenerId;
this.serializedEvent = serializedEvent;
this.eventType = eventType;
}
JpaEventPublication() {
this.id = null;
this.publicationDate = null;
this.listenerId = null;
this.serializedEvent = null;
this.eventType = null;
}
JpaEventPublication markCompleted() {

View File

@@ -16,7 +16,6 @@
package org.springframework.modulith.events.jpa;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -29,7 +28,6 @@ import org.springframework.modulith.events.config.EventPublicationConfigurationE
* @author Björn Kieling
*/
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
class JpaEventPublicationConfiguration implements EventPublicationConfigurationExtension {
@Bean

View File

@@ -16,11 +16,10 @@
package org.springframework.modulith.events.jpa;
import jakarta.persistence.EntityManager;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.springframework.modulith.events.CompletableEventPublication;
@@ -29,6 +28,7 @@ import org.springframework.modulith.events.EventPublicationRepository;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
/**
* Repository to store {@link EventPublication}s.
@@ -37,7 +37,6 @@ import org.springframework.transaction.annotation.Transactional;
* @author Dmitry Belyaev
* @author Björn Kieling
*/
@RequiredArgsConstructor
class JpaEventPublicationRepository implements EventPublicationRepository {
private static String BY_EVENT_AND_LISTENER_ID = """
@@ -66,6 +65,26 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
private final EntityManager entityManager;
private final EventSerializer serializer;
/**
* Creates a new {@link JpaEventPublicationRepository} for the given {@link EntityManager} and
* {@link EventSerializer}.
*
* @param entityManager must not be {@literal null}.
* @param serializer must not be {@literal null}.
*/
public JpaEventPublicationRepository(EntityManager entityManager, EventSerializer serializer) {
Assert.notNull(entityManager, "EntityManager must not be null!");
Assert.notNull(serializer, "EventSerializer must not be null!");
this.entityManager = entityManager;
this.serializer = serializer;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#create(org.springframework.modulith.events.EventPublication)
*/
@Override
@Transactional
public EventPublication create(EventPublication publication) {
@@ -75,6 +94,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
return publication;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#update(org.springframework.modulith.events.CompletableEventPublication)
*/
@Override
@Transactional
public EventPublication update(CompletableEventPublication publication) {
@@ -83,11 +106,15 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
var event = publication.getEvent();
findEntityBySerializedEventAndListenerIdAndCompletionDateNull(event, id) //
.ifPresent(entity -> entity.setCompletionDate(publication.getCompletionDate().orElse(null)));
.ifPresent(entity -> entity.completionDate = publication.getCompletionDate().orElse(null));
return publication;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#findIncompletePublications()
*/
@Override
@Transactional(readOnly = true)
public List<EventPublication> findIncompletePublications() {
@@ -98,6 +125,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
.toList();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#findIncompletePublicationsByEventAndTargetIdentifier(java.lang.Object, org.springframework.modulith.events.PublicationTargetIdentifier)
*/
@Override
@Transactional(readOnly = true)
public Optional<EventPublication> findIncompletePublicationsByEventAndTargetIdentifier( //
@@ -107,6 +138,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
.map(this::entityToDomain);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublicationRepository#deleteCompletedPublications()
*/
@Override
@Transactional
public void deleteCompletedPublications() {
@@ -130,55 +165,116 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
}
private JpaEventPublication domainToEntity(EventPublication domain) {
return JpaEventPublication.builder() //
.publicationDate(domain.getPublicationDate()) //
.listenerId(domain.getTargetIdentifier().getValue()) //
.serializedEvent(serializeEvent(domain.getEvent())) //
.eventType(domain.getEvent().getClass()) //
.build();
return new JpaEventPublication(domain.getPublicationDate(), domain.getTargetIdentifier().getValue(),
serializeEvent(domain.getEvent()), domain.getEvent().getClass());
}
private EventPublication entityToDomain(JpaEventPublication entity) {
return JpaEventPublicationAdapter.of(entity, serializer);
return new JpaEventPublicationAdapter(entity, serializer);
}
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of")
private static class JpaEventPublicationAdapter implements CompletableEventPublication {
private final JpaEventPublication publication;
private final EventSerializer serializer;
/**
* Creates a new {@link JpaEventPublicationAdapter} for the given {@link JpaEventPublication} and
* {@link EventSerializer}.
*
* @param publication must not be {@literal null}.
* @param serializer must not be {@literal null}.
*/
public JpaEventPublicationAdapter(JpaEventPublication publication, EventSerializer serializer) {
Assert.notNull(publication, "JpaEventPublication must not be null!");
Assert.notNull(serializer, "EventSerializer must not be null!");
this.publication = publication;
this.serializer = serializer;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getEvent()
*/
@Override
public Object getEvent() {
return serializer.deserialize(publication.getSerializedEvent(), publication.getEventType());
return serializer.deserialize(publication.serializedEvent, publication.eventType);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getTargetIdentifier()
*/
@Override
public PublicationTargetIdentifier getTargetIdentifier() {
return PublicationTargetIdentifier.of(publication.getListenerId());
return PublicationTargetIdentifier.of(publication.listenerId);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.EventPublication#getPublicationDate()
*/
@Override
public Instant getPublicationDate() {
return publication.getPublicationDate();
return publication.publicationDate;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate()
*/
@Override
public Optional<Instant> getCompletionDate() {
return Optional.ofNullable(publication.getCompletionDate());
return Optional.ofNullable(publication.completionDate);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#isPublicationCompleted()
*/
@Override
public boolean isPublicationCompleted() {
return publication.getCompletionDate() != null;
return publication.completionDate != null;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.CompletableEventPublication#markCompleted()
*/
@Override
public CompletableEventPublication markCompleted() {
publication.markCompleted();
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof JpaEventPublicationAdapter that)) {
return false;
}
return Objects.equals(publication, that.publication)
&& Objects.equals(serializer, that.serializer);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(publication, serializer);
}
}
}

View File

@@ -193,7 +193,7 @@ class JpaEventPublicationRepositoryIntegrationTests {
assertThat(em.createQuery("select p from JpaEventPublication p", JpaEventPublication.class).getResultList())
.hasSize(1) //
.element(0).extracting(JpaEventPublication::getSerializedEvent).isEqualTo(serializedEvent2);
.element(0).extracting(it -> it.serializedEvent).isEqualTo(serializedEvent2);
}
@Value

View File

@@ -15,16 +15,13 @@
*/
package org.springframework.modulith.events.mongodb;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import java.time.Instant;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.PersistenceCreator;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A MongoDB Document to represent event publications.
@@ -33,18 +30,63 @@ import java.time.Instant;
* @author Björn Kieling
*/
@Document(collection = "org_springframework_modulith_events")
@Getter
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@PersistenceCreator))
class MongoDbEventPublication {
@Id
private String id;
final ObjectId id;
final Instant publicationDate;
final String listenerId;
final Object event;
private final Instant publicationDate;
private final String listenerId;
private final Object event;
@Nullable Instant completionDate;
@Setter(AccessLevel.PACKAGE)
private Instant completionDate;
/**
* Creates a new {@link MongoDbEventPublication} for the given id, publication date, listener id, event and completion
* date.
*
* @param id must not be {@literal null}.
* @param publicationDate must not be {@literal null}.
* @param listenerId must not be {@literal null} or empty.
* @param event must not be {@literal null}.
* @param completionDate can be {@literal null}.
*/
@PersistenceCreator
MongoDbEventPublication(ObjectId id, Instant publicationDate, String listenerId, Object event,
@Nullable Instant completionDate) {
Assert.notNull(id, "Id must not be null!");
Assert.notNull(publicationDate, "Publication date must not be null!");
Assert.notNull(listenerId, "Listener id must not be null!");
Assert.notNull(event, "Event must not be null!");
this.id = id;
this.publicationDate = publicationDate;
this.listenerId = listenerId;
this.event = event;
this.completionDate = completionDate;
}
/**
* Creates a new {@link MongoDbEventPublication} for the given publication date, listener id and event.
*
* @param publicationDate must not be {@literal null}.
* @param listenerId must not be {@literal null}.
* @param event must not be {@literal null}.
*/
MongoDbEventPublication(Instant publicationDate, String listenerId, Object event) {
this(new ObjectId(), publicationDate, listenerId, event, null);
}
/**
* Marks the publication as completed at the given {@link Instant}.
*
* @param instant must not be {@literal null}.
* @return will never be {@literal null}.
*/
MongoDbEventPublication markCompleted(Instant instant) {
Assert.notNull(instant, "Instant must not be null!");
this.completionDate = instant;
return this;
}
}

View File

@@ -18,11 +18,9 @@ package org.springframework.modulith.events.mongodb;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.springframework.data.domain.Sort;
@@ -72,7 +70,7 @@ class MongoDbEventPublicationRepository implements EventPublicationRepository {
publication.getTargetIdentifier()) //
.stream() //
.findFirst() //
.map(document -> document.setCompletionDate(publication.getCompletionDate().orElse(null))) //
.map(document -> document.markCompleted(publication.getCompletionDate().orElse(null))) //
.map(mongoTemplate::save) //
.map(this::documentToDomain) //
.orElse(publication);
@@ -131,44 +129,73 @@ class MongoDbEventPublicationRepository implements EventPublicationRepository {
}
private CompletableEventPublication documentToDomain(MongoDbEventPublication document) {
return MongoDbEventPublicationAdapter.of(document);
return new MongoDbEventPublicationAdapter(document);
}
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of")
private static class MongoDbEventPublicationAdapter implements CompletableEventPublication {
private final MongoDbEventPublication publication;
MongoDbEventPublicationAdapter(MongoDbEventPublication publication) {
this.publication = publication;
}
@Override
public Object getEvent() {
return publication.getEvent();
return publication.event;
}
@Override
public PublicationTargetIdentifier getTargetIdentifier() {
return PublicationTargetIdentifier.of(publication.getListenerId());
return PublicationTargetIdentifier.of(publication.listenerId);
}
@Override
public Instant getPublicationDate() {
return publication.getPublicationDate();
return publication.publicationDate;
}
@Override
public Optional<Instant> getCompletionDate() {
return Optional.ofNullable(publication.getCompletionDate());
return Optional.ofNullable(publication.completionDate);
}
@Override
public boolean isPublicationCompleted() {
return publication.getCompletionDate() != null;
return publication.completionDate != null;
}
@Override
public CompletableEventPublication markCompleted() {
publication.setCompletionDate(Instant.now());
publication.completionDate = Instant.now();
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MongoDbEventPublicationAdapter other)) {
return false;
}
return Objects.equals(publication, other.publication);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(publication);
}
}
}

View File

@@ -186,7 +186,7 @@ class MongoDbEventPublicationRepositoryTest {
assertThat(mongoTemplate.findAll(MongoDbEventPublication.class)) //
.hasSize(1) //
.element(0).extracting(MongoDbEventPublication::getEvent).isEqualTo(testEvent2);
.element(0).extracting(it -> it.event).isEqualTo(testEvent2);
}
}

View File

@@ -89,12 +89,6 @@
<!-- Infrastructure -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>

View File

@@ -16,9 +16,9 @@
package example.inventory;
import example.order.OrderCompleted;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.modulith.ApplicationModuleListener;
import org.springframework.stereotype.Service;
@@ -27,17 +27,21 @@ import org.springframework.stereotype.Service;
*
* @author Oliver Drotbohm
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class InventoryManagement {
private final InventoryInternal dependency;
private static final Logger LOG = LoggerFactory.getLogger(InventoryManagement.class);
final InventoryInternal dependency;
InventoryManagement(InventoryInternal dependency) {
this.dependency = dependency;
}
@ApplicationModuleListener
void on(OrderCompleted event) throws InterruptedException {
var orderId = event.getOrderId();
var orderId = event.orderId();
LOG.info("Received order completion for {}.", orderId);

View File

@@ -15,25 +15,22 @@
*/
package example.inventory;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
/**
* Some Spring Boot configuration properties exposed by the inventory.
*
* @author Oliver Drotbohm
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@ConstructorBinding))
@ConfigurationProperties("example.inventory")
class InventorySettings {
record InventorySettings(int stockThreshold) {
/**
* Some Javadoc.
*
* @return
*/
int stockThreshold;
public int stockThreshold() {
return stockThreshold;
}
}

View File

@@ -15,20 +15,18 @@
*/
package example.order;
import lombok.Value;
import java.util.UUID;
/**
* @author Oliver Drotbohm
*/
@Value
public class Order {
private OrderIdentifier id = new OrderIdentifier(UUID.randomUUID());
@Value
public static class OrderIdentifier {
UUID id;
public OrderIdentifier getId() {
return id;
}
public static record OrderIdentifier(UUID id) {}
}

View File

@@ -16,14 +16,10 @@
package example.order;
import example.order.Order.OrderIdentifier;
import lombok.Value;
import org.jmolecules.event.types.DomainEvent;
/**
* @author Oliver Drotbohm
*/
@Value
public class OrderCompleted implements DomainEvent {
OrderIdentifier orderId;
}
public record OrderCompleted(OrderIdentifier orderId) implements DomainEvent {}

View File

@@ -16,7 +16,6 @@
package example.order;
import example.order.internal.OrderInternal;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@@ -26,11 +25,16 @@ import org.springframework.transaction.annotation.Transactional;
* @author Oliver Drotbohm
*/
@Service
@RequiredArgsConstructor
public class OrderManagement {
private final ApplicationEventPublisher events;
private final OrderInternal dependency;
final ApplicationEventPublisher events;
final OrderInternal dependency;
OrderManagement(ApplicationEventPublisher events, OrderInternal dependency) {
this.events = events;
this.dependency = dependency;
}
@Transactional
public void complete(Order order) {

View File

@@ -18,7 +18,6 @@ package example.order;
import static org.assertj.core.api.Assertions.*;
import example.order.EventPublicationRegistryTests.FailingAsyncTransactionalEventListener;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
@@ -34,7 +33,6 @@ import org.springframework.test.annotation.DirtiesContext;
* @author Oliver Drotbohm
*/
@ApplicationModuleTest
@RequiredArgsConstructor
@Import(FailingAsyncTransactionalEventListener.class)
@DirtiesContext
class EventPublicationRegistryTests {
@@ -42,6 +40,15 @@ class EventPublicationRegistryTests {
private final OrderManagement orders;
private final EventPublicationRegistry registry;
/**
* @param orders
* @param registry
*/
EventPublicationRegistryTests(OrderManagement orders, EventPublicationRegistry registry) {
this.orders = orders;
this.registry = registry;
}
@Test
void leavesPublicationIncompleteForFailingListener() throws Exception {

View File

@@ -17,8 +17,6 @@ package example.order;
import static org.assertj.core.api.Assertions.*;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.AssertablePublishedEvents;
@@ -27,11 +25,14 @@ import org.springframework.modulith.test.AssertablePublishedEvents;
* @author Oliver Drotbohm
*/
@ApplicationModuleTest
@RequiredArgsConstructor
class OrderIntegrationTests {
private final OrderManagement orders;
OrderIntegrationTests(OrderManagement orders) {
this.orders = orders;
}
@Test
void publishesOrderCompletion(AssertablePublishedEvents events) {
@@ -42,7 +43,7 @@ class OrderIntegrationTests {
// Verification
var matchingMapped = events.ofType(OrderCompleted.class)
.matching(OrderCompleted::getOrderId, reference.getId());
.matching(OrderCompleted::orderId, reference.getId());
assertThat(matchingMapped).hasSize(1);
@@ -50,6 +51,6 @@ class OrderIntegrationTests {
assertThat(events)
.contains(OrderCompleted.class)
.matching(OrderCompleted::getOrderId, reference.getId());
.matching(OrderCompleted::orderId, reference.getId());
}
}

View File

@@ -15,8 +15,6 @@
*/
package com.acme.myproject.moduleA;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@@ -24,11 +22,14 @@ import org.springframework.stereotype.Component;
* @author Oliver Drotbohm
*/
@Component
@RequiredArgsConstructor
public class ServiceComponentA {
private final ApplicationEventPublisher publisher;
ServiceComponentA(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void fireEvent() {
publisher.publishEvent(new SomeEventA("Message"));
}

View File

@@ -15,15 +15,10 @@
*/
package com.acme.myproject.moduleA;
import lombok.Value;
import org.jmolecules.event.annotation.DomainEvent;
/**
* @author Oliver Drotbohm
*/
@Value
@DomainEvent
public class SomeEventA {
String message;
}
public record SomeEventA(String message) {}

View File

@@ -15,8 +15,6 @@
*/
package com.acme.myproject.moduleB;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import com.acme.myproject.moduleA.ServiceComponentA;
@@ -26,9 +24,13 @@ import com.acme.myproject.moduleB.internal.InternalComponentB;
* @author Oliver Drotbohm
*/
@Component
@RequiredArgsConstructor
public class ServiceComponentB {
private final ServiceComponentA serviceComponentA;
private final InternalComponentB internalComponentB;
final ServiceComponentA serviceComponentA;
final InternalComponentB internalComponentB;
ServiceComponentB(ServiceComponentA serviceComponentA, InternalComponentB internalComponentB) {
this.serviceComponentA = serviceComponentA;
this.internalComponentB = internalComponentB;
}
}

View File

@@ -15,8 +15,6 @@
*/
package com.acme.myproject.moduleC;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import com.acme.myproject.moduleB.ServiceComponentB;
@@ -25,7 +23,11 @@ import com.acme.myproject.moduleB.ServiceComponentB;
* @author Oliver Drotbohm
*/
@Component
@RequiredArgsConstructor
class ServiceComponentC {
private final ServiceComponentB serviceComponentB;
final ServiceComponentB serviceComponentB;
ServiceComponentC(ServiceComponentB serviceComponentB) {
this.serviceComponentB = serviceComponentB;
}
}

View File

@@ -15,8 +15,6 @@
*/
package com.acme.myproject.moduleD;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import com.acme.myproject.moduleC.SomeValueC;
@@ -25,8 +23,11 @@ import com.acme.myproject.moduleC.SomeValueC;
* @author Oliver Drotbohm
*/
@ConfigurationProperties
@RequiredArgsConstructor
class ConfigurationPropertiesD {
private final SomeValueC value;
final SomeValueC value;
ConfigurationPropertiesD(SomeValueC value) {
this.value = value;
}
}

View File

@@ -15,8 +15,6 @@
*/
package com.acme.myproject.stereotypes;
import lombok.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@@ -65,17 +63,6 @@ public class Stereotypes {
@Component
static class SomeAppInterfaceImplementation implements SomeAppInterface {}
@Value
@ConfigurationProperties("org.springframework.modulith.sample")
static class SomeConfigurationProperties {
/**
* Some test property.
*/
String test;
public SomeConfigurationProperties(String test) {
this.test = test;
}
}
static record SomeConfigurationProperties(String test) {}
}

View File

@@ -51,7 +51,7 @@ class ModuleATest {
context.getBean(ServiceComponentA.class).fireEvent();
TypedPublishedEvents<SomeEventA> matching = events.ofType(SomeEventA.class) //
.matching(it -> it.getMessage().equals("Message"));
.matching(it -> it.message().equals("Message"));
assertThat(matching).hasSize(1);
}

View File

@@ -48,7 +48,7 @@ class DocumenterUnitTests {
.groupBeans(modules.getModuleByName("stereotypes").orElseThrow(RuntimeException::new));
assertThat(result.keySet())
.extracting(CanvasOptions.Grouping::getName)
.extracting(it -> it.getName())
.containsExactlyInAnyOrder("Controllers", "Services", "Repositories", "Event listeners",
"Configuration properties", "Representations", "Interface implementations", "Others");

View File

@@ -15,22 +15,78 @@
*/
package org.springframework.modulith.moments;
import lombok.Value;
import java.time.LocalDate;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published on each day.
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class DayHasPassed implements DomainEvent {
/**
* The day that has just passed.
*/
private final LocalDate date;
/**
* Creates a new {@link DayHasPassed} for the given {@link LocalDate}.
*
* @param month must not be {@literal null}.
*/
private DayHasPassed(LocalDate date) {
Assert.notNull(date, "LocalDate must not be null!");
this.date = date;
}
/**
* Creates a new {@link DayHasPassed} for the given {@link LocalDate}.
*
* @param month must not be {@literal null}.
*/
public static DayHasPassed of(LocalDate date) {
return new DayHasPassed(date);
}
/**
* The day that has just passed.
*
* @return will never be {@literal null}.
*/
public LocalDate getDate() {
return date;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DayHasPassed that)) {
return false;
}
return Objects.equals(date, that.date);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(date);
}
}

View File

@@ -15,22 +15,78 @@
*/
package org.springframework.modulith.moments;
import lombok.Value;
import java.time.LocalDateTime;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published on each day.
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class HourHasPassed implements DomainEvent {
/**
* The hour that has just passed.
*/
private final LocalDateTime time;
/**
* Creates a new {@link HourHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
private HourHasPassed(LocalDateTime time) {
Assert.notNull(time, "YearMonth must not be null!");
this.time = time;
}
/**
* Creates a new {@link HourHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
public static HourHasPassed of(LocalDateTime time) {
return new HourHasPassed(time);
}
/**
* The hour that has just passed.
*
* @return will never be {@literal null}.
*/
public LocalDateTime getTime() {
return time;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof HourHasPassed that)) {
return false;
}
return Objects.equals(time, that.time);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(time);
}
}

View File

@@ -15,23 +15,75 @@
*/
package org.springframework.modulith.moments;
import lombok.Value;
import java.time.YearMonth;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published on the last day of the month.
*
* @author Oliver Drotbohm
* @since 1.3
*/
@Value(staticConstructor = "of")
public class MonthHasPassed implements DomainEvent {
private final YearMonth month;
/**
* Creates a new {@link MonthHasPassed} for the given {@link YearMonth}.
*
* @param month must not be {@literal null}.
*/
private MonthHasPassed(YearMonth month) {
Assert.notNull(month, "YearMonth must not be null!");
this.month = month;
}
/**
* Creates a new {@link MonthHasPassed} for the given {@link YearMonth}.
*
* @param month must not be {@literal null}.
*/
public static MonthHasPassed of(YearMonth month) {
return new MonthHasPassed(month);
}
/**
* The month that has just passed.
*
* @return will never be {@literal null}.
*/
private final YearMonth month;
public YearMonth getMonth() {
return month;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MonthHasPassed that)) {
return false;
}
return Objects.equals(month, that.month);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(month);
}
}

View File

@@ -17,19 +17,16 @@ package org.springframework.modulith.moments;
import static java.time.MonthDay.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.time.Month;
import java.time.MonthDay;
import org.springframework.util.Assert;
/**
* A logical {@link Quarter} of the year.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum Quarter {
Q1(of(Month.JANUARY, 1), of(Month.MARCH, 31)), //
@@ -37,7 +34,40 @@ public enum Quarter {
Q3(of(Month.JULY, 1), of(Month.SEPTEMBER, 30)), //
Q4(of(Month.OCTOBER, 1), of(Month.DECEMBER, 31));
private final @Getter MonthDay start, end;
private final MonthDay start, end;
/**
* Creates a new {@link Quarter} for the given start and end {@link MonthDay}.
*
* @param start must not be {@literal null}.
* @param end must not be {@literal null}.
*/
private Quarter(MonthDay start, MonthDay end) {
Assert.notNull(start, "Start MonthDay must not be null!");
Assert.notNull(end, "End MonthDay must not be null!");
this.start = start;
this.end = end;
}
/**
* Returns the start {@link MonthDay}.
*
* @return will never be {@literal null}.
*/
public MonthDay getStart() {
return start;
}
/**
* Returns the end {@link MonthDay}.
*
* @return will never be {@literal null}.
*/
public MonthDay getEnd() {
return end;
}
/**
* Returns the next logical {@link Quarter}.

View File

@@ -15,25 +15,49 @@
*/
package org.springframework.modulith.moments;
import lombok.NonNull;
import lombok.Value;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published once a quarter has passed.
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class QuarterHasPassed implements DomainEvent {
private final @NonNull Year year;
private final @NonNull ShiftedQuarter quarter;
private final Year year;
private final ShiftedQuarter quarter;
/**
* Creates a new {@link QuarterHasPassed} for the given {@link Year} and {@link ShiftedQuarter}.
*
* @param year must not be {@literal null}.
* @param quarter must not be {@literal null}.
*/
private QuarterHasPassed(Year year, ShiftedQuarter quarter) {
Assert.notNull(year, "Year must not be null!");
Assert.notNull(quarter, "ShiftedQuarter must not be null!");
this.year = year;
this.quarter = quarter;
}
/**
* Returns a {@link QuarterHasPassed} for the given {@link Year} and {@link ShiftedQuarter}.
*
* @param year must not be {@literal null}.
* @param quarter must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static QuarterHasPassed of(Year year, ShiftedQuarter quarter) {
return new QuarterHasPassed(year, quarter);
}
/**
* Returns a {@link QuarterHasPassed} for the given {@link Year} and logical {@link Quarter}.
@@ -75,4 +99,32 @@ public class QuarterHasPassed implements DomainEvent {
public LocalDate getEndDate() {
return quarter.getEndDate(year);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof QuarterHasPassed that)) {
return false;
}
return Objects.equals(quarter, that.quarter) //
&& Objects.equals(year, that.year);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(quarter, year);
}
}

View File

@@ -15,15 +15,11 @@
*/
package org.springframework.modulith.moments;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import java.time.LocalDate;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.util.Assert;
@@ -33,14 +29,38 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class ShiftedQuarter {
private static final MonthDay FIRST_DAY = MonthDay.of(Month.JANUARY, 1);
private static final MonthDay LAST_DAY = MonthDay.of(Month.DECEMBER, 31);
private final @NonNull Quarter quarter;
private final @NonNull @Getter(AccessLevel.NONE) Month startMonth;
private final Quarter quarter;
private final Month startMonth;
/**
* Creates a new {@link ShiftedQuarter} for the given {@link Quarter} and start {@link Month}.
*
* @param quarter must not be {@literal null}.
* @param startMonth must not be {@literal null}.
*/
private ShiftedQuarter(Quarter quarter, Month startMonth) {
Assert.notNull(quarter, "Quarter must not be null!");
Assert.notNull(startMonth, "Start Month must not be null!");
this.quarter = quarter;
this.startMonth = startMonth;
}
/**
* Creates a new {@link ShiftedQuarter} for the given {@link Quarter} and start {@link Month}.
*
* @param quarter must not be {@literal null}.
* @param startMonth must not be {@literal null}.
*/
public static ShiftedQuarter of(Quarter quarter, Month startMonth) {
return new ShiftedQuarter(quarter, startMonth);
}
/*+
* Creates a new ShiftedQuarter for the given logical {@link Quarter}.
@@ -52,6 +72,15 @@ public class ShiftedQuarter {
return new ShiftedQuarter(quarter, Month.JANUARY);
}
/**
* Returns the logical {@link Quarter}.
*
* @return will never be {@literal null}.
*/
public Quarter getQuarter() {
return quarter;
}
/**
* Returns the next {@link ShiftedQuarter}.
*
@@ -71,25 +100,36 @@ public class ShiftedQuarter {
Assert.notNull(date, "Reference date must not be null!");
MonthDay shiftedStart = getStart();
MonthDay shiftedEnd = getEnd();
MonthDay reference = MonthDay.from(date);
var shiftedStart = getStart();
var shiftedEnd = getEnd();
var reference = MonthDay.from(date);
Stream<Range> ranges = shiftedEnd.isAfter(shiftedStart)
? Stream.of(Range.of(shiftedStart, shiftedEnd))
: Stream.of(Range.of(shiftedStart, LAST_DAY), Range.of(FIRST_DAY, shiftedEnd));
var ranges = shiftedEnd.isAfter(shiftedStart)
? Stream.of(new Range(shiftedStart, shiftedEnd))
: Stream.of(new Range(shiftedStart, LAST_DAY), new Range(FIRST_DAY, shiftedEnd));
return ranges.anyMatch(it -> it.contains(reference));
}
/**
* @return will never be {@literal null}.
*/
public MonthDay getStart() {
return getShifted(quarter.getStart());
}
/**
* @return will never be {@literal null}.
*/
public MonthDay getEnd() {
return getShifted(quarter.getEnd());
}
/**
* Returns whether the given {@link LocalDate} is the last day of the {@link ShiftedQuarter}.
*
* @param date must not be {@literal null}.
*/
public boolean isLastDay(LocalDate date) {
return MonthDay.from(date).equals(getEnd());
}
@@ -122,14 +162,39 @@ public class ShiftedQuarter {
return getStartDate(year).plusMonths(3).minusDays(1);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ShiftedQuarter that)) {
return false;
}
return quarter == that.quarter //
&& startMonth == that.startMonth;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(quarter, startMonth);
}
private MonthDay getShifted(MonthDay source) {
return source.with(source.getMonth().plus(startMonth.getValue() - 1));
}
@Value(staticConstructor = "of")
private static class Range {
MonthDay start, end;
private static record Range(MonthDay start, MonthDay end) {
public boolean contains(MonthDay day) {

View File

@@ -15,18 +15,15 @@
*/
package org.springframework.modulith.moments;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import java.time.LocalDate;
import java.time.Year;
import java.time.temporal.ChronoField;
import java.time.temporal.WeekFields;
import java.util.Locale;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published if a week has passed. The semantics of what constitutes are depended on the
@@ -34,23 +31,41 @@ import org.jmolecules.event.types.DomainEvent;
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class WeekHasPassed implements DomainEvent {
/**
* The year of the week that has just passed.
*/
private final @NonNull Year year;
/**
* The week of the {@link Year} that has just passed.
*/
private final Year year;
private final int week;
private final Locale locale;
/**
* The {@link Locale} to be used to calculate the start date of the week.
* Creates a new {@link WeekHasPassed} for the given {@link Year}, week and {@link Locale}.
*
* @param year must not be {@literal null}.
* @param week must be between 0 and 53 (inclusive).
* @param locale must not be {@literal null}.
*/
private final @NonNull @Getter(AccessLevel.NONE) Locale locale;
WeekHasPassed(Year year, int week, Locale locale) {
Assert.notNull(year, "Year must not be null!");
Assert.isTrue(week >= 0 && week <= 53, "Week must be between 0 and 53!");
Assert.notNull(locale, "Locale must not be null!");
this.year = year;
this.week = week;
this.locale = locale;
}
/**
* Creates a new {@link WeekHasPassed} for the given {@link Year}, week and {@link Locale}.
*
* @param year must not be {@literal null}.
* @param week must be between 0 and 53 (inclusive).
* @param locale must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static WeekHasPassed of(Year year, int week, Locale locale) {
return new WeekHasPassed(year, week, locale);
}
/**
* Creates a new {@link WeekHasPassed} for the given {@link Year} and week of the year.
@@ -63,6 +78,24 @@ public class WeekHasPassed implements DomainEvent {
return WeekHasPassed.of(year, week, Locale.getDefault());
}
/**
* The year of the week that has just passed.
*
* @return will never be {@literal null}.
*/
public Year getYear() {
return year;
}
/**
* The {@link Locale} to be used to calculate the start date of the week.
*
* @return will never be {@literal null}.
*/
public int getWeek() {
return week;
}
/**
* Returns the start date of the week that has passed.
*
@@ -83,4 +116,40 @@ public class WeekHasPassed implements DomainEvent {
public LocalDate getEndDate() {
return getStartDate().plusDays(6);
}
/**
* @return will never be {@literal null}.
*/
Locale getLocale() {
return locale;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof WeekHasPassed that)) {
return false;
}
return week == that.week //
&& Objects.equals(year, that.year) //
&& Objects.equals(locale, that.locale);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(locale, week, year);
}
}

View File

@@ -15,27 +15,44 @@
*/
package org.springframework.modulith.moments;
import lombok.Value;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.util.Objects;
import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;
/**
* A {@link DomainEvent} published on the last day of the year.
*
* @author Oliver Drotbohm
*/
@Value(staticConstructor = "of")
public class YearHasPassed implements DomainEvent {
/**
* The month that has just passed.
*/
private final Year year;
/**
* Creates a new {@link YearHasPassed} for the given {@link Year}.
*
* @param year must not be {@literal null}.
*/
private YearHasPassed(Year year) {
Assert.notNull(year, "Year must not be null!");
this.year = year;
}
/**
* Creates a new {@link YearHasPassed} for the given {@link Year}.
*
* @param year must not be {@literal null}.
*/
public static YearHasPassed of(Year year) {
return new YearHasPassed(year);
}
/**
* Creates a new {@link YearHasPassed} event for the given year.
*
@@ -46,6 +63,15 @@ public class YearHasPassed implements DomainEvent {
return of(Year.of(year));
}
/**
* The {@link Year} that has just passed.
*
* @return will never be {@literal null}.
*/
public Year getYear() {
return year;
}
/**
* Returns the start date of the year passed.
*
@@ -63,4 +89,31 @@ public class YearHasPassed implements DomainEvent {
LocalDate getEndDate() {
return LocalDate.of(year.getValue(), Month.DECEMBER, 31);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof YearHasPassed that)) {
return false;
}
return Objects.equals(year, that.year);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(year);
}
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.moments.support;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -39,21 +36,40 @@ import org.springframework.modulith.moments.ShiftedQuarter;
import org.springframework.modulith.moments.WeekHasPassed;
import org.springframework.modulith.moments.YearHasPassed;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.Assert;
/**
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class Moments {
private static final MonthDay DEC_31ST = MonthDay.of(Month.DECEMBER, 31);
private final @NonNull Clock clock;
private final @NonNull ApplicationEventPublisher events;
private final @NonNull MomentsProperties properties;
private final Clock clock;
private final ApplicationEventPublisher events;
private final MomentsProperties properties;
private Duration shift = Duration.ZERO;
/**
* Creates a new {@link Moments} for the given {@link Clock}, {@link ApplicationEventPublisher} and
* {@link MomentsProperties}.
*
* @param clock must not be {@literal null}.
* @param events must not be {@literal null}.
* @param properties must not be {@literal null}.
*/
public Moments(Clock clock, ApplicationEventPublisher events, MomentsProperties properties) {
Assert.notNull(clock, "Clock must not be null!");
Assert.notNull(events, "ApplicationEventPublisher must not be null!");
Assert.notNull(properties, "MomentsProperties must not be null!");
this.clock = clock;
this.events = events;
this.properties = properties;
}
/**
* Triggers event publication every hour.
*/
@@ -73,48 +89,6 @@ public class Moments {
emitEventsFor(now().toLocalDate().minusDays(1));
}
void emitEventsFor(LocalDateTime time) {
events.publishEvent(HourHasPassed.of(time.truncatedTo(ChronoUnit.HOURS)));
}
void emitEventsFor(LocalDate date) {
// Day has passed
events.publishEvent(DayHasPassed.of(date));
var year = Year.from(date);
// Week has passed
var weekFields = WeekFields.of(properties.getLocale());
var field = weekFields.weekOfWeekBasedYear();
var currentWeek = date.get(field);
var tomorrowsWeek = date.plusDays(1).get(field);
if (tomorrowsWeek != currentWeek) {
var eventYear = Year.of(date.get(weekFields.weekBasedYear()));
events.publishEvent(WeekHasPassed.of(eventYear, currentWeek, properties.getLocale()));
}
// Month has passed
if (date.getDayOfMonth() == date.lengthOfMonth()) {
events.publishEvent(MonthHasPassed.of(YearMonth.from(date)));
}
// Quarter has passed
ShiftedQuarter quarter = properties.getShiftedQuarter(date);
if (quarter.isLastDay(date)) {
events.publishEvent(QuarterHasPassed.of(year, quarter));
}
// Year has passed
if (MonthDay.from(date).equals(DEC_31ST)) {
events.publishEvent(YearHasPassed.of(year));
}
}
Moments shiftBy(Duration duration) {
LocalDateTime before = now();
@@ -153,4 +127,46 @@ public class Moments {
return LocalDateTime.ofInstant(instant, properties.getZoneId());
}
private void emitEventsFor(LocalDateTime time) {
events.publishEvent(HourHasPassed.of(time.truncatedTo(ChronoUnit.HOURS)));
}
private void emitEventsFor(LocalDate date) {
// Day has passed
events.publishEvent(DayHasPassed.of(date));
var year = Year.from(date);
// Week has passed
var weekFields = WeekFields.of(properties.getLocale());
var field = weekFields.weekOfWeekBasedYear();
var currentWeek = date.get(field);
var tomorrowsWeek = date.plusDays(1).get(field);
if (tomorrowsWeek != currentWeek) {
var eventYear = Year.of(date.get(weekFields.weekBasedYear()));
events.publishEvent(WeekHasPassed.of(eventYear, currentWeek, properties.getLocale()));
}
// Month has passed
if (date.getDayOfMonth() == date.lengthOfMonth()) {
events.publishEvent(MonthHasPassed.of(YearMonth.from(date)));
}
// Quarter has passed
ShiftedQuarter quarter = properties.getShiftedQuarter(date);
if (quarter.isLastDay(date)) {
events.publishEvent(QuarterHasPassed.of(year, quarter));
}
// Year has passed
if (MonthDay.from(date).equals(DEC_31ST)) {
events.publishEvent(YearHasPassed.of(year));
}
}
}

View File

@@ -15,11 +15,6 @@
*/
package org.springframework.modulith.moments.support;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.With;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
@@ -27,10 +22,9 @@ import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.lang.Nullable;
import org.springframework.modulith.moments.Quarter;
@@ -43,25 +37,15 @@ import org.springframework.util.Assert;
* @author Oliver Drotbohm
*/
@ConfigurationProperties(prefix = "spring.modulith.moments")
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class MomentsProperties {
public static final MomentsProperties DEFAULTS = new MomentsProperties(null, null, null, (Month) null, false);
private final @With Granularity granularity;
private final Granularity granularity;
private final ZoneId zoneId;
private final Locale locale;
/**
* The {@link ZoneId} to determine times which are attached to the events published. Defaults to
* {@value ZoneOffset#UTC}.
*/
private final @With @Getter ZoneId zoneId;
/**
* The {@link Locale} to use when determining week boundaries. Defaults to {@value Locale#getDefault()}.
*/
private final @With @Getter Locale locale;
private final @Getter boolean enableTimeMachine;
private final boolean enableTimeMachine;
private final ShiftedQuarters quarters;
@@ -88,9 +72,58 @@ public class MomentsProperties {
}
/**
* Returns whether to create hourly events.
* Creates a new {@link MomentsProperties} for the given {@link Granularity}, {@link ZoneId}, {@link Locale}, whether
* to enable the {@link TimeMachine} and {@link ShiftedQuarters}.
*
* @return
* @param granularity must not be {@literal null}.
* @param zoneId must not be {@literal null}.
* @param locale must not be {@literal null}.
* @param enableTimeMachine
* @param quarters must not be {@literal null}.
*/
private MomentsProperties(Granularity granularity, ZoneId zoneId, Locale locale, boolean enableTimeMachine,
ShiftedQuarters quarters) {
Assert.notNull(granularity, "Granilarity must not be null!");
Assert.notNull(zoneId, "ZoneId must not be null!");
Assert.notNull(locale, "Locale must not be null!");
Assert.notNull(quarters, "ShiftedQuarters must not be null!");
this.granularity = granularity;
this.zoneId = zoneId;
this.locale = locale;
this.enableTimeMachine = enableTimeMachine;
this.quarters = quarters;
}
/**
* The {@link ZoneId} to determine times which are attached to the events published. Defaults to
* {@value ZoneOffset#UTC}.
*
* @return will never be {@literal null}.
*/
public ZoneId getZoneId() {
return zoneId;
}
/**
* The {@link Locale} to use when determining week boundaries. Defaults to {@value Locale#getDefault()}.
*
* @return will never be {@literal null}.
*/
public Locale getLocale() {
return locale;
}
/**
* Whether to enable the {@link TimeMachine}.
*/
public boolean isEnableTimeMachine() {
return enableTimeMachine;
}
/**
* Returns whether to create hourly events.
*/
boolean isHourly() {
return Granularity.HOURS.equals(granularity);
@@ -100,7 +133,7 @@ public class MomentsProperties {
* Returns the {@link ShiftedQuarter} for the given reference date.
*
* @param reference must not be {@literal null}.
* @return
* @return will never be {@literal null}.
*/
public ShiftedQuarter getShiftedQuarter(LocalDate reference) {
@@ -109,15 +142,30 @@ public class MomentsProperties {
return quarters.getCurrent(reference);
}
MomentsProperties withGranularity(Granularity granularity) {
return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters);
}
MomentsProperties withZoneId(ZoneId zoneId) {
return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters);
}
MomentsProperties withLocale(Locale locale) {
return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters);
}
static enum Granularity {
HOURS, DAYS;
}
@RequiredArgsConstructor
private static class ShiftedQuarters {
private final List<ShiftedQuarter> quarters;
private ShiftedQuarters(List<ShiftedQuarter> quarters) {
this.quarters = quarters;
}
public ShiftedQuarter getCurrent(LocalDate reference) {
return quarters.stream()

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.observability;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -31,11 +29,22 @@ import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.JavaClass;
@RequiredArgsConstructor
class DefaultObservedModule implements ObservedModule {
private final ApplicationModule module;
/**
* Creates a new {@link DefaultObservedModule} for the given {@link ApplicationModule}.
*
* @param module must not be {@literal null}.
*/
DefaultObservedModule(ApplicationModule module) {
Assert.notNull(module, "ApplicationModule must not be null!");
this.module = module;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.observability.ObservedModule#getName()
@@ -73,8 +82,8 @@ class DefaultObservedModule implements ObservedModule {
// For class-based proxies, use the target class
Advised advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy();
Class<?> targetClass = advised.getTargetClass();
var advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy();
var targetClass = advised.getTargetClass();
if (module.contains(targetClass)) {
return toString(targetClass, method, module);
@@ -141,11 +150,11 @@ class DefaultObservedModule implements ObservedModule {
private static String toString(Class<?> type, Method method, ApplicationModule module) {
String typeName = module.getType(type.getName())
var typeName = module.getType(type.getName())
.map(FormatableType::of)
.map(FormatableType::getAbbreviatedFullName)
.orElseGet(() -> type.getName());
return String.format("%s.%s(…)", typeName, method.getName());
return typeName + "." + method.getName() + "(…)";
}
}

View File

@@ -15,34 +15,44 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.Baggage;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.Tracer.SpanInScope;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
@Slf4j
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class ModuleEntryInterceptor implements MethodInterceptor {
private static Logger LOGGER = LoggerFactory.getLogger(ModuleEntryInterceptor.class);
private static Map<String, ModuleEntryInterceptor> CACHE = new HashMap<>();
private final ObservedModule module;
private final Tracer tracer;
/**
* Creates a new {@link ModuleEntryInterceptor} for the given {@link ObservedModule} and {@link Tracer}.
*
* @param module must not be {@literal null}.
* @param tracer must not be {@literal null}.
*/
private ModuleEntryInterceptor(ObservedModule module, Tracer tracer) {
Assert.notNull(module, "ObservedModule must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.module = module;
this.tracer = tracer;
}
public static ModuleEntryInterceptor of(ObservedModule module, Tracer tracer) {
String name = module.getName();
return CACHE.computeIfAbsent(name, __ -> {
return CACHE.computeIfAbsent(module.getName(), __ -> {
return new ModuleEntryInterceptor(module, tracer);
});
}
@@ -54,23 +64,23 @@ class ModuleEntryInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String moduleName = module.getName();
Span currentSpan = tracer.currentSpan();
var moduleName = module.getName();
var currentSpan = tracer.currentSpan();
if (currentSpan != null) {
Baggage currentBaggage = tracer.getBaggage(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY);
var currentBaggage = tracer.getBaggage(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY);
if (currentBaggage != null && moduleName.equals(currentBaggage.get())) {
return invocation.proceed();
}
}
String invokedMethod = module.getInvokedMethod(invocation);
var invokedMethod = module.getInvokedMethod(invocation);
LOG.trace("Entering {} via {}.", module.getDisplayName(), invokedMethod);
LOGGER.trace("Entering {} via {}.", module.getDisplayName(), invokedMethod);
Span span = tracer.spanBuilder()
var span = tracer.spanBuilder()
.name(moduleName)
.tag("module.method", invokedMethod)
.tag(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY, moduleName)
@@ -84,7 +94,7 @@ class ModuleEntryInterceptor implements MethodInterceptor {
} finally {
LOG.trace("Leaving {}", module.getDisplayName());
LOGGER.trace("Leaving {}", module.getDisplayName());
span.end();
}

View File

@@ -15,27 +15,39 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
/**
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class ModuleEventListener implements ApplicationListener<ApplicationEvent> {
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
/**
* Creates a new {@link ModuleEventListener} for the given {@link ApplicationModulesRuntime} and {@link Tracer}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
*/
public ModuleEventListener(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.runtime = runtime;
this.tracer = tracer;
}
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
@@ -43,19 +55,18 @@ public class ModuleEventListener implements ApplicationListener<ApplicationEvent
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (!PayloadApplicationEvent.class.isInstance(event)) {
if (!(event instanceof PayloadApplicationEvent<?> payloadEvent)) {
return;
}
PayloadApplicationEvent<?> foo = (PayloadApplicationEvent<?>) event;
Object object = foo.getPayload();
Class<? extends Object> payloadType = object.getClass();
var object = payloadEvent.getPayload();
var payloadType = object.getClass();
if (!runtime.isApplicationClass(payloadType)) {
return;
}
ApplicationModule moduleByType = runtime.get()
var moduleByType = runtime.get()
.getModuleByType(payloadType.getSimpleName())
.orElse(null);
@@ -63,7 +74,7 @@ public class ModuleEventListener implements ApplicationListener<ApplicationEvent
return;
}
Span span = tracer.get().currentSpan();
var span = tracer.get().currentSpan();
if (span == null) {
return;

View File

@@ -16,17 +16,13 @@
package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcher;
@@ -34,6 +30,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
/**
* A {@link BeanPostProcessor} that decorates beans exposed by application modules with an interceptor that registers
@@ -41,14 +38,30 @@ import org.springframework.modulith.runtime.ApplicationModulesRuntime;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor {
public static final String MODULE_BAGGAGE_KEY = "org.springframework.modulith.module";
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
private final Map<String, Advisor> advisors = new HashMap<>();
private final Map<String, Advisor> advisors;
/**
* Creates a new {@link ModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime} and
* {@link Tracer}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
*/
public ModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.runtime = runtime;
this.tracer = tracer;
this.advisors = new HashMap<>();
}
/*
* (non-Javadoc)
@@ -69,7 +82,7 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
.map(DefaultObservedModule::new)
.map(it -> {
ObservedModuleType moduleType = it.getObservedModuleType(type, modules);
var moduleType = it.getObservedModuleType(type, modules);
return moduleType != null //
? addAdvisor(bean, getOrBuildAdvisor(it, moduleType)) //
@@ -82,19 +95,30 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
return advisors.computeIfAbsent(module.getName(), __ -> {
Advice interceptor = ModuleEntryInterceptor.of(module, tracer.get());
MethodMatcher matcher = new ObservableTypeMethodMatcher(type);
Pointcut pointcut = new ComposablePointcut(matcher);
var interceptor = ModuleEntryInterceptor.of(module, tracer.get());
var matcher = new ObservableTypeMethodMatcher(type);
var pointcut = new ComposablePointcut(matcher);
return new DefaultPointcutAdvisor(pointcut, interceptor);
});
}
@RequiredArgsConstructor
private static class ObservableTypeMethodMatcher extends StaticMethodMatcher {
private final ObservedModuleType type;
/**
* Creates a new {@link ObservableTypeMethodMatcher} for the given {@link ObservedModuleType}.
*
* @param type must not be {@literal null}.
*/
private ObservableTypeMethodMatcher(ObservedModuleType type) {
Assert.notNull(type, "ObservableModuleType must not be null!");
this.type = type;
}
/*
* (non-Javadoc)
* @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, java.lang.Class)

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.observability;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
@@ -28,6 +26,7 @@ import org.springframework.aop.framework.Advised;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.ArchitecturallyEvidentType;
import org.springframework.modulith.model.ArchitecturallyEvidentType.ReferenceMethod;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
@@ -35,7 +34,6 @@ import org.springframework.util.ReflectionUtils;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class ObservedModuleType {
private static Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
@@ -44,6 +42,25 @@ public class ObservedModuleType {
private final ObservedModule module;
private final ArchitecturallyEvidentType type;
/**
* Creates a new {@link ObservedModuleType} for the given {@link ApplicationModules}, {@link ObservedModule} and
* {@link ArchitecturallyEvidentType}.
*
* @param modules must not be {@literal null}.
* @param module must not be {@literal null}.
* @param type must not be {@literal null}.
*/
ObservedModuleType(ApplicationModules modules, ObservedModule module, ArchitecturallyEvidentType type) {
Assert.notNull(modules, "ApplicationModules must not be null!");
Assert.notNull(module, "ObservedModule must not be null!");
Assert.notNull(type, "ArchitecturallyEvidentType must not be null!");
this.modules = modules;
this.module = module;
this.type = type;
}
/**
* Returns whether the type should be traced at all. Can be skipped for types not exposed by the module unless they
* listen to events of other modules.

View File

@@ -16,7 +16,6 @@
package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
import java.util.function.Supplier;
@@ -33,16 +32,32 @@ import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
/**
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor {
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
/**
* Creates a new {@link SpringDataRestModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime}
* and {@link Tracer}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
*/
public SpringDataRestModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.runtime = runtime;
this.tracer = tracer;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
@@ -62,12 +77,26 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
return addAdvisor(bean, advisor, it -> it.setProxyTargetClass(true));
}
@RequiredArgsConstructor
private static class DataRestControllerInterceptor implements MethodInterceptor {
private final Supplier<ApplicationModules> modules;
private final Supplier<Tracer> tracer;
/**
* Creates a new {@link DataRestControllerInterceptor} for the given {@link ApplicationModules} and {@link Tracer}.
*
* @param modules must not be {@literal null}.
* @param tracer must not be {@literal null}.
*/
private DataRestControllerInterceptor(Supplier<ApplicationModules> modules, Supplier<Tracer> tracer) {
Assert.notNull(modules, "ApplicationModules must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.modules = modules;
this.tracer = tracer;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
@@ -75,13 +104,13 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
ApplicationModule module = getModuleFrom(invocation.getArguments());
var module = getModuleFrom(invocation.getArguments());
if (module == null) {
return invocation.proceed();
}
ObservedModule observed = new DefaultObservedModule(module);
var observed = new DefaultObservedModule(module);
return ModuleEntryInterceptor.of(observed, tracer.get()).invoke(invocation);
}
@@ -90,17 +119,14 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
for (Object argument : arguments) {
if (!RootResourceInformation.class.isInstance(arguments)) {
if (!(argument instanceof RootResourceInformation info)) {
continue;
}
RootResourceInformation info = (RootResourceInformation) argument;
return modules.get().getModuleByType(info.getDomainType().getName()).orElse(null);
}
return null;
}
}
}

View File

@@ -15,12 +15,10 @@
*/
package org.springframework.modulith.runtime;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.function.Supplier;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.util.Assert;
/**
* Bootstrap type to make sure we only bootstrap the initialization of a {@link ApplicationModules} instance once per
@@ -28,11 +26,26 @@ import org.springframework.modulith.model.ApplicationModules;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
public class ApplicationModulesRuntime implements Supplier<ApplicationModules> {
private final @NonNull Supplier<ApplicationModules> modules;
private final @NonNull ApplicationRuntime runtime;
private final Supplier<ApplicationModules> modules;
private final ApplicationRuntime runtime;
/**
* Creates a new {@link ApplicationModulesRuntime} for the given {@link ApplicationModules} and
* {@link ApplicationRuntime}.
*
* @param modules must not be {@literal null}.
* @param runtime must not be {@literal null}.
*/
public ApplicationModulesRuntime(Supplier<ApplicationModules> modules, ApplicationRuntime runtime) {
Assert.notNull(modules, "ApplicationModules must not be null!");
Assert.notNull(runtime, "ApplicationRuntime must not be null!");
this.modules = modules;
this.runtime = runtime;
}
/*
* (non-Javadoc)

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.runtime.autoconfigure;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -25,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.modulith.runtime.ApplicationRuntime;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -33,7 +32,6 @@ import org.springframework.util.ClassUtils;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
class SpringBootApplicationRuntime implements ApplicationRuntime {
private static final Map<String, Boolean> APPLICATION_CLASSES = new ConcurrentHashMap<>();
@@ -41,6 +39,18 @@ class SpringBootApplicationRuntime implements ApplicationRuntime {
private final ApplicationContext context;
private Class<?> mainApplicationClass;
/**
* Creates a new {@link SpringBootApplicationRuntime} for the given {@link ApplicationContext}.
*
* @param context must not be {@literal null}.
*/
SpringBootApplicationRuntime(ApplicationContext context) {
Assert.notNull(context, "ApplicationContext must not be null!");
this.context = context;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.observability.ApplicationRuntime#getId()

View File

@@ -15,14 +15,13 @@
*/
package org.springframework.modulith.runtime.autoconfigure;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -38,6 +37,7 @@ import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.FormatableType;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.modulith.runtime.ApplicationRuntime;
import org.springframework.util.Assert;
/**
* Auto-configuration to register a {@link SpringBootApplicationRuntime} and {@link ApplicationModulesRuntime} as Spring
@@ -45,10 +45,11 @@ import org.springframework.modulith.runtime.ApplicationRuntime;
*
* @author Oliver Drotbohm
*/
@Slf4j
@AutoConfiguration
class SpringModulithRuntimeAutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringModulithRuntimeAutoConfiguration.class);
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(ApplicationRuntime.class)
@@ -78,18 +79,35 @@ class SpringModulithRuntimeAutoConfiguration {
initializers.stream() //
.sorted(modules.getComparator()) //
.map(it -> LOG.isDebugEnabled() ? new LoggingApplicationModuleInitializerAdapter(it, modules) : it)
.map(it -> LOGGER.isDebugEnabled() ? new LoggingApplicationModuleInitializerAdapter(it, modules) : it)
.forEach(ApplicationModuleInitializer::initialize);
};
}
@Slf4j
@RequiredArgsConstructor
private static class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingApplicationModuleInitializerAdapter.class);
private final ApplicationModuleInitializer delegate;
private final ApplicationModules modules;
/**
* Creates a new {@link LoggingApplicationModuleInitializerAdapter} for the given
* {@link ApplicationModuleInitializer} and {@link ApplicationModule}.
*
* @param delegate must not be {@literal null}.
* @param modules must not be {@literal null}.
*/
public LoggingApplicationModuleInitializerAdapter(ApplicationModuleInitializer delegate,
ApplicationModules modules) {
Assert.notNull(delegate, "ApplicationModuleInitializer must not be null!");
Assert.notNull(modules, "ApplicationModules must not be null!");
this.delegate = delegate;
this.modules = modules;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.ApplicationModuleInitializer#initialize()
@@ -104,31 +122,32 @@ class SpringModulithRuntimeAutoConfiguration {
.map(formattable::getAbbreviatedFullName)
.orElseGet(formattable::getAbbreviatedFullName);
LOG.debug("Initializing {}.", formattedListenerType);
LOGGER.debug("Initializing {}.", formattedListenerType);
delegate.initialize();
LOG.debug("Initializing {} done.", formattedListenerType);
LOGGER.debug("Initializing {} done.", formattedListenerType);
}
}
@Slf4j
private static class ApplicationModulesBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesBootstrap.class);
static ApplicationModules initializeApplicationModules(Class<?> applicationMainClass) {
LOG.debug("Obtaining Spring Modulith application modules…");
LOGGER.debug("Obtaining Spring Modulith application modules…");
var result = ApplicationModules.of(applicationMainClass);
var numberOfModules = result.stream().count();
if (numberOfModules == 0) {
LOG.warn("No application modules detected!");
LOGGER.warn("No application modules detected!");
} else {
LOG.debug("Detected {} application modules: {}", //
LOGGER.debug("Detected {} application modules: {}", //
result.stream().count(), //
result.stream().map(ApplicationModule::getName).toList());
}
@@ -144,7 +163,6 @@ class SpringModulithRuntimeAutoConfiguration {
return modules.get();
} catch (Exception o_O) {
throw new RuntimeException(o_O);
// TODO: handle exception
}
};
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.test;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -35,8 +32,8 @@ import org.springframework.test.context.TestConstructor.AutowireMode;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Bootstraps the module containing the package of the test class annotated with {@link ApplicationModuleTest}. Will apply the
* following modifications to the Spring Boot configuration:
* Bootstraps the module containing the package of the test class annotated with {@link ApplicationModuleTest}. Will
* apply the following modifications to the Spring Boot configuration:
* <ul>
* <li>Restricts the component scanning to the module's package.
* <li>
@@ -76,7 +73,6 @@ public @interface ApplicationModuleTest {
*/
String[] extraIncludes() default {};
@RequiredArgsConstructor
public enum BootstrapMode {
/**
@@ -94,6 +90,19 @@ public @interface ApplicationModuleTest {
*/
ALL_DEPENDENCIES(DependencyDepth.ALL);
private final @Getter DependencyDepth depth;
private final DependencyDepth depth;
private BootstrapMode(DependencyDepth depth) {
this.depth = depth;
}
/**
* Returns the {@link DependencyDepth} associated with the {@link BootstrapMode}.
*
* @return will never be {@literal null}.
*/
public DependencyDepth getDepth() {
return depth;
}
}
}

View File

@@ -15,21 +15,31 @@
*/
package org.springframework.modulith.test;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.util.Assert;
/**
* Default implementation of {@link AssertablePublishedEvents}.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor
class DefaultAssertablePublishedEvents implements AssertablePublishedEvents, ApplicationListener<ApplicationEvent> {
private final DefaultPublishedEvents delegate;
/**
* Creates a new {@link DefaultAssertablePublishedEvents} with the given {@link DefaultPublishedEvents} delegate.
*
* @param delegate must not be {@literal null}.
*/
DefaultAssertablePublishedEvents(DefaultPublishedEvents delegate) {
Assert.notNull(delegate, "DefaultPublishedEvents must not be null!");
this.delegate = delegate;
}
/**
* Creates a new {@link DefaultAssertablePublishedEvents}.
*/

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.test;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -88,11 +86,14 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener<App
: source;
}
@RequiredArgsConstructor(staticName = "of")
private static class SimpleTypedPublishedEvents<T> implements TypedPublishedEvents<T> {
private final List<T> events;
private SimpleTypedPublishedEvents(List<T> events) {
this.events = events;
}
private static <T> SimpleTypedPublishedEvents<T> of(Stream<T> stream) {
return new SimpleTypedPublishedEvents<>(stream.toList());
}

View File

@@ -15,17 +15,15 @@
*/
package org.springframework.modulith.test;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.modulith.model.ApplicationModule;
@@ -47,16 +45,14 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
ApplicationModuleTest moduleTest = AnnotatedElementUtils.getMergedAnnotation(testClass,
ApplicationModuleTest.class);
var moduleTest = AnnotatedElementUtils.getMergedAnnotation(testClass, ApplicationModuleTest.class);
return moduleTest == null ? null : new ModuleContextCustomizer(testClass);
}
@Slf4j
@EqualsAndHashCode
static class ModuleContextCustomizer implements ContextCustomizer {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleContextCustomizer.class);
private static final String BEAN_NAME = ModuleTestExecution.class.getName();
private final Supplier<ModuleTestExecution> execution;
@@ -72,14 +68,14 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ModuleTestExecution testExecution = execution.get();
var testExecution = execution.get();
logModules(testExecution);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
var beanFactory = context.getBeanFactory();
beanFactory.registerSingleton(BEAN_NAME, testExecution);
DefaultPublishedEvents events = new DefaultPublishedEvents();
var events = new DefaultPublishedEvents();
beanFactory.registerSingleton(events.getClass().getName(), events);
context.addApplicationListener(events);
}
@@ -94,48 +90,47 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
var message = "Bootstrapping @%s for %s in mode %s (%s)…"
.formatted(ApplicationModuleTest.class.getName(), moduleName, bootstrapMode, modules.getModulithSource());
LOG.info(message);
LOG.info("");
LOGGER.info(message);
LOGGER.info("");
Arrays.stream(module.toString(modules).split("\n")).forEach(LOG::info);
Arrays.stream(module.toString(modules).split("\n")).forEach(LOGGER::info);
List<ApplicationModule> extraIncludes = execution.getExtraIncludes();
var extraIncludes = execution.getExtraIncludes();
if (!extraIncludes.isEmpty()) {
logHeadline("Extra includes:");
LOG.info("> " + extraIncludes.stream().map(ApplicationModule::getName).collect(Collectors.joining(", ")));
LOGGER.info("> " + extraIncludes.stream().map(ApplicationModule::getName).collect(Collectors.joining(", ")));
}
Set<ApplicationModule> sharedModules = modules.getSharedModules();
var sharedModules = modules.getSharedModules();
if (!sharedModules.isEmpty()) {
logHeadline("Shared modules:");
LOG.info("> " + sharedModules.stream().map(ApplicationModule::getName).collect(Collectors.joining(", ")));
LOGGER.info("> " + sharedModules.stream().map(ApplicationModule::getName).collect(Collectors.joining(", ")));
}
List<ApplicationModule> dependencies = execution.getDependencies();
var dependencies = execution.getDependencies();
if (!dependencies.isEmpty() || !sharedModules.isEmpty()) {
logHeadline("Included dependencies:");
Stream<ApplicationModule> dependenciesPlusMissingSharedOnes = //
Stream.concat(dependencies.stream(), sharedModules.stream() //
.filter(it -> !dependencies.contains(it)));
var dependenciesPlusMissingSharedOnes = Stream.concat(dependencies.stream(), sharedModules.stream() //
.filter(it -> !dependencies.contains(it)));
dependenciesPlusMissingSharedOnes //
.map(it -> it.toString(modules)) //
.forEach(it -> {
LOG.info("");
Arrays.stream(it.split("\n")).forEach(LOG::info);
LOGGER.info("");
Arrays.stream(it.split("\n")).forEach(LOGGER::info);
});
}
LOG.info("");
LOGGER.info("");
}
private static void logHeadline(String headline) {
@@ -144,9 +139,36 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
private static void logHeadline(String headline, Runnable additional) {
LOG.info("");
LOG.info(headline);
LOGGER.info("");
LOGGER.info(headline);
additional.run();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ModuleContextCustomizer that)) {
return false;
}
return Objects.equals(execution, that.execution);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(execution);
}
}
}

View File

@@ -15,16 +15,14 @@
*/
package org.springframework.modulith.test;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
@@ -43,7 +41,7 @@ import org.springframework.util.StringUtils;
*
* @author Oliver Drotbohm
*/
@Configuration
@Configuration(proxyBeanMethods = false)
@Import(ModuleTestAutoConfiguration.AutoConfigurationAndEntityScanPackageCustomizer.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
class ModuleTestAutoConfiguration {
@@ -51,9 +49,10 @@ class ModuleTestAutoConfiguration {
private static final String AUTOCONFIG_PACKAGES = "org.springframework.boot.autoconfigure.AutoConfigurationPackages";
private static final String ENTITY_SCAN_PACKAGE = "org.springframework.boot.autoconfigure.domain.EntityScanPackages";
@Slf4j
static class AutoConfigurationAndEntityScanPackageCustomizer implements ImportBeanDefinitionRegistrar {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoConfigurationAndEntityScanPackageCustomizer.class);
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
@@ -61,10 +60,10 @@ class ModuleTestAutoConfiguration {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ModuleTestExecution execution = ((BeanFactory) registry).getBean(ModuleTestExecution.class);
List<String> basePackages = execution.getBasePackages().toList();
var execution = ((BeanFactory) registry).getBean(ModuleTestExecution.class);
var basePackages = execution.getBasePackages().toList();
LOG.info("Re-configuring auto-configuration and entity scan packages to: {}.",
LOGGER.info("Re-configuring auto-configuration and entity scan packages to: {}.",
StringUtils.collectionToDelimitedString(basePackages, ", "));
setBasePackagesOn(registry, AUTOCONFIG_PACKAGES, "BasePackagesBeanDefinition", "basePackages", basePackages);
@@ -80,10 +79,10 @@ class ModuleTestAutoConfiguration {
return;
}
BeanDefinition definition = registry.getBeanDefinition(beanName);
var definition = registry.getBeanDefinition(beanName);
// For Boot 2.4, we deal with a BasePackagesBeanDefinition
Field field = Arrays.stream(definition.getClass().getDeclaredFields())
var field = Arrays.stream(definition.getClass().getDeclaredFields())
.filter(__ -> definition.getClass().getSimpleName().equals(definitionType))
.filter(it -> it.getName().equals(fieldName))
.findFirst()

View File

@@ -15,20 +15,16 @@
*/
package org.springframework.modulith.test;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.AnnotatedClassFinder;
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -43,26 +39,26 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* @author Oliver Drotbohm
*/
@Slf4j
@EqualsAndHashCode(of = "key")
public class ModuleTestExecution implements Iterable<ApplicationModule> {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecution.class);
private static Map<Class<?>, Class<?>> MODULITH_TYPES = new HashMap<>();
private static Map<Key, ModuleTestExecution> EXECUTIONS = new HashMap<>();
private final Key key;
private final @Getter BootstrapMode bootstrapMode;
private final @Getter ApplicationModule module;
private final @Getter ApplicationModules modules;
private final @Getter List<ApplicationModule> extraIncludes;
private final BootstrapMode bootstrapMode;
private final ApplicationModule module;
private final ApplicationModules modules;
private final List<ApplicationModule> extraIncludes;
private final Supplier<List<JavaPackage>> basePackages;
private final Supplier<List<ApplicationModule>> dependencies;
private ModuleTestExecution(ApplicationModuleTest annotation, ApplicationModules modules, ApplicationModule module) {
this.key = Key.of(module.getBasePackage().getName(), annotation);
this.key = new Key(module.getBasePackage().getName(), annotation);
this.modules = modules;
this.bootstrapMode = annotation.mode();
this.module = module;
@@ -71,18 +67,18 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
this.basePackages = Suppliers.memoize(() -> {
Stream<JavaPackage> moduleBasePackages = module.getBootstrapBasePackages(modules, bootstrapMode.getDepth());
Stream<JavaPackage> sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage());
Stream<JavaPackage> extraPackages = extraIncludes.stream().map(ApplicationModule::getBasePackage);
var moduleBasePackages = module.getBootstrapBasePackages(modules, bootstrapMode.getDepth());
var sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage());
var extraPackages = extraIncludes.stream().map(ApplicationModule::getBasePackage);
Stream<JavaPackage> intermediate = Stream.concat(moduleBasePackages, extraPackages);
var intermediate = Stream.concat(moduleBasePackages, extraPackages);
return Stream.concat(intermediate, sharedBasePackages).distinct().toList();
});
this.dependencies = Suppliers.memoize(() -> {
Stream<ApplicationModule> bootstrapDependencies = module.getBootstrapDependencies(modules,
var bootstrapDependencies = module.getBootstrapDependencies(modules,
bootstrapMode.getDepth());
return Stream.concat(bootstrapDependencies, extraIncludes.stream()).toList();
});
@@ -96,17 +92,16 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
return () -> {
ApplicationModuleTest annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class);
String packageName = type.getPackage().getName();
var annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class);
var packageName = type.getPackage().getName();
Class<?> modulithType = MODULITH_TYPES.computeIfAbsent(type,
var modulithType = MODULITH_TYPES.computeIfAbsent(type,
it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromPackage(packageName));
ApplicationModules modules = ApplicationModules.of(modulithType);
ApplicationModule module = modules.getModuleForPackage(packageName) //
.orElseThrow(
() -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName)));
var modules = ApplicationModules.of(modulithType);
var module = modules.getModuleForPackage(packageName).orElseThrow( //
() -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName)));
return EXECUTIONS.computeIfAbsent(Key.of(module.getBasePackage().getName(), annotation),
return EXECUTIONS.computeIfAbsent(new Key(module.getBasePackage().getName(), annotation),
it -> new ModuleTestExecution(annotation, modules, module));
};
}
@@ -122,11 +117,11 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
public boolean includes(String className) {
boolean result = modules.withinRootPackages(className) //
var result = modules.withinRootPackages(className) //
|| basePackages.get().stream().anyMatch(it -> it.contains(className));
if (result) {
LOG.trace("Including class {}.", className);
LOGGER.trace("Including class {}.", className);
}
return !result;
@@ -155,6 +150,42 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
module.verifyDependencies(modules);
}
/**
* Returns the {@link BootstrapMode} to be used for the executions.
*
* @return will never be {@literal null}.
*/
public BootstrapMode getBootstrapMode() {
return bootstrapMode;
}
/**
* Returns the primary {@link ApplicationModule} to bootstrap.
*
* @return the module will never be {@literal null}.
*/
public ApplicationModule getModule() {
return module;
}
/**
* Returns all {@link ApplicationModules} of the application.
*
* @return the modules will never be {@literal null}.
*/
public ApplicationModules getModules() {
return modules;
}
/**
* Returns all {@link ApplicationModule}s registered as extra includes for the execution.
*
* @return the extraIncludes will never be {@literal null}.
*/
public List<ApplicationModule> getExtraIncludes() {
return extraIncludes;
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
@@ -164,6 +195,33 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
return modules.iterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ModuleTestExecution that)) {
return false;
}
return Objects.equals(key, that.key);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(key);
}
private static Stream<ApplicationModule> getExtraModules(ApplicationModuleTest annotation,
ApplicationModules modules) {
@@ -172,11 +230,5 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
}
@Value
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
private static class Key {
String moduleBasePackage;
ApplicationModuleTest annotation;
}
private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {}
}

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.modulith.test;
import lombok.EqualsAndHashCode;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Supplier;
import org.springframework.boot.context.TypeExcludeFilter;
@@ -27,7 +26,6 @@ import org.springframework.core.type.classreading.MetadataReaderFactory;
/**
* @author Oliver Drotbohm
*/
@EqualsAndHashCode(callSuper = false)
class ModuleTypeExcludeFilter extends TypeExcludeFilter {
private final Supplier<ModuleTestExecution> execution;
@@ -36,7 +34,7 @@ class ModuleTypeExcludeFilter extends TypeExcludeFilter {
this.execution = ModuleTestExecution.of(testClass);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.boot.context.TypeExcludeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory)
*/
@@ -44,4 +42,31 @@ class ModuleTypeExcludeFilter extends TypeExcludeFilter {
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return execution.get().includes(metadataReader.getClassMetadata().getClassName());
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ModuleTypeExcludeFilter that)) {
return false;
}
return Objects.equals(execution, that.execution);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(execution);
}
}

View File

@@ -17,9 +17,6 @@ package org.springframework.modulith.test;
import static org.assertj.core.api.Assertions.*;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -69,11 +66,22 @@ public class PublishedEventsAssert extends AbstractAssert<PublishedEventsAssert,
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class PublishedEventAssert<T> {
private final TypedPublishedEvents<T> events;
/**
* Creates a new {@link PublishedEventAssert} for the given {@link TypedPublishedEvents}.
*
* @param events must not be {@literal null}.
*/
private PublishedEventAssert(TypedPublishedEvents<T> events) {
Assert.notNull(events, "TypedPublishedEvents must not be null!");
this.events = events;
}
/**
* Asserts that at least one event matches the given predicate.
*