INT-3124 Add JMX MBean Tree Inbound Adapter

Add an MBeanTreePollingMessageSource that produces a graph of simple objects representing the JMX tree (INT-3124).

The DefaultMBeanObjectConverter converts MBean objects into a graph of Lists, Maps and arrays or primitives.

Formatting tidy, per feedback.

Feedback incorporated: split setter/attributes, more tests, inner bean constructor, logging changes.

Overloaded setter methods with different parameter types are now named separately, the endpoint attributes reflect this and tests are added to reflect this.

An inner bean can be supplied to provide an alternative MBeanObjectConverter.

Log a warning instead of a more destructive UnsupportedOperationException where there's incomplete parsing in the DefaultMBeanObjectConverter and add a trace level for exception debugging.

Minor test change and doc/reference update

Added attribute filter interface as suggested. Three implementations are provided 'all', 'named only' and 'not named'

actually add notnamedfield filter (doh).

Polishing
This commit is contained in:
Pid
2013-08-30 12:24:33 +01:00
committed by Gary Russell
parent 35e8929f1c
commit 5212ea13ef
17 changed files with 1347 additions and 2 deletions

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013 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.integration.jmx;
import javax.management.ObjectName;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public class DefaultMBeanAttributeFilter implements MBeanAttributeFilter {
@Override
public boolean accept(ObjectName objectName, String attributeName) {
return true;
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2013 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.integration.jmx;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.RuntimeMBeanException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public class DefaultMBeanObjectConverter implements MBeanObjectConverter {
private static final Log log = LogFactory.getLog(DefaultMBeanObjectConverter.class);
private final MBeanAttributeFilter filter;
public DefaultMBeanObjectConverter() {
this(new DefaultMBeanAttributeFilter());
}
public DefaultMBeanObjectConverter(MBeanAttributeFilter filter) {
this.filter = filter;
}
@Override
public Object convert(MBeanServerConnection connection, ObjectInstance instance) {
Map<String, Object> attributeMap = new HashMap<String, Object>();
try {
ObjectName objName = instance.getObjectName();
if (!connection.isRegistered(objName)) {
return attributeMap;
}
MBeanInfo info = connection.getMBeanInfo(objName);
MBeanAttributeInfo[] attributeInfos = info.getAttributes();
for (MBeanAttributeInfo attrInfo : attributeInfos) {
// we don't need to repeat name of this as an attribute
if ("ObjectName".equals(attrInfo.getName()) || !filter.accept(objName, attrInfo.getName())) {
continue;
}
Object value;
try {
value = connection.getAttribute(objName, attrInfo.getName());
}
catch (RuntimeMBeanException e) {
// N.B. standard MemoryUsage MBeans will throw an exception when some
// measurement is unsupported. Logging at trace rather than debug to
// avoid confusion.
if (log.isTraceEnabled()) {
log.trace("Error getting attribute '" + attrInfo.getName() + "' on '" + objName + "'", e);
}
// try to unwrap the exception somewhat; not sure this is ideal
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
value = String.format("%s[%s]", t.getClass().getName(), t.getMessage());
}
attributeMap.put(attrInfo.getName(), checkAndConvert(value));
}
}
catch (Exception e) {
throw new IllegalArgumentException(e);
}
return attributeMap;
}
/**
* @param input
* @return recursively mapped object
*/
private Object checkAndConvert(Object input) {
if (input == null) {
return input;
}
else if (input.getClass().isArray()) {
if (CompositeData.class.isAssignableFrom(input.getClass().getComponentType())) {
List<Object> converted = new ArrayList<Object>();
int length = Array.getLength(input);
for (int i = 0; i < length; i++) {
Object value = checkAndConvert(Array.get(input, i));
converted.add(value);
}
return converted;
}
if (TabularData.class.isAssignableFrom(input.getClass().getComponentType())) {
// TODO haven't hit this yet, but expect to
log.warn("TabularData.isAssignableFrom(getComponentType) for " + input.toString());
}
}
else if (input instanceof CompositeData) {
CompositeData data = (CompositeData) input;
if (data.getCompositeType().isArray()) {
// TODO? I haven't found an example where this gets thrown - but need to test it on Tomcat/Jetty or
// something
log.warn("(data.getCompositeType().isArray for " + input.toString());
}
else {
Map<String, Object> returnable = new HashMap<String, Object>();
Set<String> keys = data.getCompositeType().keySet();
for (String key : keys) {
// we don't need to repeat name of this as an attribute
if ("ObjectName".equals(key)) {
continue;
}
Object value = checkAndConvert(data.get(key));
returnable.put(key, value);
}
return returnable;
}
}
else if (input instanceof TabularData) {
TabularData data = (TabularData) input;
if (data.getTabularType().isArray()) {
// TODO? I haven't found an example where this gets thrown, so might not be required
log.warn("TabularData.isArray for " + input.toString());
}
else {
Map<Object, Object> returnable = new HashMap<Object, Object>();
@SuppressWarnings("unchecked")
Set<List<?>> keySet = (Set<List<?>>) data.keySet();
for (List<?> keys : keySet) {
CompositeData cd = data.get(keys.toArray());
Object value = checkAndConvert(cd);
if (keys.size() == 1 && (value instanceof Map) && ((Map<?, ?>) value).size() == 2) {
Object actualKey = keys.get(0);
Map<?, ?> valueMap = (Map<?, ?>) value;
if (valueMap.containsKey("key") && valueMap.containsKey("value")
&& actualKey.equals(valueMap.get("key"))) {
returnable.put(valueMap.get("key"), valueMap.get("value"));
}
else {
returnable.put(actualKey, value);
}
}
else {
returnable.put(keys, value);
}
}
return returnable;
}
}
return input;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013 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.integration.jmx;
import javax.management.ObjectName;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public interface MBeanAttributeFilter {
/**
* @param objectName
* @param attributeName
* @return outcome of test
*/
boolean accept(ObjectName objectName, String attributeName);
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013 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.integration.jmx;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public interface MBeanObjectConverter {
/**
* @param connection
* @param instance
* @return mapped object instance
*/
Object convert(MBeanServerConnection connection, ObjectInstance instance);
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2013 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.integration.jmx;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.QueryExp;
import org.springframework.integration.MessagingException;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.AbstractMessageSource;
import org.springframework.util.Assert;
/**
* A {@link MessageSource} implementation that retrieves a snapshot of a filtered subset of the MBean tree.
*
* @author Stuart Williams
* @since 3.0
*
*/
public class MBeanTreePollingMessageSource extends AbstractMessageSource<Object> {
private volatile MBeanServerConnection server;
private volatile ObjectName queryName = null;
private volatile QueryExp queryExpression = ObjectName.WILDCARD;
private final MBeanObjectConverter converter;
/**
* @param converter
*/
public MBeanTreePollingMessageSource(MBeanObjectConverter converter) {
this.converter = converter;
}
/**
* Provides the mapped tree object
*/
@Override
protected Object doReceive() {
Assert.notNull(this.server, "MBeanServer is required");
try {
Map<String, Object> beans = new HashMap<String, Object>();
Set<ObjectInstance> results = server.queryMBeans(queryName, queryExpression);
for (ObjectInstance instance : results) {
Object result = converter.convert(server, instance);
beans.put(instance.getObjectName().getCanonicalName(), result);
}
return beans;
}
catch (Exception e) {
throw new MessagingException("Failed to retrieve tree snapshot", e);
}
}
/**
* Provide the MBeanServer where the JMX MBean has been registered.
*/
public void setServer(MBeanServerConnection server) {
this.server = server;
}
/**
* @param queryName
*/
public void setQueryName(String queryName) {
Assert.notNull(queryName, "'queryName' must not be null");
try {
setQueryNameReference(ObjectName.getInstance(queryName));
}
catch (MalformedObjectNameException e) {
throw new IllegalArgumentException(e);
}
}
/**
* @param queryName
*/
public void setQueryNameReference(ObjectName queryName) {
this.queryName = queryName;
}
/**
* @param queryExpression
*/
public void setQueryExpression(String queryExpression) {
Assert.notNull(queryExpression, "'queryExpression' must not be null");
try {
setQueryExpressionReference(ObjectName.getInstance(queryExpression));
}
catch (MalformedObjectNameException e) {
throw new IllegalArgumentException(e);
}
}
/**
* @param queryExpression
*/
public void setQueryExpressionReference(QueryExp queryExpression) {
this.queryExpression = queryExpression;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013 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.integration.jmx;
import java.util.Arrays;
import javax.management.ObjectName;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public class NamedFieldsMBeanAttributeFilter implements MBeanAttributeFilter {
private final String[] namedFields;
/**
* @param namedFields
*/
public NamedFieldsMBeanAttributeFilter(String... namedFields) {
this.namedFields = (String[]) Arrays.asList(namedFields).toArray();
}
@Override
public boolean accept(ObjectName objectName, String attributeName) {
for (String namedField : namedFields) {
if (namedField.equals(attributeName)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013 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.integration.jmx;
import java.util.Arrays;
import javax.management.ObjectName;
/**
* @author Stuart Williams
* @since 3.0
*
*/
public class NotNamedFieldsMBeanAttributeFilter implements MBeanAttributeFilter {
private final String[] namedFields;
/**
* @param namedFields
*/
public NotNamedFieldsMBeanAttributeFilter(String... namedFields) {
this.namedFields = (String[]) Arrays.asList(namedFields).toArray();
}
@Override
public boolean accept(ObjectName objectName, String attributeName) {
for (String namedField : namedFields) {
if (namedField.equals(attributeName)) {
return false;
}
}
return true;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2013 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,10 +20,11 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa
/**
* Namespace handler for Spring Integration's <em>jmx</em> namespace.
*
*
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Dave Syer
* @author Stuart Williams
* @since 2.0
*/
public class JmxNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@@ -32,6 +33,7 @@ public class JmxNamespaceHandler extends AbstractIntegrationNamespaceHandler {
this.registerBeanDefinitionParser("operation-invoking-channel-adapter", new OperationInvokingChannelAdapterParser());
this.registerBeanDefinitionParser("operation-invoking-outbound-gateway", new OperationInvokingOutboundGatewayParser());
this.registerBeanDefinitionParser("attribute-polling-channel-adapter", new AttributePollingChannelAdapterParser());
this.registerBeanDefinitionParser("tree-polling-channel-adapter", new MBeanTreePollingChannelAdapterParser());
this.registerBeanDefinitionParser("notification-listening-channel-adapter", new NotificationListeningChannelAdapterParser());
this.registerBeanDefinitionParser("notification-publishing-channel-adapter", new NotificationPublishingChannelAdapterParser());
this.registerBeanDefinitionParser("mbean-export", new MBeanExporterParser());

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2013 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.integration.jmx.config;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.jmx.DefaultMBeanObjectConverter;
import org.springframework.integration.jmx.MBeanTreePollingMessageSource;
import org.springframework.util.StringUtils;
/**
* @author Stuart Williams
* @author Gary Russell
* @since 3.0
*
*/
public class MBeanTreePollingChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser {
@Override
protected boolean shouldGenerateIdAsFallback() {
return true;
}
@Override
protected BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(
MBeanTreePollingMessageSource.class);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "server", "server");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "query-name-ref", "queryNameReference");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "query-expression-ref", "queryExpressionReference");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "query-name", "queryName");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "query-expression", "queryExpression");
BeanComponentDefinition innerBeanDef = IntegrationNamespaceUtils.parseInnerHandlerDefinition(element, parserContext);
String beanName = element.getAttribute("converter");
if (innerBeanDef != null) {
if (StringUtils.hasText(beanName)) {
parserContext.getReaderContext().error("Cannot have both a 'converter' and an inner bean", element);
}
beanName = BeanDefinitionReaderUtils.generateBeanName(innerBeanDef.getBeanDefinition(), parserContext.getRegistry(), true);
parserContext.getRegistry().registerBeanDefinition(beanName, innerBeanDef.getBeanDefinition());
}
else if (!StringUtils.hasText(beanName)) {
BeanDefinitionBuilder childBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultMBeanObjectConverter.class);
beanName = BeanDefinitionReaderUtils.generateBeanName(childBuilder.getBeanDefinition(), parserContext.getRegistry(), true);
parserContext.getRegistry().registerBeanDefinition(beanName, childBuilder.getBeanDefinition());
}
builder.addConstructorArgReference(beanName);
return builder.getBeanDefinition();
}
}