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 b36cca73a..a9b436d57 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 @@ -115,6 +115,8 @@ protected ApplicationModules(ModulithMetadata metadata, Collection packa .importPackages(packages) // .that(not(ignored.or(IS_AOT_TYPE).or(IS_SPRING_CGLIB_PROXY))); + Assert.notEmpty(allClasses, () -> "No classes found in packages %s!".formatted(packages)); + Classes classes = Classes.of(allClasses); this.modules = packages.stream() // diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModulesFactory.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModulesFactory.java new file mode 100644 index 000000000..5f993efbe --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModulesFactory.java @@ -0,0 +1,45 @@ +/* + * 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; + +/** + * Factory interface to create {@link ApplicationModules} instances for application classes. The default version will + * simply delegate to {@link ApplicationModules#of(Class)} which will only look at production classes. Our test support + * provides an alternative implementation to bootstrap an {@link ApplicationModules} instance from test types as well, + * primarily for our very own integration test purposes. + * + * @author Oliver Drotbohm + * @since 1.2 + */ +public interface ApplicationModulesFactory { + + /** + * Returns the {@link ApplicationModules} instance for the given application class. + * + * @param applicationClass must not be {@literal null}. + * @return will never be {@literal null}. + */ + ApplicationModules of(Class applicationClass); + + /** + * Creates the default {@link ApplicationModulesFactory} delegating to {@link ApplicationModules#of(Class)} + * + * @return will never be {@literal null}. + */ + public static ApplicationModulesFactory defaultFactory() { + return ApplicationModules::of; + } +} diff --git a/spring-modulith-core/src/test/java/example/empty/EmptyApplication.java b/spring-modulith-core/src/test/java/example/empty/EmptyApplication.java new file mode 100644 index 000000000..ed855a137 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/empty/EmptyApplication.java @@ -0,0 +1,26 @@ +/* + * 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 example.empty; + +import org.springframework.modulith.Modulithic; + +/** + * @author Oliver Drotbohm + */ +@Modulithic +public class EmptyApplication { + +} diff --git a/spring-modulith-integration-test/src/main/java/example/empty/EmptyApplication.java b/spring-modulith-integration-test/src/main/java/example/empty/EmptyApplication.java new file mode 100644 index 000000000..ed855a137 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/example/empty/EmptyApplication.java @@ -0,0 +1,26 @@ +/* + * 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 example.empty; + +import org.springframework.modulith.Modulithic; + +/** + * @author Oliver Drotbohm + */ +@Modulithic +public class EmptyApplication { + +} diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java index 4cde963e6..735749b2b 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java @@ -21,6 +21,7 @@ import example.declared.fourth.Fourth; import example.declared.second.Second; import example.declared.third.Third; +import example.empty.EmptyApplication; import java.util.ArrayList; import java.util.List; @@ -234,6 +235,13 @@ void detectsOpenModule() { .noneMatch(it -> it.contains("Cycle detected: Slice open")); } + @Test // GH-520 + void bootstrapsOnEmptyProject() { + + assertThatNoException().isThrownBy(() -> ApplicationModules.of(EmptyApplication.class).verify()); + assertThatIllegalArgumentException().isThrownBy(() -> ApplicationModules.of("non.existant")); + } + private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class... types) { Stream.of(types).forEach(type -> { diff --git a/spring-modulith-observability/src/main/resources/META-INF/spring.factories b/spring-modulith-observability/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..adf613bf3 --- /dev/null +++ b/spring-modulith-observability/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.modulith.core.ApplicationModulesFactory=org.springframework.modulith.test.TestApplicationModules.Factory diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java index f895e0be3..1bf2eccab 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java @@ -29,11 +29,13 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Role; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.modulith.ApplicationModuleInitializer; import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.ApplicationModulesFactory; import org.springframework.modulith.core.FormatableType; import org.springframework.modulith.runtime.ApplicationModulesRuntime; import org.springframework.modulith.runtime.ApplicationRuntime; @@ -138,12 +140,21 @@ public void initialize() { private static class ApplicationModulesBootstrap { private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesBootstrap.class); + private static final ApplicationModulesFactory BOOTSTRAP; + + static { + + var factories = SpringFactoriesLoader.loadFactories(ApplicationModulesFactory.class, + ApplicationModulesBootstrap.class.getClassLoader()); + + BOOTSTRAP = !factories.isEmpty() ? factories.get(0) : ApplicationModulesFactory.defaultFactory(); + } static ApplicationModules initializeApplicationModules(Class applicationMainClass) { LOGGER.debug("Obtaining Spring Modulith application modules…"); - var result = ApplicationModules.of(applicationMainClass); + var result = BOOTSTRAP.of(applicationMainClass); var numberOfModules = result.stream().count(); if (numberOfModules == 0) { @@ -153,7 +164,7 @@ static ApplicationModules initializeApplicationModules(Class applicationMainC } else { LOGGER.debug("Detected {} application modules: {}", // - result.stream().count(), // + numberOfModules, // result.stream().map(ApplicationModule::getName).toList()); } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/TestApplicationModules.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/TestApplicationModules.java index 99ad047dd..8b4566ac5 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/TestApplicationModules.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/TestApplicationModules.java @@ -18,6 +18,7 @@ import java.util.List; import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.ApplicationModulesFactory; import org.springframework.modulith.core.ModulithMetadata; import com.tngtech.archunit.base.DescribedPredicate; @@ -34,10 +35,44 @@ public class TestApplicationModules { * 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 + * @return will never be {@literal null}. */ public static ApplicationModules of(String basePackage) { - return new ApplicationModules(ModulithMetadata.of(basePackage), List.of(basePackage), - DescribedPredicate.alwaysFalse(), false, new ImportOption.OnlyIncludeTests()) {}; + return of(ModulithMetadata.of(basePackage), basePackage); + } + + /** + * Creates an {@link ApplicationModules} instance from the given application class but only inspecting the test code. + * + * @param applicationClass must not be {@literal null} or empty. + * @return will never be {@literal null}. + * @since 1.2 + */ + public static ApplicationModules of(Class applicationClass) { + return of(ModulithMetadata.of(applicationClass), applicationClass.getPackageName()); + } + + private static ApplicationModules of(ModulithMetadata metadata, String basePackage) { + return new ApplicationModules(metadata, List.of(basePackage), DescribedPredicate.alwaysFalse(), false, + new ImportOption.OnlyIncludeTests()) {}; + } + + /** + * Custom {@link ApplicationModulesFactory} to bootstrap an {@link ApplicationModules} instance only considering test + * code. + * + * @author Oliver Drotbohm + * @since 1.2 + */ + static class Factory implements ApplicationModulesFactory { + + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.util.ApplicationModulesFactory#of(java.lang.Class) + */ + @Override + public ApplicationModules of(Class applicationClass) { + return TestApplicationModules.of(applicationClass); + } } }