Use configured ObjectMapper, if available, in all EndpointMBeans

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
pull/2252/merge
Andy Wilkinson 10 years ago
parent 8850286937
commit becbc00a4d

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

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

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

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

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

@ -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<Map<String, Object>> {
public JsonConversionEndpoint() {
super("json-conversion");
}
@Override
public Map<String, Object> invoke() {
Map<String, Object> result = new LinkedHashMap<String, Object>();
result.put("date", new Date());
return result;
}
}
}

Loading…
Cancel
Save