From becbc00a4d4d093d273adae17026dacf3a5aaebf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Mar 2015 17:40:19 +0100 Subject: [PATCH] Use configured ObjectMapper, if available, in all EndpointMBeans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, every EndpointMBean used its own ObjectMapper. Each of these ObjectMappers was created using new ObjectMapper() with no opportunity for configuration. This commit uses the ObjectMapper from the application context and shares it among all EndpointMBeans. This gives the user control over the ObjectMapper’s configuration using spring.jackson.* properties, their own Jackson2ObjectMapperBuilder bean, etc. In the absence of an ObjectMapper in the application context a single ObjectMapper is instantiated and is used by all EndpointMBeans instead. To allow the ObjectMapper to be shared, a number of constructors have been overloaded to also take the ObjectMapper as a parameter. In these cases the old constructor has been preserved for backwards compatibility but has been deprecated. Closes gh-2393 --- .../EndpointMBeanExportAutoConfiguration.java | 10 ++- .../endpoint/jmx/DataEndpointMBean.java | 11 ++- .../actuate/endpoint/jmx/EndpointMBean.java | 12 +++- .../endpoint/jmx/EndpointMBeanExporter.java | 17 +++-- .../endpoint/jmx/ShutdownEndpointMBean.java | 11 ++- .../jmx/EndpointMBeanExporterTests.java | 69 ++++++++++++++++++- 6 files changed, 119 insertions(+), 11 deletions(-) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java index c81900cc9df..4e08ed8729d 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java @@ -31,11 +31,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * {@link EnableAutoConfiguration Auto-configuration} to enable JMX export for * {@link Endpoint}s. * * @author Christian Dupuis + * @author Andy Wilkinson */ @Configuration @ConditionalOnExpression("${endpoints.jmx.enabled:true} && ${spring.jmx.enabled:true}") @@ -44,11 +47,14 @@ import org.springframework.util.StringUtils; public class EndpointMBeanExportAutoConfiguration { @Autowired - EndpointMBeanExportProperties properties = new EndpointMBeanExportProperties(); + private EndpointMBeanExportProperties properties = new EndpointMBeanExportProperties(); + + @Autowired(required = false) + private ObjectMapper objectMapper; @Bean public EndpointMBeanExporter endpointMBeanExporter(MBeanServer server) { - EndpointMBeanExporter mbeanExporter = new EndpointMBeanExporter(); + EndpointMBeanExporter mbeanExporter = new EndpointMBeanExporter(this.objectMapper); String domain = this.properties.getDomain(); if (StringUtils.hasText(domain)) { mbeanExporter.setDomain(domain); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean.java index 097c0b59139..9e769a25f9a 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2015 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. @@ -20,19 +20,28 @@ import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Simple wrapper around {@link Endpoint} implementations that provide actuator data of * some sort. * * @author Christian Dupuis + * @author Andy Wilkinson */ @ManagedResource public class DataEndpointMBean extends EndpointMBean { + @Deprecated public DataEndpointMBean(String beanName, Endpoint endpoint) { super(beanName, endpoint); } + public DataEndpointMBean(String beanName, Endpoint endpoint, + ObjectMapper objectMapper) { + super(beanName, endpoint, objectMapper); + } + @ManagedAttribute(description = "Invoke the underlying endpoint") public Object getData() { return convert(getEndpoint().invoke()); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index 92b9df933d7..064b9f41643 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2015 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. @@ -31,18 +31,26 @@ import com.fasterxml.jackson.databind.ObjectMapper; * Simple wrapper around {@link Endpoint} implementations to enable JMX export. * * @author Christian Dupuis + * @author Andy Wilkinson */ @ManagedResource public class EndpointMBean { private final Endpoint endpoint; - private final ObjectMapper mapper = new ObjectMapper(); + private final ObjectMapper mapper; + @Deprecated public EndpointMBean(String beanName, Endpoint endpoint) { + this(beanName, endpoint, new ObjectMapper()); + } + + public EndpointMBean(String beanName, Endpoint endpoint, ObjectMapper objectMapper) { Assert.notNull(beanName, "BeanName must not be null"); Assert.notNull(endpoint, "Endpoint must not be null"); + Assert.notNull(objectMapper, "ObjectMapper must not be null"); this.endpoint = endpoint; + this.mapper = objectMapper; } @ManagedAttribute(description = "Returns the class of the underlying endpoint") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java index cdcceca3af3..c602ec2d1b5 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -49,12 +49,15 @@ import org.springframework.jmx.export.naming.SelfNaming; import org.springframework.jmx.support.ObjectNameManager; import org.springframework.util.ObjectUtils; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * {@link ApplicationListener} that registers all known {@link Endpoint}s with an * {@link MBeanServer} using the {@link MBeanExporter} located from the application * context. * * @author Christian Dupuis + * @author Andy Wilkinson */ public class EndpointMBeanExporter extends MBeanExporter implements SmartLifecycle, BeanFactoryAware, ApplicationContextAware { @@ -91,8 +94,14 @@ public class EndpointMBeanExporter extends MBeanExporter implements SmartLifecyc private Properties objectNameStaticProperties = new Properties(); + private final ObjectMapper objectMapper; + public EndpointMBeanExporter() { - super(); + this(null); + } + + public EndpointMBeanExporter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; setAutodetect(false); setNamingStrategy(this.defaultNamingStrategy); setAssembler(this.assembler); @@ -168,9 +177,9 @@ public class EndpointMBeanExporter extends MBeanExporter implements SmartLifecyc protected EndpointMBean getEndpointMBean(String beanName, Endpoint endpoint) { if (endpoint instanceof ShutdownEndpoint) { - return new ShutdownEndpointMBean(beanName, endpoint); + return new ShutdownEndpointMBean(beanName, endpoint, this.objectMapper); } - return new DataEndpointMBean(beanName, endpoint); + return new DataEndpointMBean(beanName, endpoint, this.objectMapper); } @Override diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ShutdownEndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ShutdownEndpointMBean.java index 02ba38761cd..854df90ba57 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ShutdownEndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ShutdownEndpointMBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2015 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. @@ -21,18 +21,27 @@ import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Special endpoint wrapper for {@link ShutdownEndpoint}. * * @author Christian Dupuis + * @author Andy Wilkinson */ @ManagedResource public class ShutdownEndpointMBean extends EndpointMBean { + @Deprecated public ShutdownEndpointMBean(String beanName, Endpoint endpoint) { super(beanName, endpoint); } + public ShutdownEndpointMBean(String beanName, Endpoint endpoint, + ObjectMapper mapper) { + super(beanName, endpoint, mapper); + } + @ManagedOperation(description = "Shutdown the ApplicationContext") public Object shutdown() { return convert(getEndpoint().invoke()); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java index 88eec13e078..3b546bb51c6 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2015 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. @@ -16,8 +16,11 @@ package org.springframework.boot.actuate.endpoint.jmx; +import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; @@ -28,6 +31,7 @@ import javax.management.ObjectName; import org.junit.After; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.context.ApplicationContext; @@ -36,13 +40,19 @@ import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.support.ObjectNameManager; import org.springframework.util.ObjectUtils; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; /** * Tests for {@link EndpointMBeanExporter} * * @author Christian Dupuis + * @author Andy Wilkinson */ public class EndpointMBeanExporterTests { @@ -176,6 +186,47 @@ public class EndpointMBeanExporterTests { parent.close(); } + @Test + public void jsonConversionWithDefaultObjectMapper() throws Exception { + this.context = new GenericApplicationContext(); + this.context.registerBeanDefinition("endpointMbeanExporter", + new RootBeanDefinition(EndpointMBeanExporter.class)); + this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition( + JsonConversionEndpoint.class)); + this.context.refresh(); + + MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class); + Object response = mbeanExporter.getServer().invoke( + getObjectName("endpoint1", this.context), "getData", new Object[0], + new String[0]); + + assertThat(response, is(instanceOf(Map.class))); + assertThat(((Map) response).get("date"), is(instanceOf(Long.class))); + } + + @Test + public void jsonConversionWithCustomObjectMapper() throws Exception { + this.context = new GenericApplicationContext(); + ConstructorArgumentValues constructorArgs = new ConstructorArgumentValues(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + constructorArgs.addIndexedArgumentValue(0, objectMapper); + this.context + .registerBeanDefinition("endpointMbeanExporter", new RootBeanDefinition( + EndpointMBeanExporter.class, constructorArgs, null)); + this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition( + JsonConversionEndpoint.class)); + this.context.refresh(); + + MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class); + Object response = mbeanExporter.getServer().invoke( + getObjectName("endpoint1", this.context), "getData", new Object[0], + new String[0]); + + assertThat(response, is(instanceOf(Map.class))); + assertThat(((Map) response).get("date"), is(instanceOf(String.class))); + } + private ObjectName getObjectName(String beanKey, GenericApplicationContext context) throws MalformedObjectNameException { return getObjectName("org.springframework.boot", beanKey, false, context); @@ -209,4 +260,20 @@ public class EndpointMBeanExporterTests { } } + public static class JsonConversionEndpoint extends + AbstractEndpoint> { + + public JsonConversionEndpoint() { + super("json-conversion"); + } + + @Override + public Map invoke() { + Map result = new LinkedHashMap(); + result.put("date", new Date()); + return result; + } + + } + }