Skip to content

Commit e41d7ec

Browse files
committed
GH-520 - Early reject invalid ApplicationModules bootstraps.
We now immediately reject the ApplicationModules bootstrap in case the initial scanning of the root packages yield no classes at all.
1 parent c45a0fc commit e41d7ec

File tree

8 files changed

+159
-5
lines changed

8 files changed

+159
-5
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ protected ApplicationModules(ModulithMetadata metadata, Collection<String> packa
115115
.importPackages(packages) //
116116
.that(not(ignored.or(IS_AOT_TYPE).or(IS_SPRING_CGLIB_PROXY)));
117117

118+
Assert.notEmpty(allClasses, () -> "No classes found in packages %s!".formatted(packages));
119+
118120
Classes classes = Classes.of(allClasses);
119121

120122
this.modules = packages.stream() //
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2024 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 org.springframework.modulith.core;
17+
18+
/**
19+
* Factory interface to create {@link ApplicationModules} instances for application classes. The default version will
20+
* simply delegate to {@link ApplicationModules#of(Class)} which will only look at production classes. Our test support
21+
* provides an alternative implementation to bootstrap an {@link ApplicationModules} instance from test types as well,
22+
* primarily for our very own integration test purposes.
23+
*
24+
* @author Oliver Drotbohm
25+
* @since 1.2
26+
*/
27+
public interface ApplicationModulesFactory {
28+
29+
/**
30+
* Returns the {@link ApplicationModules} instance for the given application class.
31+
*
32+
* @param applicationClass must not be {@literal null}.
33+
* @return will never be {@literal null}.
34+
*/
35+
ApplicationModules of(Class<?> applicationClass);
36+
37+
/**
38+
* Creates the default {@link ApplicationModulesFactory} delegating to {@link ApplicationModules#of(Class)}
39+
*
40+
* @return will never be {@literal null}.
41+
*/
42+
public static ApplicationModulesFactory defaultFactory() {
43+
return ApplicationModules::of;
44+
}
45+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2024 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 example.empty;
17+
18+
import org.springframework.modulith.Modulithic;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@Modulithic
24+
public class EmptyApplication {
25+
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2024 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 example.empty;
17+
18+
import org.springframework.modulith.Modulithic;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@Modulithic
24+
public class EmptyApplication {
25+
26+
}

spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import example.declared.fourth.Fourth;
2222
import example.declared.second.Second;
2323
import example.declared.third.Third;
24+
import example.empty.EmptyApplication;
2425

2526
import java.util.ArrayList;
2627
import java.util.List;
@@ -234,6 +235,13 @@ void detectsOpenModule() {
234235
.noneMatch(it -> it.contains("Cycle detected: Slice open"));
235236
}
236237

238+
@Test // GH-520
239+
void bootstrapsOnEmptyProject() {
240+
241+
assertThatNoException().isThrownBy(() -> ApplicationModules.of(EmptyApplication.class).verify());
242+
assertThatIllegalArgumentException().isThrownBy(() -> ApplicationModules.of("non.existant"));
243+
}
244+
237245
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class<?>... types) {
238246

239247
Stream.of(types).forEach(type -> {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.modulith.core.ApplicationModulesFactory=org.springframework.modulith.test.TestApplicationModules.Factory

spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
import org.springframework.context.ApplicationListener;
3030
import org.springframework.context.annotation.Bean;
3131
import org.springframework.context.annotation.Role;
32+
import org.springframework.core.io.support.SpringFactoriesLoader;
3233
import org.springframework.core.task.AsyncTaskExecutor;
3334
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3435
import org.springframework.modulith.ApplicationModuleInitializer;
3536
import org.springframework.modulith.core.ApplicationModule;
3637
import org.springframework.modulith.core.ApplicationModules;
38+
import org.springframework.modulith.core.ApplicationModulesFactory;
3739
import org.springframework.modulith.core.FormatableType;
3840
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
3941
import org.springframework.modulith.runtime.ApplicationRuntime;
@@ -138,12 +140,21 @@ public void initialize() {
138140
private static class ApplicationModulesBootstrap {
139141

140142
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesBootstrap.class);
143+
private static final ApplicationModulesFactory BOOTSTRAP;
144+
145+
static {
146+
147+
var factories = SpringFactoriesLoader.loadFactories(ApplicationModulesFactory.class,
148+
ApplicationModulesBootstrap.class.getClassLoader());
149+
150+
BOOTSTRAP = !factories.isEmpty() ? factories.get(0) : ApplicationModulesFactory.defaultFactory();
151+
}
141152

142153
static ApplicationModules initializeApplicationModules(Class<?> applicationMainClass) {
143154

144155
LOGGER.debug("Obtaining Spring Modulith application modules…");
145156

146-
var result = ApplicationModules.of(applicationMainClass);
157+
var result = BOOTSTRAP.of(applicationMainClass);
147158
var numberOfModules = result.stream().count();
148159

149160
if (numberOfModules == 0) {
@@ -153,7 +164,7 @@ static ApplicationModules initializeApplicationModules(Class<?> applicationMainC
153164
} else {
154165

155166
LOGGER.debug("Detected {} application modules: {}", //
156-
result.stream().count(), //
167+
numberOfModules, //
157168
result.stream().map(ApplicationModule::getName).toList());
158169
}
159170

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919

2020
import org.springframework.modulith.core.ApplicationModules;
21+
import org.springframework.modulith.core.ApplicationModulesFactory;
2122
import org.springframework.modulith.core.ModulithMetadata;
2223

2324
import com.tngtech.archunit.base.DescribedPredicate;
@@ -34,10 +35,44 @@ public class TestApplicationModules {
3435
* Creates an {@link ApplicationModules} instance from the given package but only inspecting the test code.
3536
*
3637
* @param basePackage must not be {@literal null} or empty.
37-
* @return
38+
* @return will never be {@literal null}.
3839
*/
3940
public static ApplicationModules of(String basePackage) {
40-
return new ApplicationModules(ModulithMetadata.of(basePackage), List.of(basePackage),
41-
DescribedPredicate.alwaysFalse(), false, new ImportOption.OnlyIncludeTests()) {};
41+
return of(ModulithMetadata.of(basePackage), basePackage);
42+
}
43+
44+
/**
45+
* Creates an {@link ApplicationModules} instance from the given application class but only inspecting the test code.
46+
*
47+
* @param applicationClass must not be {@literal null} or empty.
48+
* @return will never be {@literal null}.
49+
* @since 1.2
50+
*/
51+
public static ApplicationModules of(Class<?> applicationClass) {
52+
return of(ModulithMetadata.of(applicationClass), applicationClass.getPackageName());
53+
}
54+
55+
private static ApplicationModules of(ModulithMetadata metadata, String basePackage) {
56+
return new ApplicationModules(metadata, List.of(basePackage), DescribedPredicate.alwaysFalse(), false,
57+
new ImportOption.OnlyIncludeTests()) {};
58+
}
59+
60+
/**
61+
* Custom {@link ApplicationModulesFactory} to bootstrap an {@link ApplicationModules} instance only considering test
62+
* code.
63+
*
64+
* @author Oliver Drotbohm
65+
* @since 1.2
66+
*/
67+
static class Factory implements ApplicationModulesFactory {
68+
69+
/*
70+
* (non-Javadoc)
71+
* @see org.springframework.modulith.core.util.ApplicationModulesFactory#of(java.lang.Class)
72+
*/
73+
@Override
74+
public ApplicationModules of(Class<?> applicationClass) {
75+
return TestApplicationModules.of(applicationClass);
76+
}
4277
}
4378
}

0 commit comments

Comments
 (0)