diff --git a/pom.xml b/pom.xml
index c0776f2a..508a1df3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,18 +18,20 @@
https://spring.io/projects/spring-modulith
+ spring-modulith-actuatorspring-modulith-apispring-modulith-bomspring-modulith-core
- spring-modulith-events
- spring-modulith-testspring-modulith-docs
- spring-modulith-observability
+ spring-modulith-eventsspring-modulith-moments
+ spring-modulith-observability
+ spring-modulith-runtimespring-modulith-starter-jdbcspring-modulith-starter-jpaspring-modulith-starter-mongodbspring-modulith-starter-test
+ spring-modulith-test
@@ -449,6 +451,7 @@ limitations under the License.
1717
+ true
diff --git a/spring-modulith-actuator/pom.xml b/spring-modulith-actuator/pom.xml
new file mode 100644
index 00000000..5ddd0807
--- /dev/null
+++ b/spring-modulith-actuator/pom.xml
@@ -0,0 +1,56 @@
+
+ 4.0.0
+
+
+ org.springframework.experimental
+ spring-modulith
+ 0.2.0-SNAPSHOT
+
+
+ Spring Modulith - Actuator
+ spring-modulith-actuator
+
+
+ org.springframework.modulith.actuator
+
+
+
+
+
+ org.springframework.experimental
+ spring-modulith-runtime
+ ${project.version}
+
+
+
+ org.springframework.boot
+ spring-boot-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+ org.springframework.experimental
+ spring-modulith-test
+ ${project.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ test
+
+
+
+
+
diff --git a/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java
new file mode 100644
index 00000000..1e61e5d4
--- /dev/null
+++ b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2022 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.actuator;
+
+import static java.util.stream.Collectors.*;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
+import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
+import org.springframework.modulith.model.ApplicationModule;
+import org.springframework.modulith.model.ApplicationModuleDependency;
+import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.model.DependencyType;
+import org.springframework.util.Assert;
+
+/**
+ * A Spring Boot actuator endpoint to expose the application module structure of a Spring Modulith based application.
+ *
+ * @author Oliver Drotbohm
+ */
+@Slf4j
+@Endpoint(id = "applicationmodules")
+public class ApplicationModulesEndpoint {
+
+ private static final Function, Set> REMOVE_DEFAULT_DEPENDENCY_TYPE_IF_OTHERS_PRESENT = it -> {
+
+ if (it.stream().anyMatch(type -> type != DependencyType.DEFAULT)) {
+ it.remove(DependencyType.DEFAULT);
+ }
+
+ return it;
+ };
+
+ private static final Collector> MAPPER = mapping(
+ ApplicationModuleDependency::getDependencyType,
+ collectingAndThen(toSet(), REMOVE_DEFAULT_DEPENDENCY_TYPE_IF_OTHERS_PRESENT));
+
+ private final Supplier runtime;
+
+ /**
+ * Creates a new {@link ApplicationModulesEndpoint} for the given {@link ModulesRuntime}.
+ *
+ * @param runtime must not be {@literal null}.
+ */
+ public ApplicationModulesEndpoint(Supplier runtime) {
+
+ Assert.notNull(runtime, "ModulesRuntime must not be null!");
+
+ LOG.debug("Activating Spring Modulith actuator.");
+
+ this.runtime = runtime;
+ }
+
+ /**
+ * Returns the {@link ApplicationModules} metadata as {@link Map} (to be rendered as JSON).
+ *
+ * @return will never be {@literal null}.
+ */
+ @ReadOperation
+ Map getApplicationModules() {
+
+ var modules = runtime.get();
+
+ return modules.stream()
+ .collect(Collectors.toMap(ApplicationModule::getName, it -> toInfo(it, modules)));
+ }
+
+ private static Map toInfo(ApplicationModule module, ApplicationModules modules) {
+
+ return Map.of( //
+ "displayName", module.getDisplayName(), //
+ "basePackage", module.getBasePackage().getName(), //
+ "dependencies", module.getDependencies(modules).stream() //
+ .collect(Collectors.groupingBy(ApplicationModuleDependency::getTargetModule, MAPPER))
+ .entrySet() //
+ .stream() //
+ .map(ApplicationModulesEndpoint::toInfo) //
+ .toList() //
+ );
+ }
+
+ private static Map toInfo(Entry> types) {
+
+ return Map.of( //
+ "target", types.getKey().getName(), //
+ "types", types.getValue() //
+ );
+ }
+}
diff --git a/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfiguration.java b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfiguration.java
new file mode 100644
index 00000000..eebb7e3f
--- /dev/null
+++ b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfiguration.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 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.actuator.autoconfigure;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
+
+/**
+ * Auto-configuration for the {@link ApplicationModulesEndpoint}.
+ *
+ * @author Oliver Drotbohm
+ */
+@AutoConfiguration
+class ApplicationModulesEndpointConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ ApplicationModulesEndpoint applicationModulesEndpoint(ApplicationModulesRuntime runtime) {
+ return new ApplicationModulesEndpoint(runtime);
+ }
+}
diff --git a/spring-modulith-actuator/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-modulith-actuator/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..df45bdda
--- /dev/null
+++ b/spring-modulith-actuator/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.modulith.actuator.autoconfigure.ApplicationModulesEndpointConfiguration
diff --git a/spring-modulith-actuator/src/test/java/example/App.java b/spring-modulith-actuator/src/test/java/example/App.java
new file mode 100644
index 00000000..b379c6f4
--- /dev/null
+++ b/spring-modulith-actuator/src/test/java/example/App.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 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;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@SpringBootApplication
+class App {}
diff --git a/spring-modulith-actuator/src/test/java/example/a/ComponentA.java b/spring-modulith-actuator/src/test/java/example/a/ComponentA.java
new file mode 100644
index 00000000..97f2c95c
--- /dev/null
+++ b/spring-modulith-actuator/src/test/java/example/a/ComponentA.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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.a;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+public class ComponentA {
+
+}
diff --git a/spring-modulith-actuator/src/test/java/example/b/ComponentB.java b/spring-modulith-actuator/src/test/java/example/b/ComponentB.java
new file mode 100644
index 00000000..cfa3864b
--- /dev/null
+++ b/spring-modulith-actuator/src/test/java/example/b/ComponentB.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 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.b;
+
+import example.a.ComponentA;
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+@RequiredArgsConstructor
+class ComponentB {
+
+ final ComponentA dependency;
+
+ @EventListener
+ void on(ComponentA event) {}
+}
diff --git a/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/ApplicationModulesEndpointIntegrationTests.java b/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/ApplicationModulesEndpointIntegrationTests.java
new file mode 100644
index 00000000..d31636b1
--- /dev/null
+++ b/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/ApplicationModulesEndpointIntegrationTests.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 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.actuator;
+
+import static org.assertj.core.api.Assertions.*;
+
+import net.minidev.json.JSONArray;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.modulith.test.TestApplicationModules;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.JsonPath;
+
+/**
+ * Integration tests for {@link ApplicationModulesEndpoint}.
+ *
+ * @author Oliver Drotbohm
+ */
+class ApplicationModulesEndpointIntegrationTests {
+
+ @Test
+ void exposesApplicationModulesAsMap() throws Exception {
+
+ var modules = TestApplicationModules.of("example");
+ var endpoint = new ApplicationModulesEndpoint(() -> modules);
+ var result = endpoint.getApplicationModules();
+ var context = JsonPath.parse(new ObjectMapper().writeValueAsString(result));
+
+ assertThat(context. read("$.a.basePackage")).isEqualTo("example.a");
+ assertThat(context. read("$.a.dependencies")).isEmpty();
+
+ assertThat(context. read("$.b.basePackage")).isEqualTo("example.b");
+ assertThat(context. read("$.b.dependencies[0].target")).isEqualTo("a");
+ assertThat(context. read("$.b.dependencies[0].types"))
+ .containsExactlyInAnyOrder("EVENT_LISTENER", "USES_COMPONENT");
+ }
+}
diff --git a/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfigurationIntegrationTests.java b/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfigurationIntegrationTests.java
new file mode 100644
index 00000000..a002146a
--- /dev/null
+++ b/spring-modulith-actuator/src/test/java/org/springframework/modulith/actuator/autoconfigure/ApplicationModulesEndpointConfigurationIntegrationTests.java
@@ -0,0 +1,29 @@
+package org.springframework.modulith.actuator.autoconfigure;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
+
+/**
+ * Integration tests for {@link ApplicationModulesEndpointConfiguration}.
+ *
+ * @author Oliver Drotbohm
+ */
+@SpringBootTest
+class ApplicationModulesEndpointConfigurationIntegrationTests {
+
+ @SpringBootApplication
+ static class SampleApp {}
+
+ @Autowired ApplicationContext context;
+
+ @Test // GH-87
+ void bootstrapRegistersRuntimeInstances() {
+ assertThat(context.getBean(ApplicationModulesEndpoint.class)).isNotNull();
+ }
+}
diff --git a/spring-modulith-actuator/src/test/resources/logback.xml b/spring-modulith-actuator/src/test/resources/logback.xml
new file mode 100644
index 00000000..455fd805
--- /dev/null
+++ b/spring-modulith-actuator/src/test/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
diff --git a/spring-modulith-bom/pom.xml b/spring-modulith-bom/pom.xml
index 404131b7..ada154d8 100644
--- a/spring-modulith-bom/pom.xml
+++ b/spring-modulith-bom/pom.xml
@@ -19,6 +19,11 @@
+
+ org.springframework.experimental
+ spring-modulith-actuator
+ 0.2.0-SNAPSHOT
+ org.springframework.experimentalspring-modulith-api
@@ -69,6 +74,11 @@
spring-modulith-observability0.2.0-SNAPSHOT
+
+ org.springframework.experimental
+ spring-modulith-runtime
+ 0.2.0-SNAPSHOT
+ org.springframework.experimentalspring-modulith-starter-jdbc
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
index 5d4bd69a..2de83a46 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
@@ -119,9 +119,8 @@ public class ApplicationModule {
* @return will never be {@literal null} or empty.
*/
public String getDisplayName() {
-
return information.getDisplayName()
- .orElseGet(() -> getName());
+ .orElseGet(() -> StringUtils.capitalize(basePackage.getLocalName()));
}
/**
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
index 82d5a75b..25e013fd 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
@@ -54,8 +54,8 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
public class ApplicationModules implements Iterable {
private static final Map CACHE = new HashMap<>();
-
private static final ApplicationModuleDetectionStrategy DETECTION_STRATEGY;
+ private static final ImportOption IMPORT_OPTION = new ImportOption.DoNotIncludeTests();
static {
@@ -82,13 +82,12 @@ public class ApplicationModules implements Iterable {
private boolean verified;
- private ApplicationModules(ModulithMetadata metadata, Collection packages,
- DescribedPredicate ignored,
- boolean useFullyQualifiedModuleNames) {
+ protected ApplicationModules(ModulithMetadata metadata, Collection packages,
+ DescribedPredicate ignored, boolean useFullyQualifiedModuleNames, ImportOption option) {
this.metadata = metadata;
this.allClasses = new ClassFileImporter() //
- .withImportOption(new ImportOption.DoNotIncludeTests()) //
+ .withImportOption(option) //
.importPackages(packages) //
.that(not(ignored));
@@ -189,7 +188,7 @@ public class ApplicationModules implements Iterable {
basePackages.addAll(metadata.getAdditionalPackages());
ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
- metadata.useFullyQualifiedModuleNames());
+ metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
Set sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
@@ -281,7 +280,7 @@ public class ApplicationModules implements Iterable {
}
/**
- * Execute all verifications to be applied, unless the verifcation has been executed before.
+ * Execute all verifications to be applied, unless the verification has been executed before.
*
* @return will never be {@literal null}.
*/
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java
index 092cf6bb..29755d5a 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java
@@ -25,7 +25,7 @@ import org.springframework.modulith.Modulithic;
import org.springframework.modulith.model.Types.SpringTypes;
import org.springframework.util.Assert;
-interface ModulithMetadata {
+public interface ModulithMetadata {
static final String ANNOTATION_MISSING = "Modules can only be retrieved from a root type, but %s is not annotated with either @%s, @%s or @%s!";
diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleUnitTest.java
index d2110329..22d0096a 100644
--- a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleUnitTest.java
+++ b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleUnitTest.java
@@ -39,9 +39,9 @@ import com.tngtech.archunit.core.importer.ClassFileImporter;
@TestInstance(Lifecycle.PER_CLASS)
class ModuleUnitTest {
- ClassFileImporter importer = new ClassFileImporter();
- JavaClasses classes = importer.importPackages("com.acme.withatbean"); //
- JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "");
+ 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);
@@ -69,4 +69,9 @@ class ModuleUnitTest {
assertThat(it.getSources()).isNotEmpty();
});
}
+
+ @Test // GH-87
+ void usesCapitalizedNameAsDisplayNameByDefault() {
+ assertThat(module.getDisplayName()).isEqualTo("Withatbean");
+ }
}
diff --git a/spring-modulith-observability/pom.xml b/spring-modulith-observability/pom.xml
index 4574603c..d00ed87c 100644
--- a/spring-modulith-observability/pom.xml
+++ b/spring-modulith-observability/pom.xml
@@ -24,6 +24,12 @@
${project.version}
+
+ org.springframework.experimental
+ spring-modulith-runtime
+ ${project.version}
+
+
org.springframework.bootspring-boot-autoconfigure
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java
index 59a385d5..1093b461 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java
@@ -25,6 +25,7 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.modulith.model.ApplicationModule;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* @author Oliver Drotbohm
@@ -32,7 +33,7 @@ import org.springframework.modulith.model.ApplicationModule;
@RequiredArgsConstructor
public class ModuleEventListener implements ApplicationListener {
- private final ModulesRuntime modules;
+ private final ApplicationModulesRuntime modules;
private final Supplier tracer;
/*
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingBeanPostProcessor.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingBeanPostProcessor.java
index 1d085c9c..fb9af23d 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingBeanPostProcessor.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingBeanPostProcessor.java
@@ -32,6 +32,7 @@ import org.springframework.aop.support.StaticMethodMatcher;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* @author Oliver Drotbohm
@@ -40,11 +41,11 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
public static final String MODULE_BAGGAGE_KEY = "org.springframework.modulith.module";
- private final ApplicationRuntime runtime;
+ private final ApplicationModulesRuntime runtime;
private final Tracer tracer;
private final Map advisors = new HashMap<>();
- public ModuleTracingBeanPostProcessor(ApplicationRuntime runtime, Tracer tracer) {
+ public ModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Tracer tracer) {
super(runtime);
@@ -65,7 +66,7 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
return bean;
}
- ApplicationModules modules = getModules();
+ ApplicationModules modules = runtime.get();
return modules.getModuleByType(type.getName())
.map(DefaultObservedModule::new)
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingSupport.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingSupport.java
index 9b15de28..59061160 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingSupport.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleTracingSupport.java
@@ -16,13 +16,12 @@
package org.springframework.modulith.observability;
import java.util.function.Consumer;
-import java.util.function.Supplier;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
-import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
/**
@@ -30,16 +29,14 @@ import org.springframework.util.Assert;
*/
class ModuleTracingSupport implements BeanClassLoaderAware {
- private final Supplier modules;
- private final ApplicationRuntime context;
+ private final ApplicationModulesRuntime runtime;
private ClassLoader classLoader;
- protected ModuleTracingSupport(ApplicationRuntime context) {
+ protected ModuleTracingSupport(ApplicationModulesRuntime runtime) {
- Assert.notNull(context, "ApplicationContext must not be null!");
+ Assert.notNull(runtime, "ModulesRuntime must not be null!");
- this.modules = ModulesRuntime.of(context);
- this.context = context;
+ this.runtime = runtime;
}
/*
@@ -51,17 +48,8 @@ class ModuleTracingSupport implements BeanClassLoaderAware {
this.classLoader = classLoader;
}
- protected final ApplicationModules getModules() {
-
- try {
- return modules.get();
- } catch (Exception o_O) {
- throw new RuntimeException(o_O);
- }
- }
-
protected final Class> getBeanUserClass(Object bean, String beanName) {
- return context.getUserClass(bean, beanName);
+ return runtime.getUserClass(bean, beanName);
}
protected final Object addAdvisor(Object bean, Advisor advisor) {
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModulesRuntime.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModulesRuntime.java
deleted file mode 100644
index bb972510..00000000
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModulesRuntime.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2022 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.observability;
-
-import lombok.RequiredArgsConstructor;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.function.Supplier;
-
-import org.springframework.modulith.model.ApplicationModules;
-
-/**
- * Bootstrap type to make sure we only bootstrap the initialization of a {@link ApplicationModules} instance per application class
- * once.
- *
- * @author Oliver Drotbohm
- */
-@RequiredArgsConstructor
-public class ModulesRuntime implements Supplier {
-
- private static final Map MODULES = new HashMap<>();
-
- private final Supplier modules;
- private final ApplicationRuntime runtime;
-
- /*
- * (non-Javadoc)
- * @see java.util.function.Supplier#get()
- */
- @Override
- public ApplicationModules get() {
- return modules.get();
- }
-
- boolean isApplicationClass(Class> type) {
- return runtime.isApplicationClass(type);
- }
-
- public static ModulesRuntime of(ApplicationRuntime runtime) {
-
- return MODULES.computeIfAbsent(runtime.getId(), it -> {
-
- Class> mainClass = runtime.getMainApplicationClass();
- Future modules = Executors.newFixedThreadPool(1).submit(() -> ApplicationModules.of(mainClass));
-
- return new ModulesRuntime(toSupplier(modules), runtime);
- });
- }
-
- private static Supplier toSupplier(Future modules) {
-
- return () -> {
- try {
- return modules.get();
- } catch (Exception o_O) {
- throw new RuntimeException(o_O);
- // TODO: handle exception
- }
- };
- }
-}
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java
index 87e2eb29..ed448ad5 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java
@@ -18,6 +18,8 @@ package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
+import java.util.function.Supplier;
+
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@@ -30,6 +32,7 @@ import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* @author Oliver Drotbohm
@@ -37,9 +40,9 @@ import org.springframework.modulith.model.ApplicationModules;
public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor {
private final Tracer tracer;
- private final ApplicationRuntime runtime;
+ private final ApplicationModulesRuntime runtime;
- public SpringDataRestModuleTracingBeanPostProcessor(ApplicationRuntime runtime, Tracer tracer) {
+ public SpringDataRestModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Tracer tracer) {
super(runtime);
@@ -60,7 +63,7 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
return bean;
}
- Advice interceptor = new DataRestControllerInterceptor(getModules(), tracer);
+ Advice interceptor = new DataRestControllerInterceptor(runtime, tracer);
Advisor advisor = new DefaultPointcutAdvisor(interceptor);
return addAdvisor(bean, advisor, it -> it.setProxyTargetClass(true));
@@ -69,7 +72,7 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
@RequiredArgsConstructor
private static class DataRestControllerInterceptor implements MethodInterceptor {
- private final ApplicationModules modules;
+ private final Supplier modules;
private final Tracer tracer;
/*
@@ -100,7 +103,7 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
RootResourceInformation info = (RootResourceInformation) argument;
- return modules.getModuleByType(info.getDomainType().getName()).orElse(null);
+ return modules.get().getModuleByType(info.getDomainType().getName()).orElse(null);
}
return null;
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/ModuleObservabilityAutoConfiguration.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/ModuleObservabilityAutoConfiguration.java
index e82726ff..9605c7bb 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/ModuleObservabilityAutoConfiguration.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/ModuleObservabilityAutoConfiguration.java
@@ -27,13 +27,11 @@ import io.micrometer.tracing.Tracer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.modulith.observability.ApplicationRuntime;
import org.springframework.modulith.observability.ModuleEventListener;
import org.springframework.modulith.observability.ModuleTracingBeanPostProcessor;
-import org.springframework.modulith.observability.ModulesRuntime;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* @author Oliver Drotbohm
@@ -43,19 +41,15 @@ import org.springframework.modulith.observability.ModulesRuntime;
class ModuleObservabilityAutoConfiguration {
@Bean
- static SpringBootApplicationRuntime modulithsApplicationRuntime(ApplicationContext context) {
- return new SpringBootApplicationRuntime(context);
- }
-
- @Bean
- static ModuleTracingBeanPostProcessor moduleTracingBeanPostProcessor(ApplicationRuntime runtime,
+ static ModuleTracingBeanPostProcessor moduleTracingBeanPostProcessor(ApplicationModulesRuntime runtime,
Tracer tracer) {
return new ModuleTracingBeanPostProcessor(runtime, tracer);
}
@Bean
- static ModuleEventListener tracingModuleEventListener(ApplicationRuntime runtime, ObjectProvider tracer) {
- return new ModuleEventListener(ModulesRuntime.of(runtime), () -> tracer.getObject());
+ static ModuleEventListener tracingModuleEventListener(ApplicationModulesRuntime runtime,
+ ObjectProvider tracer) {
+ return new ModuleEventListener(runtime, () -> tracer.getObject());
}
/**
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringDataRestModuleObservabilityAutoConfiguration.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringDataRestModuleObservabilityAutoConfiguration.java
index d226644b..1fa1ff3f 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringDataRestModuleObservabilityAutoConfiguration.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringDataRestModuleObservabilityAutoConfiguration.java
@@ -21,8 +21,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.webmvc.RepositoryController;
-import org.springframework.modulith.observability.ApplicationRuntime;
import org.springframework.modulith.observability.SpringDataRestModuleTracingBeanPostProcessor;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* @author Oliver Drotbohm
@@ -33,7 +33,7 @@ class SpringDataRestModuleObservabilityAutoConfiguration {
@Bean
static SpringDataRestModuleTracingBeanPostProcessor springDataRestModuleTracingBeanPostProcessor(
- ApplicationRuntime runtime, Tracer tracer) {
+ ApplicationModulesRuntime runtime, Tracer tracer) {
return new SpringDataRestModuleTracingBeanPostProcessor(runtime, tracer);
}
}
diff --git a/spring-modulith-runtime/pom.xml b/spring-modulith-runtime/pom.xml
new file mode 100644
index 00000000..5f38ee5a
--- /dev/null
+++ b/spring-modulith-runtime/pom.xml
@@ -0,0 +1,45 @@
+
+ 4.0.0
+
+
+ org.springframework.experimental
+ spring-modulith
+ 0.2.0-SNAPSHOT
+
+
+ Spring Modulith - Runtime support
+ spring-modulith-runtime
+
+
+ org.springframework.modulith.runtime
+
+
+
+
+
+ org.springframework.experimental
+ spring-modulith-core
+ ${project.version}
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-actuator-autoconfigure
+ test
+
+
+
+
+
diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java
new file mode 100644
index 00000000..2997f155
--- /dev/null
+++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 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.runtime;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+import java.util.function.Supplier;
+
+import org.springframework.modulith.model.ApplicationModules;
+
+/**
+ * Bootstrap type to make sure we only bootstrap the initialization of a {@link ApplicationModules} instance once per
+ * application class.
+ *
+ * @author Oliver Drotbohm
+ */
+@RequiredArgsConstructor
+public class ApplicationModulesRuntime implements Supplier {
+
+ private final @NonNull Supplier modules;
+ private final @NonNull ApplicationRuntime runtime;
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.function.Supplier#get()
+ */
+ @Override
+ public ApplicationModules get() {
+ return modules.get();
+ }
+
+ /**
+ * Returns whether a given {@link Class} is considered an application one (versus Framework ones).
+ *
+ * @param type
+ * @return
+ */
+ public boolean isApplicationClass(Class> type) {
+ return runtime.isApplicationClass(type);
+ }
+
+ /**
+ * Returns the actual user class for a given bean and bean name.
+ *
+ * @param bean must not be {@literal null}.
+ * @param beanName must not be {@literal null} or empty.
+ * @return will never be {@literal null}.
+ */
+ public Class> getUserClass(Object bean, String beanName) {
+ return runtime.getUserClass(bean, beanName);
+ }
+}
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ApplicationRuntime.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationRuntime.java
similarity index 76%
rename from spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ApplicationRuntime.java
rename to spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationRuntime.java
index f12713b3..4fc89b2c 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ApplicationRuntime.java
+++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationRuntime.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.modulith.observability;
+package org.springframework.modulith.runtime;
/**
* Abstraction of the application runtime environment. Primarily to keep references to Spring Boot out of the core
@@ -26,14 +26,14 @@ public interface ApplicationRuntime {
/**
* Returns the identifier of the application.
*
- * @return
+ * @return will never be {@literal null}.
*/
String getId();
/**
* Returns the primary application class.
*
- * @return
+ * @return will never be {@literal null}.
*/
Class> getMainApplicationClass();
@@ -41,18 +41,17 @@ public interface ApplicationRuntime {
* Obtain the end user class for the given bean and bean name. Necessary to reveal the actual user type from
* potentially proxied instances.
*
- * @param bean
- * @param beanName
- * @return
+ * @param bean must not be {@literal null}.
+ * @param beanName must not be {@literal null} or empty.
+ * @return will never be {@literal null}.
*/
Class> getUserClass(Object bean, String beanName);
/**
* Returns whether the given type is an application class, i.e. user code in one of the application packages.
*
- * @param type
- * @return
- * @see #getApplicationPackages()
+ * @param type must not be {@literal null}.
+ * @return whether the given type is an application class, i.e. user code in one of the application packages.
*/
boolean isApplicationClass(Class> type);
}
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntime.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java
similarity index 89%
rename from spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntime.java
rename to spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java
index 4294ddb6..8163deb1 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntime.java
+++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.modulith.observability.autoconfigure;
+package org.springframework.modulith.runtime.autoconfigure;
import lombok.RequiredArgsConstructor;
@@ -23,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
-import org.springframework.modulith.observability.ApplicationRuntime;
+import org.springframework.modulith.runtime.ApplicationRuntime;
import org.springframework.util.ClassUtils;
/**
@@ -52,7 +52,7 @@ class SpringBootApplicationRuntime implements ApplicationRuntime {
@Override
public Class> getMainApplicationClass() {
- String[] mainBeanNames = context.getBeanNamesForAnnotation(SpringBootApplication.class);
+ var mainBeanNames = context.getBeanNamesForAnnotation(SpringBootApplication.class);
return context.getType(mainBeanNames[0]);
}
@@ -64,7 +64,7 @@ class SpringBootApplicationRuntime implements ApplicationRuntime {
@Override
public Class> getUserClass(Object bean, String beanName) {
- Class> beanType = context.containsBean(beanName)
+ var beanType = context.containsBean(beanName)
? context.getType(beanName)
: bean.getClass();
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
new file mode 100644
index 00000000..83c64baa
--- /dev/null
+++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 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.runtime.autoconfigure;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Supplier;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.modulith.model.ApplicationModule;
+import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
+import org.springframework.modulith.runtime.ApplicationRuntime;
+
+/**
+ * Auto-configuration to register a {@link SpringBootApplicationRuntime} and {@link ApplicationModulesRuntime} as Spring
+ * Bean.
+ *
+ * @author Oliver Drotbohm
+ */
+@Slf4j
+@AutoConfiguration
+class SpringModulithRuntimeAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(ApplicationRuntime.class)
+ SpringBootApplicationRuntime modulithsApplicationRuntime(ApplicationContext context) {
+ return new SpringBootApplicationRuntime(context);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ ApplicationModulesRuntime modulesRuntime(ApplicationRuntime runtime) {
+
+ var mainClass = runtime.getMainApplicationClass();
+ var modules = Executors.newFixedThreadPool(1)
+ .submit(() -> SpringModulithRuntimeAutoConfiguration.initializeApplicationModules(mainClass));
+
+ return new ApplicationModulesRuntime(toSupplier(modules), runtime);
+ }
+
+ private static ApplicationModules initializeApplicationModules(Class> applicationMainClass) {
+
+ LOG.debug("Obtaining Spring Modulith application modules…");
+
+ var result = ApplicationModules.of(applicationMainClass);
+ var numberOfModules = result.stream().count();
+
+ if (numberOfModules == 0) {
+
+ LOG.warn("No application modules detected!");
+
+ } else {
+
+ LOG.debug("Detected {} application modules: {}", //
+ result.stream().count(), //
+ result.stream().map(ApplicationModule::getName).toList());
+ }
+
+ return result;
+ }
+
+ private static Supplier toSupplier(Future modules) {
+
+ return () -> {
+ try {
+ return modules.get();
+ } catch (Exception o_O) {
+ throw new RuntimeException(o_O);
+ // TODO: handle exception
+ }
+ };
+ }
+}
diff --git a/spring-modulith-runtime/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-modulith-runtime/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..f29aff39
--- /dev/null
+++ b/spring-modulith-runtime/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfiguration
diff --git a/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntimeUnitTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntimeUnitTests.java
similarity index 88%
rename from spring-modulith-observability/src/test/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntimeUnitTests.java
rename to spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntimeUnitTests.java
index 3dcfcac4..18c652c2 100644
--- a/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/autoconfigure/SpringBootApplicationRuntimeUnitTests.java
+++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntimeUnitTests.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.modulith.observability.autoconfigure;
+package org.springframework.modulith.runtime.autoconfigure;
import static org.assertj.core.api.Assertions.*;
@@ -23,7 +23,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.context.ApplicationContext;
-import org.springframework.modulith.observability.ApplicationRuntime;
+import org.springframework.modulith.runtime.ApplicationRuntime;
/**
* Unit tests for {@link SpringBootApplicationRuntime}.
@@ -31,11 +31,11 @@ import org.springframework.modulith.observability.ApplicationRuntime;
* @author Oliver Drotbohm
*/
@ExtendWith(MockitoExtension.class)
-public class SpringBootApplicationRuntimeUnitTests {
+class SpringBootApplicationRuntimeUnitTests {
@Mock ApplicationContext context;
- @Test
+ @Test // GH-87
void extractsUserTypeFromClassBasedProxy() {
Object proxy = new ProxyFactory(new Sample()).getProxy();
diff --git a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java
new file mode 100644
index 00000000..545c17bd
--- /dev/null
+++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.runtime.autoconfigure;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.modulith.runtime.ApplicationModulesRuntime;
+import org.springframework.modulith.runtime.ApplicationRuntime;
+
+/**
+ * Integration thest for {@link SpringModulithRuntimeAutoConfiguration}.
+ *
+ * @author Oliver Drotbohm
+ */
+@SpringBootTest
+class SpringModulithRuntimeAutoConfigurationIntegrationTests {
+
+ @SpringBootApplication
+ static class SampleApp {}
+
+ @Autowired ApplicationContext context;
+
+ @Test // GH-87
+ void bootstrapRegistersRuntimeInstances() {
+
+ assertThat(context.getBean(ApplicationRuntime.class)).isNotNull();
+ assertThat(context.getBean(ApplicationModulesRuntime.class)).isNotNull();
+ }
+}
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
new file mode 100644
index 00000000..a89055a6
--- /dev/null
+++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/TestApplicationModules.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.test;
+
+import java.util.List;
+
+import org.springframework.modulith.model.ApplicationModules;
+import org.springframework.modulith.model.ModulithMetadata;
+
+import com.tngtech.archunit.base.DescribedPredicate;
+import com.tngtech.archunit.core.importer.ImportOption;
+
+/**
+ * Utility methods to work with test {@link ApplicationModules}. Not intended public API!
+ *
+ * @author Oliver Drotbohm
+ */
+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
+ */
+ public static ApplicationModules of(String basePackage) {
+ return new ApplicationModules(ModulithMetadata.of(basePackage), List.of(basePackage),
+ DescribedPredicate.alwaysFalse(), false, new ImportOption.OnlyIncludeTests()) {};
+ }
+}
diff --git a/src/docs/asciidoc/70-observability.adoc b/src/docs/asciidoc/70-observability.adoc
index 3f88339f..f213fedb 100644
--- a/src/docs/asciidoc/70-observability.adoc
+++ b/src/docs/asciidoc/70-observability.adoc
@@ -24,3 +24,91 @@ image::observability.png[]
In this particular case, triggering the payment changes the state of the order which then causes an order completion event being triggered.
This gets picked up asynchronously by the engine that triggers another state change on the order, works for a couple of seconds and triggers the final state change on the order in turn.
+== Application Module Actuator
+
+The application module structure can be exposed as Spring Boot actuator.
+To enable the actuator, add the `spring-modulith-actuator` dependency to the project:
+
+[source, xml]
+----
+
+ org.springframework.modulith
+ spring-modulith-actuator
+ {projectVersion}
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+ …
+ runtime
+
+----
+
+Running the application will now expose an `applicationmodules` actuator resource:
+
+[source, json]
+----
+GET http://localhost:8080/actuator
+
+{
+ "_links": {
+ "self": {
+ "href": "http://localhost:8080/actuator",
+ "templated": false
+ },
+ "health-path": {
+ "href": "http://localhost:8080/actuator/health/{*path}",
+ "templated": true
+ },
+ "health": {
+ "href": "http://localhost:8080/actuator/health",
+ "templated": false
+ },
+ "applicationmodules": { <1>
+ "href": "http://localhost:8080/actuator/applicationmodules",
+ "templated": false
+ }
+ }
+}
+----
+<1> The `applicationmodules` actuator resource advertised.
+
+The `applicationmodules` resource adheres to the following structure:
+
+[%autowidth.stretch]
+|===
+|JSONPath|Description
+
+|`$.{moduleName}`|The technical name of an application module. Target for module references in `dependencies.target`.
+|`$.{moduleName}.displayName`|The human-readable name of the application module.
+|`$.{moduleName}.basePackage`|The application module's base package.
+|`$.{moduleName}.dependencies[]`|All outgoing dependencies of the application module
+|`$.{moduleName}.dependencies[].target`|The name of the application module depended on. A reference to a `{moduleName}`.
+|`$.{moduleName}.dependencies[].types[]`|The types of dependencies towards the target module. Can either be `DEFAULT` (simple type dependency), `USES_COMPONENT` (Spring bean dependency) or `EVENT_LISTENER`.
+|===
+
+An example module arrangement would look like this:
+
+[source, json]
+----
+{
+ "a": {
+ "basePackage": "example.a",
+ "displayName": "A",
+ "dependencies": []
+ },
+ "b": {
+ "basePackage": "example.b",
+ "displayName": "B",
+ "dependencies": [ {
+ "target": "a",
+ "types": [ "EVENT_LISTENER", "USES_COMPONENT" ]
+ } ]
+ }
+}
+
+----
+