From 2fb039004d844159bc389710c36fe8798dafc09d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 13 Oct 2015 12:26:44 -0400 Subject: [PATCH] INT-3852: Support ObjectName Customization JIRA: https://jira.spring.io/browse/INT-3852 Custom object names in `@IntegrationManagedResource` were ignored. Some EIP objects were wrapped with their `Lifecycle` in order to expose a single MBean with all attributes and operations. This process "hid" the annotation so the object namer failed to detect the presence of a custom object name or the other attributes. Also, update the `@IntegrationManagedResource` to use the Spring 4.2 `@AliasFor` annotation. INT-3852: Polishing Polishing - Test with Proxy --- .../IntegrationManagedResource.java | 6 +- .../monitor/IntegrationMBeanExporter.java | 85 ++++++--- .../config/CustomObjectNameTests-context.xml | 49 +++++ .../jmx/config/CustomObjectNameTests.java | 168 ++++++++++++++++++ 4 files changed, 282 insertions(+), 26 deletions(-) create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests-context.xml create mode 100644 spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/management/IntegrationManagedResource.java b/spring-integration-core/src/main/java/org/springframework/integration/support/management/IntegrationManagedResource.java index a2b641ca4c..c53b62d758 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/management/IntegrationManagedResource.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/management/IntegrationManagedResource.java @@ -22,11 +22,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.jmx.export.annotation.ManagedResource; /** * Clone of {@link ManagedResource} limiting beans thus annotated so that they - * will only be exported by the {@code IntegrationMBeanExporter}. + * will only be exported by the {@code IntegrationMBeanExporter} and prevented + * from being exported by other MBeanExporters (if present). * * @author Gary Russell * @since 4.2 @@ -43,8 +45,10 @@ public @interface IntegrationManagedResource { * attribute, for simple default usage. * @return the value. */ + @AliasFor("objectName") String value() default ""; + @AliasFor("value") String objectName() default ""; String description() default ""; diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java index 0205a9a983..d71534df41 100644 --- a/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java @@ -25,8 +25,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import javax.management.Descriptor; import javax.management.DynamicMBean; import javax.management.JMException; +import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.modelmbean.ModelMBean; @@ -36,6 +38,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.Advised; import org.springframework.beans.BeansException; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.annotation.AnnotationBeanUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -71,9 +74,10 @@ import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.export.annotation.ManagedOperation; -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.metadata.ManagedResource; import org.springframework.jmx.export.naming.MetadataNamingStrategy; import org.springframework.jmx.support.MetricType; import org.springframework.messaging.MessageChannel; @@ -83,7 +87,6 @@ import org.springframework.util.PatternMatchUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.FieldCallback; import org.springframework.util.ReflectionUtils.FieldFilter; -import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** @@ -113,7 +116,7 @@ import org.springframework.util.StringValueResolver; * @author Gary Russell * @author Artem Bilan */ -@ManagedResource +@org.springframework.jmx.export.annotation.ManagedResource public class IntegrationMBeanExporter extends MBeanExporter implements ApplicationContextAware, EmbeddedValueResolverAware { @@ -121,7 +124,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements Applicati public static final String DEFAULT_DOMAIN = "org.springframework.integration"; - private final AnnotationJmxAttributeSource attributeSource = new IntegrationJmxAttributeSource(); + private final IntegrationJmxAttributeSource attributeSource = new IntegrationJmxAttributeSource(); private ApplicationContext applicationContext; @@ -155,9 +158,9 @@ public class IntegrationMBeanExporter extends MBeanExporter implements Applicati private final Properties objectNameStaticProperties = new Properties(); - private final MetadataMBeanInfoAssembler assembler = new MetadataMBeanInfoAssembler(attributeSource); + private final MetadataMBeanInfoAssembler assembler = new IntegrationMetadataMBeanInfoAssembler(attributeSource); - private final MetadataNamingStrategy defaultNamingStrategy = new MetadataNamingStrategy(attributeSource); + private final MetadataNamingStrategy defaultNamingStrategy = new IntegrationMetadataNamingStrategy(attributeSource); private String[] componentNamePatterns = { "*" }; @@ -165,8 +168,6 @@ public class IntegrationMBeanExporter extends MBeanExporter implements Applicati private final AtomicBoolean shuttingDown = new AtomicBoolean(); - private StringValueResolver embeddedValueResolver; - public IntegrationMBeanExporter() { super(); @@ -221,7 +222,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements Applicati @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; + this.attributeSource.setValueResolver(resolver); } @Override @@ -1067,29 +1068,63 @@ public class IntegrationMBeanExporter extends MBeanExporter implements Applicati return ReflectionUtils.getField(field, target); } - private class IntegrationJmxAttributeSource extends AnnotationJmxAttributeSource { + private static Object extractManagedBean(Object managedBean) { + if (managedBean instanceof LifecycleMessageHandlerMetrics + || managedBean instanceof LifecycleMessageSourceMetrics) { + DirectFieldAccessor accessor = new DirectFieldAccessor(managedBean); + return accessor.getPropertyValue("delegate"); + } + return managedBean; + } + + private static class IntegrationJmxAttributeSource extends AnnotationJmxAttributeSource { + + private StringValueResolver valueResolver; + + void setValueResolver(StringValueResolver valueResolver) { + this.valueResolver = valueResolver; + } @Override - public org.springframework.jmx.export.metadata.ManagedResource getManagedResource(Class beanClass) - throws InvalidMetadataException { - IntegrationManagedResource ann = - AnnotationUtils.getAnnotation(beanClass, IntegrationManagedResource.class); + public ManagedResource getManagedResource(Class beanClass) throws InvalidMetadataException { + IntegrationManagedResource ann = AnnotationUtils.getAnnotation(beanClass, IntegrationManagedResource.class); if (ann == null) { return null; } - org.springframework.jmx.export.metadata.ManagedResource managedResource = - new org.springframework.jmx.export.metadata.ManagedResource(); - AnnotationBeanUtils.copyPropertiesToBean(ann, managedResource, - IntegrationMBeanExporter.this.embeddedValueResolver); - if (!"".equals(ann.value()) && !StringUtils.hasLength(managedResource.getObjectName())) { - String value = ann.value(); - if (IntegrationMBeanExporter.this.embeddedValueResolver != null) { - value = IntegrationMBeanExporter.this.embeddedValueResolver.resolveStringValue(value); - } - managedResource.setObjectName(value); - } + ManagedResource managedResource = new ManagedResource(); + AnnotationBeanUtils.copyPropertiesToBean(ann, managedResource, this.valueResolver); return managedResource; } + } + + private static class IntegrationMetadataMBeanInfoAssembler extends MetadataMBeanInfoAssembler { + + public IntegrationMetadataMBeanInfoAssembler(JmxAttributeSource attributeSource) { + super(attributeSource); + } + + @Override + protected String getDescription(Object managedBean, String beanKey) { + return super.getDescription(extractManagedBean(managedBean), beanKey); + } + + @Override + protected void populateMBeanDescriptor(Descriptor desc, Object managedBean, String beanKey) { + super.populateMBeanDescriptor(desc, extractManagedBean(managedBean), beanKey); + } + + } + + private static class IntegrationMetadataNamingStrategy extends MetadataNamingStrategy { + + public IntegrationMetadataNamingStrategy(JmxAttributeSource attributeSource) { + super(attributeSource); + } + + @Override + public ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException { + return super.getObjectName(extractManagedBean(managedBean), beanKey); + } } diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests-context.xml new file mode 100644 index 0000000000..ee04714f3b --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests-context.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + custom:type=MessageChannel,name=foo + custom:type=MessageHandler,name=foo + custom:type=MessageSource,name=foo + custom:type=MessageRouter,name=foo + + + + + + + + + + + + + + + + + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests.java new file mode 100644 index 0000000000..af1467d571 --- /dev/null +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/CustomObjectNameTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. 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.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.management.Descriptor; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.integration.endpoint.AbstractMessageSource; +import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.integration.router.AbstractMappingMessageRouter; +import org.springframework.integration.support.management.IntegrationManagedResource; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Gary Russell + * @since 4.2.1 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext +public class CustomObjectNameTests { + + @Autowired + private MBeanServer server; + + @Autowired + private MessageHandler customHandler; + + @Test + public void testCustomMBeanRegistration() throws Exception { + Set names = server.queryNames(new ObjectName("custom:type=MessageChannel,*"), null); + assertEquals(1, names.size()); + ObjectName name = names.iterator().next(); + assertEquals("custom:type=MessageChannel,name=foo", name.toString()); + MBeanInfo mBeanInfo = server.getMBeanInfo(name); + assertEquals("custom channel", mBeanInfo.getDescription()); + names = server.queryNames(new ObjectName("custom:type=MessageHandler,*"), null); + assertEquals(1, names.size()); + name = names.iterator().next(); + assertEquals("custom:type=MessageHandler,name=foo", name.toString()); + mBeanInfo = server.getMBeanInfo(name); + assertEquals("custom handler", mBeanInfo.getDescription()); + Descriptor descriptor = mBeanInfo.getDescriptor(); + assertEquals("true", descriptor.getFieldValue("log")); + assertEquals("foo", descriptor.getFieldValue("logFile")); + assertEquals("1000", descriptor.getFieldValue("currencyTimeLimit")); + assertEquals("bar", descriptor.getFieldValue("persistLocation")); + assertEquals("baz", descriptor.getFieldValue("persistName")); + assertEquals("10", descriptor.getFieldValue("persistPeriod")); + assertEquals("Never", descriptor.getFieldValue("persistPolicy")); + names = server.queryNames(new ObjectName("custom:type=MessageSource,*"), null); + assertEquals(1, names.size()); + name = names.iterator().next(); + assertEquals("custom:type=MessageSource,name=foo", name.toString()); + names = server.queryNames(new ObjectName("custom:type=MessageRouter,*"), null); + assertEquals(1, names.size()); + name = names.iterator().next(); + assertEquals("custom:type=MessageRouter,name=foo", name.toString()); + names = server.queryNames(new ObjectName("test.custom:type=MessageHandler,*"), null); + assertEquals(2, names.size()); + Iterator iterator = names.iterator(); + name = iterator.next(); + assertEquals("test.custom:type=MessageHandler,name=standardHandler,bean=handler", name.toString()); + name = iterator.next(); + assertEquals("test.custom:type=MessageHandler,name=errorLogger,bean=internal", name.toString()); + assertTrue(AopUtils.isJdkDynamicProxy(this.customHandler)); + } + + @IntegrationManagedResource(objectName="${customChannelName}", description="custom channel") + public static class ChannelWithCustomObjectName extends AbstractMessageChannel { + + @Override + protected boolean doSend(Message message, long timeout) { + return false; + } + + } + + @IntegrationManagedResource(objectName="${customHandlerName}", description="custom handler", + currencyTimeLimit=1000, log=true, logFile="foo", persistLocation="bar", persistName="baz", persistPeriod=10, + persistPolicy="Never") + @Transactional + public static class HandlerWithCustomObjectName extends AbstractMessageHandler { + + @Override + public void handleMessageInternal(Message message) throws Exception { + } + + } + + @IntegrationManagedResource("${customSourceName}") + public static class SourceWithCustomObjectName extends AbstractMessageSource { + + @Override + public String getComponentType() { + return "foo"; + } + + @Override + protected Object doReceive() { + return null; + } + + } + + @IntegrationManagedResource("${customRouterName}") + public static class RouterWithCustomObjectName extends AbstractMappingMessageRouter { + + @Override + protected List getChannelKeys(Message message) { + return null; + } + + } + + @IntegrationManagedResource + public static class HandlerWithStandardObjectName extends AbstractMessageHandler { + + @Override + protected void handleMessageInternal(Message message) throws Exception { + } + + } + + public static class TxConfig { + + @Bean + public PlatformTransactionManager transactionManager() { + return mock(PlatformTransactionManager.class); + } + + } + +}