GH-119 - Improve rendering of application modules structure as JSON.

Introduced ApplicationModulesExporter to render an ApplicationModules instances as JSON directly. To avoid a dependency to a JSON library and as we only have to be able to render rather simple arrangements, we just build up the JSON string ourselves.

ApplicationModulesEndpoint now caches the structure calculated once to avoid repeated work.
This commit is contained in:
Oliver Drotbohm
2023-01-18 18:31:56 +01:00
parent 9082ca27bb
commit 090ddc6fba
6 changed files with 254 additions and 58 deletions

View File

@@ -15,26 +15,17 @@
*/
package org.springframework.modulith.actuator;
import static java.util.stream.Collectors.*;
import java.util.LinkedHashMap;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModuleDependency;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.DependencyType;
import org.springframework.modulith.core.util.ApplicationModulesExporter;
import org.springframework.util.Assert;
import org.springframework.util.function.SingletonSupplier;
/**
* A Spring Boot actuator endpoint to expose the application module structure of a Spring Modulith based application.
@@ -46,20 +37,7 @@ public class ApplicationModulesEndpoint {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesEndpoint.class);
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;
private final SingletonSupplier<String> structure;
/**
* Creates a new {@link ApplicationModulesEndpoint} for the given {@link ApplicationModules}.
@@ -72,7 +50,7 @@ public class ApplicationModulesEndpoint {
LOGGER.debug("Activating Spring Modulith actuator.");
this.runtime = runtime;
this.structure = SingletonSupplier.of(new ApplicationModulesExporter(runtime.get())::toJson);
}
/**
@@ -81,34 +59,7 @@ public class ApplicationModulesEndpoint {
* @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), (l, r) -> r, LinkedHashMap::new));
}
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() //
);
String getApplicationModules() {
return structure.obtain();
}
}

View File

@@ -22,7 +22,6 @@ 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;
/**
@@ -38,7 +37,7 @@ class ApplicationModulesEndpointIntegrationTests {
var modules = TestApplicationModules.of("example");
var endpoint = new ApplicationModulesEndpoint(() -> modules);
var result = endpoint.getApplicationModules();
var context = JsonPath.parse(new ObjectMapper().writeValueAsString(result));
var context = JsonPath.parse(result);
assertThat(context.<String> read("$.a.basePackage")).isEqualTo("example.a");
assertThat(context.<JSONArray> read("$.a.dependencies")).isEmpty();