diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java index 4b4d626c4..037b6c05a 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java @@ -143,18 +143,44 @@ private ApplicationModules(ModulithMetadata metadata, Collection package .distinct() .collect(Collectors.toUnmodifiableSet()); - this.modules = sources.stream() // - .map(it -> { + var exclusions = sources.stream() + .map(ApplicationModuleSource::getModuleBasePackage) + .toList(); + + var modules = sources.stream() // + .map(it -> new ApplicationModule(it, new JavaPackages(exclusions))) + .toList(); + + var duplicates = modules.stream() + .collect(groupingBy(ApplicationModule::getIdentifier)) + .entrySet().stream() + .filter(it -> it.getValue().size() > 1) + .toList(); + + if (!duplicates.isEmpty()) { - var exclusions = sources.stream() - .map(ApplicationModuleSource::getModuleBasePackage) - .toList(); + var message = duplicates.stream() + .map(it -> { - return new ApplicationModule(it, new JavaPackages(exclusions)); + var basePackages = it.getValue().stream() + .map(module -> "'" + module.getBasePackage().getName() + "'") + .sorted() + .collect(joining(" and ")); - }) + return ("Application module identifier '%s' is not unique. The modules with the base packages %s use it." + + " Declare an explicit identifier with @ApplicationModule(id = …), or set @Modulithic(useFullyQualifiedModuleNames = true).") + .formatted(it.getKey(), basePackages); + + }) + .collect(joining("\n")); + + throw new IllegalStateException(message); + } + + this.modules = modules.stream() .collect(toMap(ApplicationModule::getIdentifier, Function.identity())); + this.rootPackages = packagesToImport.stream() // .distinct() .map(it -> JavaPackage.of(classes, it).toSingle()) // diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java index 85f4c4ac9..bc6f05e41 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java @@ -89,4 +89,12 @@ void findsTypeBySimpleName() { assertThat(it.contains("RootType")).isTrue(); }); } + + @Test // GH-1964 + void reportsDuplicateModuleIdentifiersWithDescriptiveMessage() { + + assertThatIllegalStateException() + .isThrownBy(() -> TestUtils.of(reproducers.gh1964.app.Application.class)) + .withMessage("Application module identifier 'billing' is not unique. The modules with the base packages 'reproducers.gh1964.first.billing' and 'reproducers.gh1964.second.billing' use it. Declare an explicit identifier with @ApplicationModule(id = …), or set @Modulithic(useFullyQualifiedModuleNames = true)."); + } } diff --git a/spring-modulith-core/src/test/java/reproducers/gh1964/Application.java b/spring-modulith-core/src/test/java/reproducers/gh1964/Application.java new file mode 100644 index 000000000..0f8a0d55e --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1964/Application.java @@ -0,0 +1,21 @@ +/* + * Copyright 2026 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 reproducers.gh1964; + +import org.springframework.modulith.Modulithic; + +@Modulithic(additionalPackages = { "reproducers.gh1964.first", "reproducers.gh1964.second" }) +public class Application {} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1964/app/Application.java b/spring-modulith-core/src/test/java/reproducers/gh1964/app/Application.java new file mode 100644 index 000000000..4ebf6e115 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1964/app/Application.java @@ -0,0 +1,21 @@ +/* + * Copyright 2026 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 reproducers.gh1964.app; + +import org.springframework.modulith.Modulithic; + +@Modulithic(additionalPackages = { "reproducers.gh1964.first", "reproducers.gh1964.second" }) +public class Application {} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1964/first/billing/FirstBilling.java b/spring-modulith-core/src/test/java/reproducers/gh1964/first/billing/FirstBilling.java new file mode 100644 index 000000000..6300b212a --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1964/first/billing/FirstBilling.java @@ -0,0 +1,18 @@ +/* + * Copyright 2026 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 reproducers.gh1964.first.billing; + +public class FirstBilling {} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1964/second/billing/SecondBilling.java b/spring-modulith-core/src/test/java/reproducers/gh1964/second/billing/SecondBilling.java new file mode 100644 index 000000000..41a8199c8 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1964/second/billing/SecondBilling.java @@ -0,0 +1,18 @@ +/* + * Copyright 2026 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 reproducers.gh1964.second.billing; + +public class SecondBilling {}