GH-578 - Support for nested application modules.

The ApplicationModules bootstrap now triggers the module base package detection, followed by a new, additional pass of detecting nested application module packages. Those packages are now added to the ones we create ApplicationModule instances for and also handed into the module instance creation step as exclusions to make sure that parent modules do not include code residing in sub-modules.

The bootstrap of ApplicationModules now uses a dedicated ApplicationModuleSource to allow calculating a default module name relative to the application base package.

Each module now operates on the Classes instance obtained from the JavaPackage instance that constitutes the module's base package but filtered by the given exclusions.
This commit is contained in:
Oliver Drotbohm
2023-11-06 18:26:07 +01:00
committed by Oliver Drotbohm
parent b927bf1211
commit 7838204c25
31 changed files with 1315 additions and 133 deletions

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.empty;
package empty;
import org.springframework.modulith.Modulithic;
@@ -21,6 +21,4 @@ import org.springframework.modulith.Modulithic;
* @author Oliver Drotbohm
*/
@Modulithic
public class EmptyApplication {
}
public class EmptyApplication {}

View File

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

View File

@@ -1,2 +1,2 @@
@org.jmolecules.ddd.annotation.Module
package jmolecules;
package example.jmolecules;

View File

@@ -15,7 +15,17 @@
*/
package example.ni;
import example.ni.nested.b.first.InNestedBFirst;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author Oliver Drotbohm
*/
public interface RootType {}
@Component
@RequiredArgsConstructor
public class RootType {
final InNestedBFirst inNestedBFirst;
}

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.ni.nested.b;
import example.ni.RootType;
import example.ni.nested.b.first.InNestedBFirst;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author Oliver Drotbohm
*/
@Component
@RequiredArgsConstructor
public class InNestedB {
final InNestedBFirst downward;
final RootType upward;
}

View File

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

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule
package example.ni.nested.b.first;

