Skip to content

Commit 9148a57

Browse files
committed
GH-317 - Detect violations from types located in root packages.
We now create artificial root application modules for all root packages to detect violations (for example, types located in root packages referring to module-internal types).
1 parent 4a857dc commit 9148a57

File tree

6 files changed

+151
-23
lines changed

6 files changed

+151
-23
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,28 @@ public Violations detectDependencies(ApplicationModules modules) {
317317
.reduce(Violations.NONE, Violations::and);
318318
}
319319

320+
/**
321+
* Returns whether the module is considered a root one, i.e., it is an artificial one created for each base package
322+
* configured.
323+
*
324+
* @return whether the module is considered a root one.
325+
* @since 1.1
326+
*/
327+
public boolean isRootModule() {
328+
return false;
329+
}
330+
331+
/**
332+
* Returns whether the module has a base package with the given name.
333+
*
334+
* @param candidate must not be {@literal null} or empty.
335+
* @return whether the module has a base package with the given name.
336+
* @since 1.1
337+
*/
338+
boolean hasBasePackage(String candidate) {
339+
return basePackage.getName().equals(candidate);
340+
}
341+
320342
/*
321343
* (non-Javadoc)
322344
* @see java.lang.Object#toString()

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.*;
2323
import java.util.concurrent.ConcurrentHashMap;
2424
import java.util.function.Function;
25+
import java.util.function.Supplier;
2526
import java.util.stream.Collectors;
2627
import java.util.stream.Stream;
2728
import java.util.stream.StreamSupport;
@@ -31,16 +32,13 @@
3132
import org.jgrapht.graph.DefaultEdge;
3233
import org.jgrapht.traverse.TopologicalOrderIterator;
3334
import org.jmolecules.archunit.JMoleculesDddRules;
34-
import org.springframework.core.Ordered;
3535
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
36-
import org.springframework.core.annotation.Order;
3736
import org.springframework.core.io.support.SpringFactoriesLoader;
3837
import org.springframework.lang.Nullable;
39-
import org.springframework.modulith.Modulith;
40-
import org.springframework.modulith.Modulithic;
4138
import org.springframework.modulith.core.Types.JMoleculesTypes;
4239
import org.springframework.util.Assert;
4340
import org.springframework.util.ClassUtils;
41+
import org.springframework.util.function.SingletonSupplier;
4442

4543
import com.tngtech.archunit.base.DescribedPredicate;
4644
import com.tngtech.archunit.core.domain.JavaClass;
@@ -86,6 +84,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
8684
private final Map<String, ApplicationModule> modules;
8785
private final JavaClasses allClasses;
8886
private final List<JavaPackage> rootPackages;
87+
private final Supplier<List<ApplicationModule>> rootModules;
8988
private final Set<ApplicationModule> sharedModules;
9089
private final List<String> orderedNames;
9190

@@ -112,6 +111,10 @@ protected ApplicationModules(ModulithMetadata metadata, Collection<String> packa
112111
.map(it -> JavaPackage.of(classes, it).toSingle()) //
113112
.toList();
114113

114+
this.rootModules = SingletonSupplier.of(() -> rootPackages.stream()
115+
.map(ApplicationModules::rootModuleFor)
116+
.toList());
117+
115118
this.sharedModules = Collections.emptySet();
116119

117120
this.orderedNames = JGRAPHT_PRESENT //
@@ -121,32 +124,35 @@ protected ApplicationModules(ModulithMetadata metadata, Collection<String> packa
121124

122125
/**
123126
* Creates a new {@link ApplicationModules} for the given {@link ModulithMetadata}, {@link ApplicationModule}s,
124-
* {@link JavaClasses}, {@link JavaPackage}s, shared {@link ApplicationModule}s, ordered module names and verified
125-
* flag.
127+
* {@link JavaClasses}, {@link JavaPackage}s, root and shared {@link ApplicationModule}s, ordered module names and
128+
* verified flag.
126129
*
127130
* @param metadata must not be {@literal null}.
128131
* @param modules must not be {@literal null}.
129132
* @param allClasses must not be {@literal null}.
130133
* @param rootPackages must not be {@literal null}.
134+
* @param rootModules must not be {@literal null}.
131135
* @param sharedModules must not be {@literal null}.
132136
* @param orderedNames must not be {@literal null}.
133137
* @param verified
134138
*/
135139
private ApplicationModules(ModulithMetadata metadata, Map<String, ApplicationModule> modules, JavaClasses classes,
136-
List<JavaPackage> rootPackages, Set<ApplicationModule> sharedModules, List<String> orderedNames,
137-
boolean verified) {
140+
List<JavaPackage> rootPackages, Supplier<List<ApplicationModule>> rootModules,
141+
Set<ApplicationModule> sharedModules, List<String> orderedNames, boolean verified) {
138142

139143
Assert.notNull(metadata, "ModulithMetadata must not be null!");
140144
Assert.notNull(modules, "Application modules must not be null!");
141145
Assert.notNull(classes, "JavaClasses must not be null!");
142146
Assert.notNull(rootPackages, "Root JavaPackages must not be null!");
147+
Assert.notNull(rootModules, "Root modules must not be null!");
143148
Assert.notNull(sharedModules, "Shared ApplicationModules must not be null!");
144149
Assert.notNull(orderedNames, "Ordered application module names must not be null!");
145150

146151
this.metadata = metadata;
147152
this.modules = modules;
148153
this.allClasses = classes;
149154
this.rootPackages = rootPackages;
155+
this.rootModules = rootModules;
150156
this.sharedModules = sharedModules;
151157
this.orderedNames = orderedNames;
152158
this.verified = verified;
@@ -296,7 +302,7 @@ public Optional<ApplicationModule> getModuleByType(JavaClass type) {
296302

297303
Assert.notNull(type, "Type must not be null!");
298304

299-
return modules.values().stream() //
305+
return allModules() //
300306
.filter(it -> it.contains(type)) //
301307
.findFirst();
302308
}
@@ -311,7 +317,7 @@ public Optional<ApplicationModule> getModuleByType(String candidate) {
311317

312318
Assert.hasText(candidate, "Candidate must not be null or empty!");
313319

314-
return modules.values().stream() //
320+
return allModules() //
315321
.filter(it -> it.contains(candidate)) //
316322
.findFirst();
317323
}
@@ -328,15 +334,20 @@ public Optional<ApplicationModule> getModuleByType(Class<?> candidate) {
328334

329335
/**
330336
* Returns the {@link ApplicationModule} containing the given package.
331-
*
337+
*
332338
* @param name must not be {@literal null} or empty.
333339
* @return will never be {@literal null}.
334340
*/
335341
public Optional<ApplicationModule> getModuleForPackage(String name) {
336342

337343
return modules.values().stream() //
338344
.filter(it -> it.containsPackage(name)) //
339-
.findFirst();
345+
.findFirst()
346+
.or(() -> {
347+
return rootModules.get().stream()
348+
.filter(it -> it.hasBasePackage(name))
349+
.findFirst();
350+
});
340351
}
341352

342353
/**
@@ -383,7 +394,7 @@ public Violations detectViolations() {
383394
}
384395
}
385396

386-
return modules.values().stream() //
397+
return Stream.concat(rootModules.get().stream(), modules.values().stream()) //
387398
.map(it -> it.detectDependencies(this)) //
388399
.reduce(violations, Violations::and);
389400
}
@@ -458,7 +469,8 @@ public String toString() {
458469
}
459470

460471
private ApplicationModules withSharedModules(Set<ApplicationModule> sharedModules) {
461-
return new ApplicationModules(metadata, modules, allClasses, rootPackages, sharedModules, orderedNames, verified);
472+
return new ApplicationModules(metadata, modules, allClasses, rootPackages, rootModules, sharedModules, orderedNames,
473+
verified);
462474
}
463475

464476
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
@@ -506,6 +518,16 @@ private ApplicationModule getRequiredModule(String moduleName) {
506518
return module;
507519
}
508520

521+
/**
522+
* Returns of all {@link ApplicationModule}s, including root ones (last).
523+
*
524+
* @return will never be {@literal null}.
525+
* @since 1.1
526+
*/
527+
private Stream<ApplicationModule> allModules() {
528+
return Stream.concat(modules.values().stream(), rootModules.get().stream());
529+
}
530+
509531
/**
510532
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
511533
*
@@ -532,6 +554,29 @@ private static ApplicationModules of(CacheKey key) {
532554
return modules.withSharedModules(sharedModules);
533555
}
534556

557+
/**
558+
* Creates a special root {@link ApplicationModule} for the given {@link JavaPackage}.
559+
*
560+
* @param javaPackage must not be {@literal null}.
561+
* @return will never be {@literal null}.
562+
* @since 1.1
563+
*/
564+
private static ApplicationModule rootModuleFor(JavaPackage javaPackage) {
565+
566+
return new ApplicationModule(javaPackage, true) {
567+
568+
@Override
569+
public String getName() {
570+
return "root:" + super.getName();
571+
}
572+
573+
@Override
574+
public boolean isRootModule() {
575+
return true;
576+
}
577+
};
578+
}
579+
535580
public static class Filters {
536581

537582
public static DescribedPredicate<JavaClass> withoutModules(String... names) {

spring-modulith-core/src/main/java/org/springframework/modulith/core/Violations.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.List;
2121
import java.util.stream.Collector;
2222
import java.util.stream.Collectors;
23-
import java.util.stream.Stream;
2423

2524
import org.springframework.util.Assert;
2625

@@ -70,6 +69,19 @@ public String getMessage() {
7069
.collect(Collectors.joining("\n- ", "- ", ""));
7170
}
7271

72+
/**
73+
* Returns all violations' messages.
74+
*
75+
* @return will never be {@literal null}.
76+
* @since 1.1
77+
*/
78+
public List<String> getMessages() {
79+
80+
return exceptions.stream() //
81+
.map(RuntimeException::getMessage)
82+
.toList();
83+
}
84+
7385
/**
7486
* Returns whether there are violations available.
7587
*
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.acme.myproject;
17+
18+
import org.springframework.stereotype.Component;
19+
20+
import com.acme.myproject.moduleB.internal.InternalComponentB;
21+
22+
/**
23+
*
24+
* @author Oliver Drotbohm
25+
*/
26+
@Component
27+
class CentralConfiguration {
28+
private InternalComponentB referenceToModuleInternalComponent;
29+
}

spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import org.assertj.core.api.InstanceOfAssertFactories;
2021
import org.junit.jupiter.api.Test;
2122
import org.springframework.modulith.core.ApplicationModule;
2223
import org.springframework.modulith.core.ApplicationModuleDependencies;
@@ -56,7 +57,22 @@ void verifyModules() {
5657

5758
@Test
5859
void verifyModulesWithoutInvalid() {
59-
ApplicationModules.of(Application.class, DEFAULT_EXCLUSIONS.or(Filters.withoutModule("invalid"))).verify();
60+
61+
assertThatExceptionOfType(Violations.class).isThrownBy(() -> {
62+
63+
ApplicationModules
64+
.of(Application.class, DEFAULT_EXCLUSIONS.or(Filters.withoutModule("invalid")))
65+
.verify();
66+
67+
}).satisfies(it -> {
68+
69+
assertThat(it.getMessages())
70+
.hasSize(1)
71+
.element(0, as(InstanceOfAssertFactories.STRING))
72+
.contains("root:com.acme.myproject")
73+
.contains(InternalComponentB.class.getName());
74+
});
75+
6076
}
6177

6278
@Test

spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Arrays;
1919
import java.util.List;
2020
import java.util.Objects;
21+
import java.util.function.Predicate;
2122
import java.util.function.Supplier;
2223
import java.util.stream.Collectors;
2324
import java.util.stream.Stream;
@@ -191,14 +192,14 @@ public int hashCode() {
191192
* @since 1.1
192193
*/
193194
private static class ModuleTestExecutionBeanDefinitionSelector implements BeanDefinitionRegistryPostProcessor {
194-
195+
195196
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecutionBeanDefinitionSelector.class);
196197

197198
private final ModuleTestExecution execution;
198199

199200
/**
200201
* Creates a new {@link ModuleTestExecutionBeanDefinitionSelector} for the given {@link ModuleTestExecution}.
201-
*
202+
*
202203
* @param execution must not be {@literal null}.
203204
*/
204205
private ModuleTestExecutionBeanDefinitionSelector(ModuleTestExecution execution) {
@@ -208,7 +209,7 @@ private ModuleTestExecutionBeanDefinitionSelector(ModuleTestExecution execution)
208209
this.execution = execution;
209210
}
210211

211-
/*
212+
/*
212213
* (non-Javadoc)
213214
* @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(org.springframework.beans.factory.support.BeanDefinitionRegistry)
214215
*/
@@ -224,7 +225,8 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
224225
for (String name : registry.getBeanDefinitionNames()) {
225226

226227
var type = factory.getType(name, false);
227-
var module = modules.getModuleByType(type);
228+
var module = modules.getModuleByType(type)
229+
.filter(Predicate.not(ApplicationModule::isRootModule));
228230

229231
// Not a module type -> pass
230232
if (module.isEmpty()) {
@@ -239,15 +241,17 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
239241
.filter(packagesIncludedInTestRun::contains).isPresent()) {
240242
continue;
241243
}
242-
243-
LOGGER.trace("Dropping bean definition {} for type {} as it is not included in an application module to be bootstrapped!", name, type.getName());
244+
245+
LOGGER.trace(
246+
"Dropping bean definition {} for type {} as it is not included in an application module to be bootstrapped!",
247+
name, type.getName());
244248

245249
// Remove bean definition from bootstrap
246250
registry.removeBeanDefinition(name);
247251
}
248252
}
249253

250-
/*
254+
/*
251255
* (non-Javadoc)
252256
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
253257
*/

0 commit comments

Comments
 (0)