From 5212ea13ef36bb110e98acf0bf1b0b87a6659f89 Mon Sep 17 00:00:00 2001 From: Pid Date: Fri, 30 Aug 2013 12:24:33 +0100 Subject: [PATCH] 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 --- .../jmx/DefaultMBeanAttributeFilter.java | 32 +++ .../jmx/DefaultMBeanObjectConverter.java | 192 +++++++++++++++ .../integration/jmx/MBeanAttributeFilter.java | 34 +++ .../integration/jmx/MBeanObjectConverter.java | 36 +++ .../jmx/MBeanTreePollingMessageSource.java | 127 ++++++++++ .../jmx/NamedFieldsMBeanAttributeFilter.java | 48 ++++ .../NotNamedFieldsMBeanAttributeFilter.java | 48 ++++ .../jmx/config/JmxNamespaceHandler.java | 6 +- .../MBeanTreePollingChannelAdapterParser.java | 77 ++++++ .../jmx/config/spring-integration-jmx-3.0.xsd | 93 +++++++ .../jmx/MBeanAttributeFilterTests-context.xml | 68 ++++++ .../jmx/MBeanAttributeFilterTests.java | 109 +++++++++ .../MBeanTreePollingMessageSourceTests.java | 107 ++++++++ ...llingChannelAdapterParserTests-context.xml | 98 ++++++++ ...nTreePollingChannelAdapterParserTests.java | 230 ++++++++++++++++++ src/reference/docbook/jmx.xml | 33 +++ src/reference/docbook/whats-new.xml | 11 + 17 files changed, 1347 insertions(+), 2 deletions(-) create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanAttributeFilter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanObjectConverter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanAttributeFilter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanObjectConverter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanTreePollingMessageSource.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NamedFieldsMBeanAttributeFilter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NotNamedFieldsMBeanAttributeFilter.java create mode 100644 spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParser.java create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests-context.xml create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests.java create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanTreePollingMessageSourceTests.java create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests-context.xml create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests.java diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanAttributeFilter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanAttributeFilter.java new file mode 100644 index 0000000000..d8e0085865 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanAttributeFilter.java @@ -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; + } + +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanObjectConverter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanObjectConverter.java new file mode 100644 index 0000000000..864b7e5a21 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/DefaultMBeanObjectConverter.java @@ -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 attributeMap = new HashMap(); + + 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 converted = new ArrayList(); + 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 returnable = new HashMap(); + Set 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 returnable = new HashMap(); + @SuppressWarnings("unchecked") + Set> keySet = (Set>) 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; + } +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanAttributeFilter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanAttributeFilter.java new file mode 100644 index 0000000000..bb295c9d24 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanAttributeFilter.java @@ -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); + +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanObjectConverter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanObjectConverter.java new file mode 100644 index 0000000000..348ec1c135 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanObjectConverter.java @@ -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); + +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanTreePollingMessageSource.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanTreePollingMessageSource.java new file mode 100644 index 0000000000..837e0df590 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/MBeanTreePollingMessageSource.java @@ -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 { + + 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 beans = new HashMap(); + Set 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; + } +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NamedFieldsMBeanAttributeFilter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NamedFieldsMBeanAttributeFilter.java new file mode 100644 index 0000000000..f7e045442f --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NamedFieldsMBeanAttributeFilter.java @@ -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; + } + +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NotNamedFieldsMBeanAttributeFilter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NotNamedFieldsMBeanAttributeFilter.java new file mode 100644 index 0000000000..de31d41488 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/NotNamedFieldsMBeanAttributeFilter.java @@ -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; + } + +} diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/JmxNamespaceHandler.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/JmxNamespaceHandler.java index 3d498e3924..45d05c69f4 100644 --- a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/JmxNamespaceHandler.java +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/JmxNamespaceHandler.java @@ -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 jmx 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()); diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParser.java b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParser.java new file mode 100644 index 0000000000..305da25ed2 --- /dev/null +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParser.java @@ -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(); + } + +} diff --git a/spring-integration-jmx/src/main/resources/org/springframework/integration/jmx/config/spring-integration-jmx-3.0.xsd b/spring-integration-jmx/src/main/resources/org/springframework/integration/jmx/config/spring-integration-jmx-3.0.xsd index ceaae8ef8b..65b2ff0920 100644 --- a/spring-integration-jmx/src/main/resources/org/springframework/integration/jmx/config/spring-integration-jmx-3.0.xsd +++ b/spring-integration-jmx/src/main/resources/org/springframework/integration/jmx/config/spring-integration-jmx-3.0.xsd @@ -32,6 +32,36 @@ + + + + + Defines an inbound Channel Adapter that polls for JMX MBeans. + + + + + + + + + + + + + + + + + + + + + + + + + @@ -181,6 +211,69 @@ + + + + Defines inbound operation invoking type + + + + + + + + + A string to be parsed into an ObjectName object + + + + + + + A reference to an ObjectName instance + + + + + + + + + + + + A string to be parsed into a QueryExp object + + + + + + + A reference to a QueryExp instance + + + + + + + + + + + + A reference to an MBeanObjectConverter. + + + + + + + + + + + + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests-context.xml new file mode 100644 index 0000000000..0a3cc9b813 --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests-context.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + SendCount + SendErrorCount + + + + + + + + + + + + + + + + + + + + + + + + SendCount + SendErrorCount + + + + + + + + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests.java new file mode 100644 index 0000000000..1fa4f09a3d --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanAttributeFilterTests.java @@ -0,0 +1,109 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.Message; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.endpoint.SourcePollingChannelAdapter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Stuart Williams + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class MBeanAttributeFilterTests { + + @Autowired + @Qualifier("out") + private PollableChannel channel; + + @Autowired + private SourcePollingChannelAdapter adapter; + + @Autowired + private SourcePollingChannelAdapter adapterNot; + + private final long testTimeout = 1000L; + + @Test + public void testAttributeFilter() { + while (channel.receive(0) != null) { + ; + } + adapter.start(); + + Message result = channel.receive(testTimeout); + assertNotNull(result); + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map payload = (Map) result.getPayload(); + assertEquals(4, payload.size()); + + @SuppressWarnings("unchecked") + Map bean = (Map) payload + .get("org.springframework.integration:name=in,type=MessageChannel"); + + assertEquals(2, bean.size()); + assertTrue(bean.containsKey("SendCount")); + assertTrue(bean.containsKey("SendErrorCount")); + + adapter.stop(); + } + + @Test + public void testAttributeFilterNot() { + while (channel.receive(0) != null) { + ; + } + adapterNot.start(); + + Message result = channel.receive(testTimeout); + assertNotNull(result); + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map payload = (Map) result.getPayload(); + assertEquals(4, payload.size()); + + @SuppressWarnings("unchecked") + Map bean = (Map) payload + .get("org.springframework.integration:name=in,type=MessageChannel"); + + assertEquals(8, bean.size()); + assertFalse(bean.containsKey("SendCount")); + assertFalse(bean.containsKey("SendErrorCount")); + + adapterNot.stop(); + } + +} diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanTreePollingMessageSourceTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanTreePollingMessageSourceTests.java new file mode 100644 index 0000000000..586b2fa0c8 --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/MBeanTreePollingMessageSourceTests.java @@ -0,0 +1,107 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import javax.management.MBeanServer; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.jmx.support.MBeanServerFactoryBean; + +/** + * @author Stuart Williams + * + */ +public class MBeanTreePollingMessageSourceTests { + + private MBeanServer server; + + @Before + public void setup() { + MBeanServerFactoryBean factoryBean = new MBeanServerFactoryBean(); + factoryBean.setLocateExistingServerIfPossible(true); + factoryBean.afterPropertiesSet(); + this.server = factoryBean.getObject(); + } + + @Test + public void testDefaultPoll() { + + MBeanObjectConverter converter = new DefaultMBeanObjectConverter(); + MBeanTreePollingMessageSource source = new MBeanTreePollingMessageSource(converter); + source.setServer(server); + + Object received = source.doReceive(); + + assertEquals(HashMap.class, received.getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) received; + + // test for a couple of MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + } + + @Test + public void testQueryNameFilteredPoll() { + MBeanObjectConverter converter = new DefaultMBeanObjectConverter(); + MBeanTreePollingMessageSource source = new MBeanTreePollingMessageSource(converter); + source.setServer(server); + source.setQueryName("java.lang:*"); + + Object received = source.doReceive(); + + assertEquals(HashMap.class, received.getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) received; + + // test for a few MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + assertFalse(beans.containsKey("java.util.logging:type=Logging")); + } + + @Test + public void testQueryExpressionFilteredPoll() { + MBeanObjectConverter converter = new DefaultMBeanObjectConverter(); + MBeanTreePollingMessageSource source = new MBeanTreePollingMessageSource(converter); + source.setServer(server); + source.setQueryExpression("*:type=Logging"); + + Object received = source.doReceive(); + + assertEquals(HashMap.class, received.getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) received; + + // test for a few MBeans + assertFalse(beans.containsKey("java.lang:type=OperatingSystem")); + assertFalse(beans.containsKey("java.lang:type=Runtime")); + assertTrue(beans.containsKey("java.util.logging:type=Logging")); + } + +} diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests-context.xml new file mode 100644 index 0000000000..4594f50722 --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests-context.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.lang:type=OperatingSystem + + + + + + + + + + + + + + java.lang:type=Runtime + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests.java new file mode 100644 index 0000000000..2cd2531585 --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/MBeanTreePollingChannelAdapterParserTests.java @@ -0,0 +1,230 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.QueryExp; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.Message; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.endpoint.SourcePollingChannelAdapter; +import org.springframework.integration.jmx.MBeanObjectConverter; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Stuart Williams + * @author Gary Russell + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class MBeanTreePollingChannelAdapterParserTests { + + @Autowired + @Qualifier("channel1") + private PollableChannel channel1; + + @Autowired + @Qualifier("channel2") + private PollableChannel channel2; + + @Autowired + @Qualifier("channel3") + private PollableChannel channel3; + + @Autowired + @Qualifier("channel4") + private PollableChannel channel4; + + @Autowired + @Qualifier("channel5") + private PollableChannel channel5; + + @Autowired + @Qualifier("channel6") + private PollableChannel channel6; + + @Autowired + @Qualifier("adapter-default") + private SourcePollingChannelAdapter adapterDefault; + + @Autowired + @Qualifier("adapter-inner") + private SourcePollingChannelAdapter adapterInner; + + @Autowired + @Qualifier("adapter-query-name") + private SourcePollingChannelAdapter adapterQueryName; + + @Autowired + @Qualifier("adapter-query-name-bean") + private SourcePollingChannelAdapter adapterQueryNameBean; + + @Autowired + @Qualifier("adapter-query-expr-bean") + private SourcePollingChannelAdapter adapterQueryExprBean; + + @Autowired + @Qualifier("adapter-converter") + private SourcePollingChannelAdapter adapterConverter; + + @Autowired + private MBeanObjectConverter converter; + + @Autowired + private MBeanServer mbeanServer; + + private final long testTimeout = 2000L; + + @Test + public void pollDefaultAdapter() throws Exception { + adapterDefault.start(); + + Message result = channel1.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + } + + @Test + public void pollInnerAdapter() throws Exception { + adapterInner.start(); + + Message result = channel2.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + } + + @Test + public void pollQueryNameAdapter() throws Exception { + adapterQueryName.start(); + + ObjectName queryName = TestUtils.getPropertyValue(adapterQueryName, "source.queryName", ObjectName.class); + assertEquals("java.lang:type=Runtime", queryName.getCanonicalName()); + + QueryExp queryExp = TestUtils.getPropertyValue(adapterQueryName, "source.queryExpression", QueryExp.class); + assertTrue(queryExp.apply(new ObjectName("java.lang:type=Runtime"))); + + Message result = channel3.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertFalse(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + } + + @Test + public void pollQueryNameBeanAdapter() throws Exception { + adapterQueryNameBean.start(); + + Message result = channel4.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertFalse(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + } + + @Test + public void pollQueryExprBeanAdapter() throws Exception { + adapterQueryExprBean.start(); + + Message result = channel5.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertFalse(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + } + + @Test + public void pollConverterAdapter() throws Exception { + adapterConverter.start(); + + Message result = channel6.receive(testTimeout); + assertNotNull(result); + + assertEquals(HashMap.class, result.getPayload().getClass()); + + @SuppressWarnings("unchecked") + Map beans = (Map) result.getPayload(); + + // test for a couple of MBeans + assertTrue(beans.containsKey("java.lang:type=OperatingSystem")); + assertTrue(beans.containsKey("java.lang:type=Runtime")); + + adapterDefault.stop(); + assertSame(converter, TestUtils.getPropertyValue(adapterConverter, "source.converter")); + } + +} diff --git a/src/reference/docbook/jmx.xml b/src/reference/docbook/jmx.xml index 00230f8b1a..b41b366eda 100644 --- a/src/reference/docbook/jmx.xml +++ b/src/reference/docbook/jmx.xml @@ -137,6 +137,39 @@ ]]> +
+ Tree Polling Channel Adapter + + The Tree Polling Channel Adapter queries the JMX MBean tree and + sends a message with a payload that is the graph of objects that matches the query. By + default the MBeans are mapped to primitives and simple Objects like Map, List and arrays - + permitting simple transformation, for example, to JSON. An MBeanServer reference is also + required, but it will automatically check for a bean named mbeanServer + by default, just like the Notification-listening Channel Adapter + described above. A basic configuration would be: + + + +]]> + + This will include all attributes on the MBeans selected. You can filter the attributes by + providing an MBeanObjectConverter that + has an appropriate filter configured. The converter can be provided as a reference to + a bean definition using the converter attribute, or as an + inner <bean/> definition. A DefaultMBeanObjectConverter is + provided which can take a MBeanAttributeFilter in + its constructor argument. + + + Two standard filters are provided; the NamedFieldsMBeanAttributeFilter + allows you to specify a list of attributes to include and the + NotNamedFieldsMBeanAttributeFilter allows you to specify a list + of attributes to exclude. You can also implement your own filter + +
+
Operation Invoking Channel Adapter diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index b9c512e73b..800456cd6e 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -60,6 +60,17 @@ .
+
+ JMX Support + + A new <int-jmx:tree-polling-channel-adapter/> is provided; this + adapter queries the JMX MBean tree and sends a message with a payload that is the + graph of objects that matches the query. By default the MBeans are mapped to + primitives and simple Objects like Map, List and arrays - permitting simple + transformation, for example, to JSON + . + +
'Tail' Support