Improve MBean without backing Endpoint support

Improve support for MBeans without a backing endpoint by introducing
a `JmxEndpoint` interface. The `JmxEndpoint` is intentionally
similar in design to the `MvcEndpoint` from the `mvc` package and
allows for completely custom JMX beans that are not backed by any
real actuator `Endpoint`.

The `AuditEventsMBean` has been refactored to use the new interface and
has been renamed to `AuditEventsJmxEndpoint`.

See gh-6579
This commit is contained in:
Phillip Webb 2017-01-02 19:28:49 -08:00
parent 2f1e4f0c02
commit 5b40eb48e0
11 changed files with 286 additions and 163 deletions

View File

@ -25,7 +25,7 @@ import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.autoconfigure.EndpointMBeanExportAutoConfiguration.JmxEnabledCondition;
import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.jmx.AuditEventsMBean;
import org.springframework.boot.actuate.endpoint.jmx.AuditEventsJmxEndpoint;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -90,9 +90,9 @@ public class EndpointMBeanExportAutoConfiguration {
@Bean
@ConditionalOnBean(AuditEventRepository.class)
@ConditionalOnEnabledEndpoint("auditevents")
public AuditEventsMBean abstractEndpointMBean(
public AuditEventsJmxEndpoint abstractEndpointMBean(
AuditEventRepository auditEventRepository) {
return new AuditEventsMBean(this.objectMapper, auditEventRepository);
return new AuditEventsJmxEndpoint(this.objectMapper, auditEventRepository);
}
/**

View File

@ -22,16 +22,21 @@ import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.EndpointProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.ObjectUtils;
/**
* Abstract base class for JMX endpoint implementations without a backing
* Abstract base class for {@link JmxEndpoint} implementations without a backing
* {@link Endpoint}.
*
* @author Vedran Pavic
* @author Phillip Webb
* @since 1.5.0
*/
public abstract class AbstractEndpointMBean extends EndpointMBeanSupport
implements EnvironmentAware {
@ManagedResource
public abstract class AbstractJmxEndpoint implements JmxEndpoint, EnvironmentAware {
private final DataConverter dataConverter;
private Environment environment;
@ -40,23 +45,8 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport
*/
private Boolean enabled;
/**
* Mark if the endpoint exposes sensitive information.
*/
private Boolean sensitive;
private final boolean sensitiveDefault;
public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive) {
super(objectMapper);
this.sensitiveDefault = sensitive;
}
public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive,
boolean enabled) {
super(objectMapper);
this.sensitiveDefault = sensitive;
this.enabled = enabled;
public AbstractJmxEndpoint(ObjectMapper objectMapper) {
this.dataConverter = new DataConverter(objectMapper);
}
@Override
@ -68,6 +58,7 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport
return this.environment;
}
@Override
public boolean isEnabled() {
return EndpointProperties.isEnabled(this.environment, this.enabled);
}
@ -77,18 +68,23 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport
}
@Override
public boolean isSensitive() {
return EndpointProperties.isSensitive(this.environment, this.sensitive,
this.sensitiveDefault);
}
public void setSensitive(Boolean sensitive) {
this.sensitive = sensitive;
public String getIdentity() {
return ObjectUtils.getIdentityHexString(this);
}
@Override
public String getEndpointClass() {
@SuppressWarnings("rawtypes")
public Class<? extends Endpoint> getEndpointType() {
return null;
}
/**
* Convert the given data into JSON.
* @param data the source data
* @return the JSON representation
*/
protected Object convert(Object data) {
return this.dataConverter.convert(data);
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -28,57 +27,55 @@ import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.Assert;
/**
* Special JMX endpoint wrapper for {@link AuditEventRepository}.
* {@link JmxEndpoint} for {@link AuditEventRepository}.
*
* @author Vedran Pavic
* @since 1.5.0
*/
@ManagedResource
@ConfigurationProperties(prefix = "endpoints.auditevents")
public class AuditEventsMBean extends AbstractEndpointMBean {
public class AuditEventsJmxEndpoint extends AbstractJmxEndpoint {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
private final AuditEventRepository auditEventRepository;
public AuditEventsMBean(ObjectMapper objectMapper,
public AuditEventsJmxEndpoint(ObjectMapper objectMapper,
AuditEventRepository auditEventRepository) {
super(objectMapper, true);
super(objectMapper);
Assert.notNull(auditEventRepository, "AuditEventRepository must not be null");
this.auditEventRepository = auditEventRepository;
}
@ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria")
public Object getData(String dateAfter) {
List<AuditEvent> auditEvents = this.auditEventRepository.find(
parseDate(dateAfter));
List<AuditEvent> auditEvents = this.auditEventRepository
.find(parseDate(dateAfter));
return convert(auditEvents);
}
@ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria")
public Object getData(String dateAfter, String principal) {
List<AuditEvent> auditEvents = this.auditEventRepository.find(
principal, parseDate(dateAfter));
List<AuditEvent> auditEvents = this.auditEventRepository.find(principal,
parseDate(dateAfter));
return convert(auditEvents);
}
@ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria")
public Object getData(String principal, String dateAfter, String type) {
List<AuditEvent> auditEvents = this.auditEventRepository.find(
principal, parseDate(dateAfter), type);
List<AuditEvent> auditEvents = this.auditEventRepository.find(principal,
parseDate(dateAfter), type);
return convert(auditEvents);
}
private Date parseDate(String date) {
try {
return dateFormat.parse(date);
return new SimpleDateFormat(DATE_FORMAT).parse(date);
}
catch (ParseException e) {
throw new IllegalArgumentException(e);
catch (ParseException ex) {
throw new IllegalArgumentException(ex);
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Internal converter that uses an {@link ObjectMapper} to convert to JSON.
*
* @author Christian Dupuis
* @author Andy Wilkinson
* @author Phillip Webb
*/
class DataConverter {
private final ObjectMapper objectMapper;
private final JavaType listObject;
private final JavaType mapStringObject;
DataConverter(ObjectMapper objectMapper) {
this.objectMapper = (objectMapper == null ? new ObjectMapper() : objectMapper);
this.listObject = this.objectMapper.getTypeFactory()
.constructParametricType(List.class, Object.class);
this.mapStringObject = this.objectMapper.getTypeFactory()
.constructParametricType(Map.class, String.class, Object.class);
}
public Object convert(Object data) {
if (data == null) {
return null;
}
if (data instanceof String) {
return data;
}
if (data.getClass().isArray() || data instanceof List) {
return this.objectMapper.convertValue(data, this.listObject);
}
return this.objectMapper.convertValue(data, this.mapStringObject);
}
}

View File

@ -23,16 +23,22 @@ import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Simple wrapper around {@link Endpoint} implementations to enable JMX export.
* Base for adapters that convert {@link Endpoint} implementations to {@link JmxEndpoint}.
*
* @author Christian Dupuis
* @author Andy Wilkinson
* @author Vedran Pavic
* @author Phillip Webb
* @see JmxEndpoint
* @see DataEndpointMBean
*/
@ManagedResource
public class EndpointMBean extends EndpointMBeanSupport {
public abstract class EndpointMBean implements JmxEndpoint {
private final DataConverter dataConverter;
private final Endpoint<?> endpoint;
@ -44,7 +50,7 @@ public class EndpointMBean extends EndpointMBeanSupport {
*/
public EndpointMBean(String beanName, Endpoint<?> endpoint,
ObjectMapper objectMapper) {
super(objectMapper);
this.dataConverter = new DataConverter(objectMapper);
Assert.notNull(beanName, "BeanName must not be null");
Assert.notNull(endpoint, "Endpoint must not be null");
this.endpoint = endpoint;
@ -52,7 +58,12 @@ public class EndpointMBean extends EndpointMBeanSupport {
@ManagedAttribute(description = "Returns the class of the underlying endpoint")
public String getEndpointClass() {
return ClassUtils.getQualifiedName(this.endpoint.getClass());
return ClassUtils.getQualifiedName(getEndpointType());
}
@Override
public boolean isEnabled() {
return this.endpoint.isEnabled();
}
@ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information")
@ -60,8 +71,28 @@ public class EndpointMBean extends EndpointMBeanSupport {
return this.endpoint.isSensitive();
}
@Override
public String getIdentity() {
return ObjectUtils.getIdentityHexString(getEndpoint());
}
@Override
@SuppressWarnings("rawtypes")
public Class<? extends Endpoint> getEndpointType() {
return getEndpoint().getClass();
}
public Endpoint<?> getEndpoint() {
return this.endpoint;
}
/**
* Convert the given data into JSON.
* @param data the source data
* @return the JSON representation
*/
protected Object convert(Object data) {
return this.dataConverter.convert(data);
}
}

View File

@ -39,7 +39,6 @@ import org.springframework.boot.actuate.endpoint.LoggersEndpoint;
import org.springframework.boot.actuate.endpoint.ShutdownEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.jmx.export.MBeanExportException;
@ -53,7 +52,7 @@ import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;
/**
* {@link ApplicationListener} that registers all known {@link Endpoint}s with an
* {@link SmartLifecycle} bean that registers all known {@link Endpoint}s with an
* {@link MBeanServer} using the {@link MBeanExporter} located from the application
* context.
*
@ -79,7 +78,7 @@ public class EndpointMBeanExporter extends MBeanExporter
private final MetadataNamingStrategy defaultNamingStrategy = new MetadataNamingStrategy(
this.attributeSource);
private final Set<Endpoint<?>> registeredEndpoints = new HashSet<Endpoint<?>>();
private final Set<Class<?>> registeredEndpoints = new HashSet<Class<?>>();
private volatile boolean autoStartup = true;
@ -156,39 +155,88 @@ public class EndpointMBeanExporter extends MBeanExporter
locateAndRegisterEndpoints();
}
@SuppressWarnings({ "rawtypes" })
protected void locateAndRegisterEndpoints() {
Map<String, Endpoint> endpoints = this.beanFactory.getBeansOfType(Endpoint.class);
for (Map.Entry<String, Endpoint> endpointEntry : endpoints.entrySet()) {
if (!this.registeredEndpoints.contains(endpointEntry.getValue())
&& endpointEntry.getValue().isEnabled()) {
registerEndpoint(endpointEntry.getKey(), endpointEntry.getValue());
this.registeredEndpoints.add(endpointEntry.getValue());
registerJmxEndpoints(this.beanFactory.getBeansOfType(JmxEndpoint.class));
registerEndpoints(this.beanFactory.getBeansOfType(Endpoint.class));
}
private void registerJmxEndpoints(Map<String, JmxEndpoint> endpoints) {
for (Map.Entry<String, JmxEndpoint> entry : endpoints.entrySet()) {
String name = entry.getKey();
JmxEndpoint endpoint = entry.getValue();
Class<?> type = (endpoint.getEndpointType() != null
? endpoint.getEndpointType() : endpoint.getClass());
if (!this.registeredEndpoints.contains(type) && endpoint.isEnabled()) {
try {
registerBeanNameOrInstance(endpoint, name);
}
catch (MBeanExportException ex) {
logger.error("Could not register JmxEndpoint [" + name + "]", ex);
}
this.registeredEndpoints.add(type);
}
}
}
@SuppressWarnings("rawtypes")
private void registerEndpoints(Map<String, Endpoint> endpoints) {
for (Map.Entry<String, Endpoint> entry : endpoints.entrySet()) {
String name = entry.getKey();
Endpoint endpoint = entry.getValue();
Class<?> type = endpoint.getClass();
if (!this.registeredEndpoints.contains(type) && endpoint.isEnabled()) {
registerEndpoint(name, endpoint);
this.registeredEndpoints.add(type);
}
}
}
/**
* Register a regular {@link Endpoint} with the {@link MBeanServer}.
* @param beanName the bean name
* @param endpoint the endpoint to register
* @deprecated as of 1.5 in favor of direct {@link JmxEndpoint} registration or
* {@link #adaptEndpoint(String, Endpoint)}
*/
@Deprecated
protected void registerEndpoint(String beanName, Endpoint<?> endpoint) {
@SuppressWarnings("rawtypes")
Class<? extends Endpoint> type = endpoint.getClass();
if (AnnotationUtils.findAnnotation(type, ManagedResource.class) != null) {
// Already managed
return;
}
if (type.isMemberClass()
&& AnnotationUtils.findAnnotation(type.getEnclosingClass(),
ManagedResource.class) != null) {
// Nested class with @ManagedResource in parent
Class<?> type = endpoint.getClass();
if (isAnnotatedWithManagedResource(type) || (type.isMemberClass()
&& isAnnotatedWithManagedResource(type.getEnclosingClass()))) {
// Endpoint is directly managed
return;
}
JmxEndpoint jmxEndpoint = adaptEndpoint(beanName, endpoint);
try {
registerBeanNameOrInstance(getEndpointMBean(beanName, endpoint), beanName);
registerBeanNameOrInstance(jmxEndpoint, beanName);
}
catch (MBeanExportException ex) {
logger.error("Could not register MBean for endpoint [" + beanName + "]", ex);
}
}
private boolean isAnnotatedWithManagedResource(Class<?> type) {
return AnnotationUtils.findAnnotation(type, ManagedResource.class) != null;
}
/**
* Adapt the given {@link Endpoint} to a {@link JmxEndpoint}.
* @param beanName the bean name
* @param endpoint the endpoint to adapt
* @return an adapted endpoint
*/
protected JmxEndpoint adaptEndpoint(String beanName, Endpoint<?> endpoint) {
return getEndpointMBean(beanName, endpoint);
}
/**
* Get a {@link EndpointMBean} for the specified {@link Endpoint}.
* @param beanName the bean name
* @param endpoint the endpoint
* @return an {@link EndpointMBean}
* @deprecated as of 1.5 in favor of {@link #adaptEndpoint(String, Endpoint)}
*/
@Deprecated
protected EndpointMBean getEndpointMBean(String beanName, Endpoint<?> endpoint) {
if (endpoint instanceof ShutdownEndpoint) {
return new ShutdownEndpointMBean(beanName, endpoint, this.objectMapper);
@ -205,27 +253,29 @@ public class EndpointMBeanExporter extends MBeanExporter
if (bean instanceof SelfNaming) {
return ((SelfNaming) bean).getObjectName();
}
if (bean instanceof EndpointMBean) {
StringBuilder builder = new StringBuilder();
builder.append(this.domain);
builder.append(":type=Endpoint");
builder.append(",name=" + beanKey);
if (parentContextContainsSameBean(this.applicationContext, beanKey)) {
builder.append(",context="
+ ObjectUtils.getIdentityHexString(this.applicationContext));
}
if (this.ensureUniqueRuntimeObjectNames) {
builder.append(",identity=" + ObjectUtils
.getIdentityHexString(((EndpointMBean) bean).getEndpoint()));
}
builder.append(getStaticNames());
return ObjectNameManager.getInstance(builder.toString());
return getObjectName((EndpointMBean) bean, beanKey);
}
return this.defaultNamingStrategy.getObjectName(bean, beanKey);
}
private ObjectName getObjectName(JmxEndpoint jmxEndpoint, String beanKey)
throws MalformedObjectNameException {
StringBuilder builder = new StringBuilder();
builder.append(this.domain);
builder.append(":type=Endpoint");
builder.append(",name=" + beanKey);
if (parentContextContainsSameBean(this.applicationContext, beanKey)) {
builder.append(",context="
+ ObjectUtils.getIdentityHexString(this.applicationContext));
}
if (this.ensureUniqueRuntimeObjectNames) {
builder.append(",identity=" + jmxEndpoint.getIdentity());
}
builder.append(getStaticNames());
return ObjectNameManager.getInstance(builder.toString());
}
private boolean parentContextContainsSameBean(ApplicationContext applicationContext,
String beanKey) {
if (applicationContext.getParent() != null) {

View File

@ -1,70 +0,0 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.endpoint.jmx;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.util.Assert;
/**
* Abstract base class for JMX endpoint implementations.
*
* @author Vedran Pavic
* @since 1.5.0
*/
public abstract class EndpointMBeanSupport {
private final ObjectMapper mapper;
private final JavaType listObject;
private final JavaType mapStringObject;
public EndpointMBeanSupport(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.mapper = objectMapper;
this.listObject = objectMapper.getTypeFactory()
.constructParametricType(List.class, Object.class);
this.mapStringObject = objectMapper.getTypeFactory()
.constructParametricType(Map.class, String.class, Object.class);
}
@ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information")
public abstract boolean isSensitive();
@ManagedAttribute(description = "Returns the class of the underlying endpoint")
public abstract String getEndpointClass();
protected Object convert(Object result) {
if (result == null) {
return null;
}
if (result instanceof String) {
return result;
}
if (result.getClass().isArray() || result instanceof List) {
return this.mapper.convertValue(result, this.listObject);
}
return this.mapper.convertValue(result, this.mapStringObject);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.endpoint.jmx;
import org.springframework.boot.actuate.endpoint.Endpoint;
/**
* A strategy for the JMX layer on top of an {@link Endpoint}. Implementations are allowed
* to use {@code @ManagedAttribute} and the full Spring JMX machinery. Implementations may
* be backed by an actual {@link Endpoint} or may be specifically designed for JMX only.
*
* @author Phillip Webb
* @since 1.5.0
* @see EndpointMBean
* @see AbstractJmxEndpoint
*/
public interface JmxEndpoint {
/**
* Return if the JMX endpoint is enabled.
* @return if the endpoint is enabled
*/
boolean isEnabled();
/**
* Return the MBean identity for this endpoint.
* @return the MBean identity.
*/
String getIdentity();
/**
* Return the type of {@link Endpoint} exposed, or {@code null} if this
* {@link JmxEndpoint} exposes information that cannot be represented as a traditional
* {@link Endpoint}.
* @return the endpoint type
*/
@SuppressWarnings("rawtypes")
Class<? extends Endpoint> getEndpointType();
}

View File

@ -202,8 +202,8 @@ public class ManagementWebSecurityAutoConfigurationTests {
HttpMessageConvertersAutoConfiguration.class,
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class,
WebMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class);
WebMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
AuditAutoConfiguration.class);
this.context.refresh();
Filter filter = this.context.getBean("springSecurityFilterChain", Filter.class);

View File

@ -128,7 +128,7 @@ public class EndpointMBeanExporterTests {
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.registerBeanDefinition("endpoint2",
new RootBeanDefinition(TestEndpoint.class));
new RootBeanDefinition(TestEndpoint2.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer()
@ -329,6 +329,10 @@ public class EndpointMBeanExporterTests {
}
public static class TestEndpoint2 extends TestEndpoint {
}
public static class JsonMapConversionEndpoint
extends AbstractEndpoint<Map<String, Object>> {

View File

@ -964,7 +964,6 @@ content into your application; rather pick only the properties that you need.
endpoints.auditevents.enabled= # Enable the endpoint.
endpoints.auditevents.id= # Endpoint identifier.
endpoints.auditevents.path= # Endpoint path.
endpoints.auditevents.sensitive= # Mark if the endpoint exposes sensitive information.
endpoints.autoconfig.enabled= # Enable the endpoint.
endpoints.autoconfig.id= # Endpoint identifier.
endpoints.autoconfig.path= # Endpoint path.