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:
Oliver Drotbohm
2022-12-22 20:43:39 +01:00
parent 9dff5cc973
commit c1fc3032b7
34 changed files with 847 additions and 150 deletions

View File

@@ -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>

View 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>

View File

@@ -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() //
);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
org.springframework.modulith.actuator.autoconfigure.ApplicationModulesEndpointConfiguration

View 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 {}

View File

@@ -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 {
}

View File

@@ -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) {}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View 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>

View File

@@ -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>

View File

@@ -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()));
}
/**

View File

@@ -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}.
*/

View File

@@ -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!";

View File

@@ -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");
}
}

View File

@@ -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>

View File

@@ -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;
/*

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
}
};
}
}

View File

@@ -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;

View File

@@ -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());
}
/**

View File

@@ -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);
}
}

View 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>

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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
}
};
}
}

View File

@@ -0,0 +1 @@
org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfiguration

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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()) {};
}
}

View File

@@ -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" ]
} ]
}
}
----