View File

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

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule
package example.ni.nested.b.second;

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule
package example.ni.nested;

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link ApplicationModules}.
*
* @author Oliver Drotbohm
*/
class ApplicationModulesUnitTests {
ApplicationModules modules = TestUtils.of("example", "example.ninvalid");
@Test // GH 578
void discoversComplexModuleArrangement() {
assertThat(modules)
.extracting(ApplicationModule::getName)
.containsExactlyInAnyOrder(
"invalid",
"jmolecules",
"ni",
"ni.nested",
"ni.nested.b.first",
"ni.nested.b.second",
"springbean");
}
@Test // GH 578
void detectsModuleNesting() {
var ni = modules.getModuleByName("ni").orElseThrow();
var nested = modules.getModuleByName("ni.nested").orElseThrow();
var inner = modules.getModuleByName("ni.nested.b.first").orElseThrow();
assertThat(inner.getParentModule(modules)).hasValue(nested);
assertThat(nested.getParentModule(modules)).hasValue(ni);
assertThat(ni.getParentModule(modules)).isEmpty();
assertThat(ni.getDirectlyNestedModules(modules))
.extracting(ApplicationModule::getName)
.containsExactlyInAnyOrder("ni.nested");
assertThat(ni.getNestedModules(modules))
.extracting(ApplicationModule::getName)
.containsExactlyInAnyOrder("ni.nested", "ni.nested.b.first", "ni.nested.b.second");
}
@Test // GH 578
void detectsInvalidReferenceToNestedModule() {
var violations = modules.detectViolations();
var messages = violations.getMessages();
assertThat(messages)
.hasSize(3)
.satisfiesExactlyInAnyOrder(
it -> assertThat(it).contains("Invalid", "'invalid'", "'ni.nested'"),
it -> assertThat(it).contains("Invalid", "'invalid'", "'ni.nested.b.first'"),
it -> assertThat(it).contains("Invalid", "'ni'", "'ni.nested.b.first'"));
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import example.ni.nested.a.InNestedA;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
/**
* Unit tests for {@link JavaPackage}.
*
* @author Oliver Drotbohm
*/
class JavaPackageUnitTests {
static final JavaClasses ALL_CLASSES = new ClassFileImporter() //
.withImportOption(ImportOption.Predefined.ONLY_INCLUDE_TESTS)
.importPackages("example");
Classes classes = Classes.of(ALL_CLASSES);
JavaPackage pkg = JavaPackage.of(classes, "example.ni");
@Test // GH-578
void detectsDirectSubPackages() throws Exception {
assertThat(pkg.getLocalName()).isEqualTo("ni");
assertThat(pkg.getDirectSubPackages()) //
.extracting(JavaPackage::getLocalName) //
.containsExactlyInAnyOrder("api", "internal", "nested", "ontype", "spi");
}
@Test // GH-578
void detectsAllSubPackages() {
var subPackages = pkg.getSubPackages();
assertThat(subPackages.stream()
.map(JavaPackage::getPackageName)
.map(it -> it.getLocalName("example.ni")))
.containsExactly(
"api",
"internal",
"nested",
"nested.a",
"nested.b",
"nested.b.first",
"nested.b.second",
"ontype",
"spi");
}
@Test // GH-578
void detectsFlattenedSubPackages() {
var subPackages = pkg.getSubPackages();
assertThat(subPackages.flatten().stream()
.map(JavaPackage::getPackageName)
.map(it -> it.getLocalName("example.ni")))
.containsExactlyInAnyOrder("api", "internal", "nested", "ontype", "spi");
}
@Test // GH-578
void considersExclusionsForClassesLookup() {
assertThat(pkg.contains(InNestedA.class.getName()));
var nestedA = JavaPackage.of(classes, "example.ni.nested.a");
assertThat(nestedA.contains(InNestedA.class.getName()));
assertThat(pkg.getClasses(List.of(nestedA)).stream().map(JavaClass::getName))
.doesNotContain(InNestedA.class.getName());
}
@Test // GH-578
void detectsSubPackage() {
var classes = Classes.of(ALL_CLASSES);
var root = JavaPackage.of(classes, "example.ni");
var direct = JavaPackage.of(classes, "example.ni.internal");
var nested = JavaPackage.of(classes, "example.ni.internal.nested");
assertThat(direct.isSubPackageOf(root)).isTrue();
assertThat(nested.isSubPackageOf(root)).isTrue();
assertThat(nested.isSubPackageOf(direct)).isTrue();
}
@Test // GH-578
void samePackagesConsideredEqual() {
var first = JavaPackage.of(classes, "example.ni");
var second = JavaPackage.of(classes, "example.ni");
assertThat(first.equals(second)).isTrue();
assertThat(second.equals(first)).isTrue();
}
}

View File

@@ -34,11 +34,12 @@ class ModuleDetectionStrategyUnitTest {
var classes = new ClassFileImporter() //
.withImportOption(new ImportOption.OnlyIncludeTests()) //
.importPackages("jmolecules");
.importPackages("example.jmolecules");
var javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
var javaPackage = JavaPackage.of(Classes.of(classes), "example");
var expected = javaPackage.getSubPackage("jmolecules").orElseThrow();
assertThat(ApplicationModuleDetectionStrategy.explicitlyAnnotated().getModuleBasePackages(javaPackage))
.containsExactly(javaPackage);
.containsExactly(expected);
}
}

View File

@@ -33,8 +33,6 @@ import com.acme.withatbean.SampleAggregate;
import com.acme.withatbean.TestEvents.JMoleculesAnnotated;
import com.acme.withatbean.TestEvents.JMoleculesImplementing;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
/**
* Unit tests for {@link ApplicationModule}.
@@ -45,10 +43,7 @@ import com.tngtech.archunit.core.importer.ClassFileImporter;
class ModuleUnitTest {
String packageName = "com.acme.withatbean";
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), packageName);
ApplicationModule module = new ApplicationModule(javaPackage, false);
ApplicationModule module = TestUtils.getApplicationModule(packageName);
@Test
public void considersExternalSpringBeans() {
@@ -61,8 +56,10 @@ class ModuleUnitTest {
@Test
void discoversPublishedEvents() {
JavaClass jMoleculesAnnotated = classes.get(JMoleculesAnnotated.class);
JavaClass jMoleculesImplementing = classes.get(JMoleculesImplementing.class);
var classes = module.getClasses();
JavaClass jMoleculesAnnotated = classes.getRequiredClass(JMoleculesAnnotated.class);
JavaClass jMoleculesImplementing = classes.getRequiredClass(JMoleculesImplementing.class);
List<EventType> events = module.getPublishedEvents();

View File

@@ -23,6 +23,11 @@ import example.ni.api.ApiType;
import example.ni.internal.AdditionalSpiType;
import example.ni.internal.DefaultedNamedInterfaceType;
import example.ni.internal.Internal;
import example.ni.nested.InNested;
import example.ni.nested.a.InNestedA;
import example.ni.nested.b.InNestedB;
import example.ni.nested.b.first.InNestedBFirst;
import example.ni.nested.b.second.InNestedBSecond;
import example.ni.ontype.Exposed;
import example.ni.spi.SpiType;
import example.ninvalid.InvalidDefaultNamedInterface;
@@ -62,7 +67,8 @@ class NamedInterfacesUnitTests {
var javaPackage = TestUtils.getPackage(InvalidDefaultNamedInterface.class);
assertThatIllegalStateException().isThrownBy(() -> NamedInterfaces.discoverNamedInterfaces(javaPackage))
assertThatIllegalStateException()
.isThrownBy(() -> NamedInterfaces.discoverNamedInterfaces(javaPackage))
.withMessageContaining("named interface defaulting")
.withMessageContaining(InvalidDefaultNamedInterface.class.getSimpleName());
}
@@ -76,7 +82,9 @@ class NamedInterfacesUnitTests {
assertThat(interfaces).map(NamedInterface::getName)
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype");
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class);
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME,
RootType.class, Internal.class, InNested.class, InNestedA.class, InNestedB.class, InNestedBFirst.class,
InNestedBSecond.class);
}
@Test // GH-595

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link PackageName}.
*
* @author Oliver Drotbohm
*/
class PackageNameUnitTests {
@Test // GH-578
void sortsPackagesByNameAndDepth() {
var comAcme = new PackageName("com.acme");
var comAcmeA = new PackageName("com.acme.a");
var comAcmeAFirst = new PackageName("com.acme.a.first");
var comAcmeAFirstOne = new PackageName("com.acme.a.first.one");
var comAcmeASecond = new PackageName("com.acme.a.second");
var comAcmeB = new PackageName("com.acme.b");
assertThat(List.of(comAcmeAFirstOne, comAcmeB, comAcmeASecond, comAcmeAFirst, comAcme, comAcmeA)
.stream()
.sorted((l, r) -> -l.compareTo(r))
.map(it -> it.getLocalName("com")))
.containsExactly("acme.b", "acme.a.second", "acme.a.first.one", "acme.a.first", "acme.a", "acme");
}
}

View File

@@ -46,6 +46,17 @@ public class TestUtils {
private static Supplier<Classes> classes = SingletonSupplier
.of(() -> Classes.of(imported.get()).that(IS_MODULE_TYPE));
/**
* Creates an {@link ApplicationModules} instance from the given package but only inspecting the test code.
*
* @param basePackage must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.3
*/
public static ApplicationModules of(String basePackage, String... ignoredPackages) {
return of(ModulithMetadata.of(basePackage), JavaClass.Predicates.resideInAnyPackage(ignoredPackages));
}
/**
* Returns all {@link Classes} of this module.
*
@@ -78,12 +89,12 @@ public class TestUtils {
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
}
public static ApplicationModules of(String basePackage, String... ignoredPackages) {
return of(ModulithMetadata.of(basePackage), JavaClass.Predicates.resideInAnyPackage(ignoredPackages));
}
public static ApplicationModule getApplicationModule(String packageName) {
return new ApplicationModule(getPackage(packageName), false);
var pkg = getPackage(packageName);
var source = ApplicationModuleSource.from(pkg, pkg.getLocalName());
return new ApplicationModule(source);
}
private static JavaPackage getPackage(String name) {