GH-87 - Expose module structure as actuator endpoint.
Introduce Spring Modulith Actuator module to expose the application module structure as Spring Boot actuator. This required a Spring Modulith Runtime module to be extracted from the Spring Modulith Observability one. The former now contains the auto-configured ApplicationRuntime and ApplicationModulesRuntime bean instances to be able to bootstrap a ApplicationModules asynchronously on application startup. The actuator module then contains a Spring Boot @Endpoint implementation consuming the ApplicationModules to render JSON describing the application module structure.
This commit is contained in:
9
pom.xml
9
pom.xml
@@ -18,18 +18,20 @@
|
||||
<url>https://spring.io/projects/spring-modulith</url>
|
||||
|
||||
<modules>
|
||||
<module>spring-modulith-actuator</module>
|
||||
<module>spring-modulith-api</module>
|
||||
<module>spring-modulith-bom</module>
|
||||
<module>spring-modulith-core</module>
|
||||
<module>spring-modulith-events</module>
|
||||
<module>spring-modulith-test</module>
|
||||
<module>spring-modulith-docs</module>
|
||||
<module>spring-modulith-observability</module>
|
||||
<module>spring-modulith-events</module>
|
||||
<module>spring-modulith-moments</module>
|
||||
<module>spring-modulith-observability</module>
|
||||
<module>spring-modulith-runtime</module>
|
||||
<module>spring-modulith-starter-jdbc</module>
|
||||
<module>spring-modulith-starter-jpa</module>
|
||||
<module>spring-modulith-starter-mongodb</module>
|
||||
<module>spring-modulith-starter-test</module>
|
||||
<module>spring-modulith-test</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
@@ -449,6 +451,7 @@ limitations under the License.
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
||||
56
spring-modulith-actuator/pom.xml
Normal file
56
spring-modulith-actuator/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Modulith - Actuator</name>
|
||||
<artifactId>spring-modulith-actuator</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springframework.modulith.actuator</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-runtime</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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<DependencyType>, Set<DependencyType>> 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<ApplicationModuleDependency, ?, Set<DependencyType>> MAPPER = mapping(
|
||||
ApplicationModuleDependency::getDependencyType,
|
||||
collectingAndThen(toSet(), REMOVE_DEFAULT_DEPENDENCY_TYPE_IF_OTHERS_PRESENT));
|
||||
|
||||
private final Supplier<ApplicationModules> runtime;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModulesEndpoint} for the given {@link ModulesRuntime}.
|
||||
*
|
||||
* @param runtime must not be {@literal null}.
|
||||
*/
|
||||
public ApplicationModulesEndpoint(Supplier<ApplicationModules> 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<String, Object> getApplicationModules() {
|
||||
|
||||
var modules = runtime.get();
|
||||
|
||||
return modules.stream()
|
||||
.collect(Collectors.toMap(ApplicationModule::getName, it -> toInfo(it, modules)));
|
||||
}
|
||||
|
||||
private static Map<String, Object> 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<String, Object> toInfo(Entry<ApplicationModule, ? extends Set<DependencyType>> types) {
|
||||
|
||||
return Map.of( //
|
||||
"target", types.getKey().getName(), //
|
||||
"types", types.getValue() //
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.modulith.actuator.autoconfigure.ApplicationModulesEndpointConfiguration
|
||||
24
spring-modulith-actuator/src/test/java/example/App.java
Normal file
24
spring-modulith-actuator/src/test/java/example/App.java
Normal file
@@ -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 {}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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.<String> read("$.a.basePackage")).isEqualTo("example.a");
|
||||
assertThat(context.<JSONArray> read("$.a.dependencies")).isEmpty();
|
||||
|
||||
assertThat(context.<String> read("$.b.basePackage")).isEqualTo("example.b");
|
||||
assertThat(context.<String> read("$.b.dependencies[0].target")).isEqualTo("a");
|
||||
assertThat(context.<JSONArray> read("$.b.dependencies[0].types"))
|
||||
.containsExactlyInAnyOrder("EVENT_LISTENER", "USES_COMPONENT");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
14
spring-modulith-actuator/src/test/resources/logback.xml
Normal file
14
spring-modulith-actuator/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="error">
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -19,6 +19,11 @@
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-actuator</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-api</artifactId>
|
||||
@@ -69,6 +74,11 @@
|
||||
<artifactId>spring-modulith-observability</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-runtime</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-starter-jdbc</artifactId>
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,8 +54,8 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
|
||||
private static final Map<CacheKey, ApplicationModules> 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<ApplicationModule> {
|
||||
|
||||
private boolean verified;
|
||||
|
||||
private ApplicationModules(ModulithMetadata metadata, Collection<String> packages,
|
||||
DescribedPredicate<JavaClass> ignored,
|
||||
boolean useFullyQualifiedModuleNames) {
|
||||
protected ApplicationModules(ModulithMetadata metadata, Collection<String> packages,
|
||||
DescribedPredicate<JavaClass> 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<ApplicationModule> {
|
||||
basePackages.addAll(metadata.getAdditionalPackages());
|
||||
|
||||
ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
|
||||
metadata.useFullyQualifiedModuleNames());
|
||||
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
|
||||
|
||||
Set<ApplicationModule> sharedModules = metadata.getSharedModuleNames() //
|
||||
.map(modules::getRequiredModule) //
|
||||
@@ -281,7 +280,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
|
||||
@@ -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!";
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-runtime</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
|
||||
@@ -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<ApplicationEvent> {
|
||||
|
||||
private final ModulesRuntime modules;
|
||||
private final ApplicationModulesRuntime modules;
|
||||
private final Supplier<Tracer> tracer;
|
||||
|
||||
/*
|
||||
|
||||
@@ -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<String, Advisor> 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)
|
||||
|
||||
@@ -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<ApplicationModules> 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) {
|
||||
|
||||
@@ -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<ApplicationModules> {
|
||||
|
||||
private static final Map<String, ModulesRuntime> MODULES = new HashMap<>();
|
||||
|
||||
private final Supplier<ApplicationModules> 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<ApplicationModules> modules = Executors.newFixedThreadPool(1).submit(() -> ApplicationModules.of(mainClass));
|
||||
|
||||
return new ModulesRuntime(toSupplier(modules), runtime);
|
||||
});
|
||||
}
|
||||
|
||||
private static Supplier<ApplicationModules> toSupplier(Future<ApplicationModules> modules) {
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
return modules.get();
|
||||
} catch (Exception o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
// TODO: handle exception
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<ApplicationModules> 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;
|
||||
|
||||
@@ -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> tracer) {
|
||||
return new ModuleEventListener(ModulesRuntime.of(runtime), () -> tracer.getObject());
|
||||
static ModuleEventListener tracingModuleEventListener(ApplicationModulesRuntime runtime,
|
||||
ObjectProvider<Tracer> tracer) {
|
||||
return new ModuleEventListener(runtime, () -> tracer.getObject());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
45
spring-modulith-runtime/pom.xml
Normal file
45
spring-modulith-runtime/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Modulith - Runtime support</name>
|
||||
<artifactId>spring-modulith-runtime</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springframework.modulith.runtime</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-modulith-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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<ApplicationModules> {
|
||||
|
||||
private final @NonNull Supplier<ApplicationModules> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ApplicationModules> toSupplier(Future<ApplicationModules> modules) {
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
return modules.get();
|
||||
} catch (Exception o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
// TODO: handle exception
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfiguration
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}. <em>Not intended public API!</em>
|
||||
*
|
||||
* @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()) {};
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-actuator</artifactId>
|
||||
<version>{projectVersion}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot actuator starter required to enable actuators in general -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<version>…</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
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" ]
|
||||
} ]
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user