From dbb075ecd8c15496709dd24250b344dbd356b0a2 Mon Sep 17 00:00:00 2001 From: Stefan Rempfer Date: Fri, 24 Apr 2020 22:33:33 +0200 Subject: [PATCH] Added Jackson Module for Spring Boot Admin closes #1348 --- .../config/AdminServerWebConfiguration.java | 11 +- .../InstanceRegistrationUpdatedEvent.java | 8 +- .../admin/server/domain/values/Endpoint.java | 6 +- .../admin/server/domain/values/Endpoints.java | 3 + .../InstanceEndpointsDetectedEventMixin.java | 21 +++ .../utils/jackson/InstanceEventMixin.java | 34 +++++ .../utils/jackson/SpringBootAdminModule.java | 49 +++++++ .../utils/jackson/InstanceEventMixinTest.java | 122 ++++++++++++++++++ .../jackson/RegistrationDeserializerTest.java | 5 +- 9 files changed, 243 insertions(+), 16 deletions(-) create mode 100644 spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixin.java create mode 100644 spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixin.java create mode 100644 spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/SpringBootAdminModule.java create mode 100644 spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixinTest.java diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java index 82312338b5e..046f1f909a2 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java @@ -26,13 +26,10 @@ import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; -import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; import de.codecentric.boot.admin.server.services.ApplicationRegistry; import de.codecentric.boot.admin.server.services.InstanceRegistry; -import de.codecentric.boot.admin.server.utils.jackson.RegistrationBeanSerializerModifier; -import de.codecentric.boot.admin.server.utils.jackson.RegistrationDeserializer; -import de.codecentric.boot.admin.server.utils.jackson.SanitizingMapSerializer; +import de.codecentric.boot.admin.server.utils.jackson.SpringBootAdminModule; import de.codecentric.boot.admin.server.web.ApplicationsController; import de.codecentric.boot.admin.server.web.InstancesController; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; @@ -48,11 +45,7 @@ public AdminServerWebConfiguration(AdminServerProperties adminServerProperties) @Bean public SimpleModule adminJacksonModule() { - SimpleModule module = new SimpleModule(); - module.addDeserializer(Registration.class, new RegistrationDeserializer()); - module.setSerializerModifier(new RegistrationBeanSerializerModifier( - new SanitizingMapSerializer(this.adminServerProperties.getMetadataKeysToSanitize()))); - return module; + return new SpringBootAdminModule(this.adminServerProperties.getMetadataKeysToSanitize()); } @Bean diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegistrationUpdatedEvent.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegistrationUpdatedEvent.java index f063160b31a..99c1c8da659 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegistrationUpdatedEvent.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegistrationUpdatedEvent.java @@ -18,6 +18,8 @@ import java.time.Instant; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; @@ -39,8 +41,10 @@ public InstanceRegistrationUpdatedEvent(InstanceId instance, long version, Regis this(instance, version, Instant.now(), registration); } - public InstanceRegistrationUpdatedEvent(InstanceId instance, long version, Instant timestamp, - Registration registration) { + @JsonCreator + public InstanceRegistrationUpdatedEvent(@JsonProperty("instance") InstanceId instance, + @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, + @JsonProperty("registration") Registration registration) { super(instance, version, "REGISTRATION_UPDATED", timestamp); this.registration = registration; } diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java index 9e2b9e508c5..239259b40a0 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java @@ -18,6 +18,9 @@ import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import org.springframework.util.Assert; @lombok.Data @@ -52,7 +55,8 @@ private Endpoint(String id, String url) { this.url = url; } - public static Endpoint of(String id, String url) { + @JsonCreator + public static Endpoint of(@JsonProperty("id") String id, @JsonProperty("url") String url) { return new Endpoint(id, url); } diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java index 8cdb12f6217..970cbd5b5e3 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java @@ -28,6 +28,8 @@ import javax.annotation.Nullable; +import com.fasterxml.jackson.annotation.JsonCreator; + import static java.util.stream.Collectors.toMap; @lombok.EqualsAndHashCode @@ -68,6 +70,7 @@ public static Endpoints single(String id, String url) { return new Endpoints(Collections.singletonList(Endpoint.of(id, url))); } + @JsonCreator public static Endpoints of(@Nullable Collection endpoints) { if (endpoints == null || endpoints.isEmpty()) { return empty(); diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixin.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixin.java new file mode 100644 index 00000000000..a5e8b838c19 --- /dev/null +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixin.java @@ -0,0 +1,21 @@ +package de.codecentric.boot.admin.server.utils.jackson; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.domain.values.InstanceId; + +/** + * Jackson Mixin for {@link InstanceEndpointsDetectedEvent}. + */ +public class InstanceEndpointsDetectedEventMixin { + + @JsonCreator + public InstanceEndpointsDetectedEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, + @JsonProperty("timestamp") Instant timestamp, @JsonProperty("endpoints") Endpoints endpoints) { + } +} + diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixin.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixin.java new file mode 100644 index 00000000000..1490dcf58e1 --- /dev/null +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixin.java @@ -0,0 +1,34 @@ +package de.codecentric.boot.admin.server.utils.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link InstanceEvent}s. + * + * @author Stefan Rempfer + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = InstanceEndpointsDetectedEvent.class, name = "ENDPOINTS_DETECTED"), + @JsonSubTypes.Type(value = InstanceRegistrationUpdatedEvent.class, name = "REGISTRATION_UPDATED"), + @JsonSubTypes.Type(value = InstanceInfoChangedEvent.class, name = "INFO_CHANGED"), + @JsonSubTypes.Type(value = InstanceDeregisteredEvent.class, name = "DEREGISTERED"), + @JsonSubTypes.Type(value = InstanceRegisteredEvent.class, name = "REGISTERED"), + @JsonSubTypes.Type(value = InstanceStatusChangedEvent.class, name = "STATUS_CHANGED") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class InstanceEventMixin { + +} diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/SpringBootAdminModule.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/SpringBootAdminModule.java new file mode 100644 index 00000000000..aa22653fe62 --- /dev/null +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/SpringBootAdminModule.java @@ -0,0 +1,49 @@ +package de.codecentric.boot.admin.server.utils.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; +import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.domain.values.Registration; + +/** + * Jackson module for spring-boot-admin. This module register {@link InstanceEventMixin}. + * In order to use this module just add this modules into your ObjectMapper configuration. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new SpringBootAdminModule());
+ *     mapper.registerModule(new JavaTimeModule());
+ * 
+ * + * @author Stefan Rempfer + */ +public class SpringBootAdminModule extends SimpleModule { + + private final String[] metadataKeyPatterns; + + /** + * Constructs the module with a pattern for metadata keys. + * The values of the matched metadata keys will be sanitized before serializing to json. + * + * @param metadataKeyPatterns pattern for metadata keys which should be sanitized + */ + public SpringBootAdminModule(String[] metadataKeyPatterns) { + super(SpringBootAdminModule.class.getName(), new Version(1, 0, 0, null, null, null)); + this.metadataKeyPatterns = metadataKeyPatterns; + } + + @Override + public void setupModule(SetupContext context) { + SimpleDeserializers simpleDeserializers = new SimpleDeserializers(); + simpleDeserializers.addDeserializer(Registration.class, new RegistrationDeserializer()); + context.addDeserializers(simpleDeserializers); + + context.addBeanSerializerModifier( + new RegistrationBeanSerializerModifier(new SanitizingMapSerializer(metadataKeyPatterns))); + + context.setMixInAnnotations(InstanceEvent.class, InstanceEventMixin.class); + context.setMixInAnnotations(InstanceEndpointsDetectedEvent.class, InstanceEndpointsDetectedEventMixin.class); + } +} diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixinTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixinTest.java new file mode 100644 index 00000000000..8411697a108 --- /dev/null +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixinTest.java @@ -0,0 +1,122 @@ +package de.codecentric.boot.admin.server.utils.jackson; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.domain.values.InstanceId; +import de.codecentric.boot.admin.server.domain.values.Registration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InstanceEventMixinTest { + + private final ObjectMapper objectMapper; + + public InstanceEventMixinTest() { + SpringBootAdminModule springBootAdminJacksonModule = new SpringBootAdminModule(new String[] { ".*password$" }); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + objectMapper = Jackson2ObjectMapperBuilder.json().modules(springBootAdminJacksonModule, javaTimeModule).build(); + } + + @Test + public void verifySerializeOfInstanceId() throws JsonProcessingException { + InstanceId id = InstanceId.of("test123"); + String json = objectMapper.writeValueAsString(id); + assertThat(json).isEqualTo("\"test123\""); + } + + @Test + public void verifyDeserializeOfInstanceId() throws JsonProcessingException { + InstanceId id = objectMapper.readValue("\"test123\"", InstanceId.class); + assertThat(id).isEqualTo(InstanceId.of("test123")); + } + + @Nested + class InstanceEndpointsDetectedEventTests { + static final String INSTANCE_ENDPOINTS_DETECTED_EVENT_JSON = + "{\"instance\":\"test123\",\"version\":12345678,\"timestamp\":1587751031.000000000,\"endpoints\":[" + + "{\"id\":\"health\",\"url\":\"http://localhost:8080/health\"}," + + "{\"id\":\"info\",\"url\":\"http://localhost:8080/info\"}" + + "],\"type\":\"ENDPOINTS_DETECTED\"}"; + + @Test + public void verifySerialize() throws JsonProcessingException { + InstanceId id = InstanceId.of("test123"); + Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); + Endpoints endpoints = Endpoints.single("info", "http://localhost:8080/info") + .withEndpoint("health", "http://localhost:8080/health"); + + InstanceEndpointsDetectedEvent event = new InstanceEndpointsDetectedEvent(id, 12345678L, timestamp, endpoints); + String json = objectMapper.writeValueAsString(event); + assertThat(json).isEqualTo(INSTANCE_ENDPOINTS_DETECTED_EVENT_JSON); + } + + @Test + public void verifyDeserialize() throws JsonProcessingException { + InstanceEndpointsDetectedEvent event = objectMapper + .readValue(INSTANCE_ENDPOINTS_DETECTED_EVENT_JSON, InstanceEndpointsDetectedEvent.class); + assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); + assertThat(event.getVersion()).isEqualTo(12345678L); + assertThat(event.getTimestamp()) + .isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); + Endpoints endpoints = event.getEndpoints(); + assertThat(endpoints).contains(Endpoint.of("info", "http://localhost:8080/info"), + Endpoint.of("health", "http://localhost:8080/health")); + } + } + + @Nested + class InstanceRegistrationUpdatedEventTest { + + static final String INSTANCE_REGISTRATION_UPDATED_EVENT_JSON = + "{\"instance\":\"test123\",\"version\":12345678,\"timestamp\":1587751031.000000000,\"registration\":{" + + "\"name\":\"test\"," + + "\"managementUrl\":\"http://localhost:8080/management\"," + + "\"healthUrl\":\"http://localhost:8080/health\"," + + "\"serviceUrl\":\"http://localhost:8080/servie\"," + + "\"source\":\"dummy-source\"" + + ",\"metadata\":{\"PASSWORD\":\"******\",\"user\":\"humptydumpty\"}}," + + "\"type\":\"REGISTRATION_UPDATED\"}"; + + @Test + public void verifySerialize() throws JsonProcessingException { + InstanceId id = InstanceId.of("test123"); + Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); + Registration registration = Registration.create("test", "http://localhost:8080/health") + .managementUrl("http://localhost:8080/management") + .serviceUrl("http://localhost:8080/servie") + .source("dummy-source") + .metadata("PASSWORD", "qwertz123") + .metadata("user", "humptydumpty") + .build(); + + InstanceRegistrationUpdatedEvent event = new InstanceRegistrationUpdatedEvent(id, 12345678L, timestamp, registration); + String json = objectMapper.writeValueAsString(event); + assertThat(json).isEqualTo(INSTANCE_REGISTRATION_UPDATED_EVENT_JSON); + } + + @Test + public void verifyDeserialize() throws JsonProcessingException { + InstanceRegistrationUpdatedEvent event = objectMapper + .readValue(INSTANCE_REGISTRATION_UPDATED_EVENT_JSON, InstanceRegistrationUpdatedEvent.class); + assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); + assertThat(event.getVersion()).isEqualTo(12345678L); + assertThat(event.getTimestamp()) + .isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); + Registration registration = event.getRegistration(); + } + } + + +} diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializerTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializerTest.java index 43245b55a3a..fbe3bf35da9 100644 --- a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializerTest.java +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializerTest.java @@ -33,10 +33,7 @@ public class RegistrationDeserializerTest { private final ObjectMapper objectMapper; public RegistrationDeserializerTest() { - SimpleModule module = new SimpleModule(); - module.addDeserializer(Registration.class, new RegistrationDeserializer()); - module.setSerializerModifier( - new RegistrationBeanSerializerModifier(new SanitizingMapSerializer(new String[] { ".*password$" }))); + SpringBootAdminModule module = new SpringBootAdminModule(new String[] { ".*password$" }); objectMapper = Jackson2ObjectMapperBuilder.json().modules(module).build(); }