/*
* Copyright 2012-2017 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.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.actuate.endpoint.Endpoint;
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.SmartLifecycle;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.jmx.export.MBeanExportException;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler;
import org.springframework.jmx.export.metadata.InvalidMetadataException;
import org.springframework.jmx.export.metadata.JmxAttributeSource;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;
import org.springframework.jmx.export.naming.SelfNaming;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* {@link SmartLifecycle} bean 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
* @author Vedran Pavic
*/
public class EndpointMBeanExporter extends MBeanExporter
implements SmartLifecycle, ApplicationContextAware {
/**
* The default JMX domain.
*/
public static final String DEFAULT_DOMAIN = "org.springframework.boot";
private static final Log logger = LogFactory.getLog(EndpointMBeanExporter.class);
private final AnnotationJmxAttributeSource attributeSource = new EndpointJmxAttributeSource();
private final MetadataMBeanInfoAssembler assembler = new MetadataMBeanInfoAssembler(
this.attributeSource);
private final MetadataNamingStrategy defaultNamingStrategy = new MetadataNamingStrategy(
this.attributeSource);
private final Set<Class<?>> registeredEndpoints = new HashSet<>();
private volatile boolean autoStartup = true;
private volatile int phase = 0;
private volatile boolean running = false;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private ApplicationContext applicationContext;
private ListableBeanFactory beanFactory;
private String domain = DEFAULT_DOMAIN;
private boolean ensureUniqueRuntimeObjectNames = false;
private Properties objectNameStaticProperties = new Properties();
private final ObjectMapper objectMapper;
/**
* Create a new {@link EndpointMBeanExporter} instance.
*/
public EndpointMBeanExporter() {
this(null);
}
/**
* Create a new {@link EndpointMBeanExporter} instance.
* @param objectMapper the object mapper
*/
public EndpointMBeanExporter(ObjectMapper objectMapper) {
this.objectMapper = (objectMapper == null ? new ObjectMapper() : objectMapper);
setAutodetect(false);
setNamingStrategy(this.defaultNamingStrategy);
setAssembler(this.assembler);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
if (beanFactory instanceof ListableBeanFactory) {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
else {
logger.warn("EndpointMBeanExporter not running in a ListableBeanFactory: "
+ "autodetection of Endpoints not available.");
}
}
public void setDomain(String domain) {
this.domain = domain;
}
@Override
public void setEnsureUniqueRuntimeObjectNames(
boolean ensureUniqueRuntimeObjectNames) {
super.setEnsureUniqueRuntimeObjectNames(ensureUniqueRuntimeObjectNames);
this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames;
}
public void setObjectNameStaticProperties(Properties objectNameStaticProperties) {
this.objectNameStaticProperties = objectNameStaticProperties;
}
protected void doStart() {
locateAndRegisterEndpoints();
}
protected void locateAndRegisterEndpoints() {
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) {
Class<?> type = endpoint.getClass();
if (isAnnotatedWithManagedResource(type) || (type.isMemberClass()
&& isAnnotatedWithManagedResource(type.getEnclosingClass()))) {
// Endpoint is directly managed
return;
}
JmxEndpoint jmxEndpoint = adaptEndpoint(beanName, endpoint);
try {
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) {
if (endpoint instanceof ShutdownEndpoint) {
return new ShutdownEndpointMBean(beanName, endpoint, this.objectMapper);
}
if (endpoint instanceof LoggersEndpoint) {
return new LoggersEndpointMBean(beanName, endpoint, this.objectMapper);
}
return new DataEndpointMBean(beanName, endpoint, this.objectMapper);
}
@Override
protected ObjectName getObjectName(Object bean, String beanKey)
throws MalformedObjectNameException {
if (bean instanceof SelfNaming) {
return ((SelfNaming) bean).getObjectName();
}
if (bean instanceof JmxEndpoint) {
return getObjectName((JmxEndpoint) 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) {
try {
Object bean = this.applicationContext.getParent().getBean(beanKey);
if (bean instanceof Endpoint || bean instanceof JmxEndpoint) {
return true;
}
}
catch (BeansException ex) {
// Ignore and continue
}
return parentContextContainsSameBean(applicationContext.getParent(), beanKey);
}
return false;
}
private String getStaticNames() {
if (this.objectNameStaticProperties.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (Entry<Object, Object> name : this.objectNameStaticProperties.entrySet()) {
builder.append("," + name.getKey() + "=" + name.getValue());
}
return builder.toString();
}
@Override
public final int getPhase() {
return this.phase;
}
@Override
public final boolean isAutoStartup() {
return this.autoStartup;
}
@Override
public final boolean isRunning() {
this.lifecycleLock.lock();
try {
return this.running;
}
finally {
this.lifecycleLock.unlock();
}
}
@Override
public final void start() {
this.lifecycleLock.lock();
try {
if (!this.running) {
this.doStart();
this.running = true;
}
}
finally {
this.lifecycleLock.unlock();
}
}
@Override
public final void stop() {
this.lifecycleLock.lock();
try {
if (this.running) {
this.running = false;
}
}
finally {
this.lifecycleLock.unlock();
}
}
@Override
public final void stop(Runnable callback) {
this.lifecycleLock.lock();
try {
this.stop();
callback.run();
}
finally {
this.lifecycleLock.unlock();
}
}
/**
* {@link JmxAttributeSource} for {@link JmxEndpoint JmxEndpoints}.
*/
private static class EndpointJmxAttributeSource extends AnnotationJmxAttributeSource {
@Override
public org.springframework.jmx.export.metadata.ManagedResource getManagedResource(
Class<?> beanClass) throws InvalidMetadataException {
Assert.state(super.getManagedResource(beanClass) == null,
"@ManagedResource annotation found on JmxEndpoint " + beanClass);
return new org.springframework.jmx.export.metadata.ManagedResource();
}
}
}