diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index 8a3a79e668e..327b1e17dc3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -53,22 +53,26 @@ public class EndpointMBean implements DynamicMBean { private final JmxOperationResponseMapper responseMapper; + private final ClassLoader classLoader; + private final ExposableJmxEndpoint endpoint; private final MBeanInfo info; private final Map operations; - EndpointMBean(JmxOperationResponseMapper responseMapper, + EndpointMBean(JmxOperationResponseMapper responseMapper, ClassLoader classLoader, ExposableJmxEndpoint endpoint) { Assert.notNull(responseMapper, "ResponseMapper must not be null"); Assert.notNull(endpoint, "Endpoint must not be null"); this.responseMapper = responseMapper; + this.classLoader = classLoader; this.endpoint = endpoint; this.info = new MBeanInfoFactory(responseMapper).getMBeanInfo(endpoint); this.operations = getOperations(endpoint); } + private Map getOperations(ExposableJmxEndpoint endpoint) { Map operations = new HashMap<>(); endpoint.getOperations() @@ -90,7 +94,25 @@ public class EndpointMBean implements DynamicMBean { + "' has no operation named " + actionName; throw new ReflectionException(new IllegalArgumentException(message), message); } - return invoke(operation, params); + ClassLoader previousClassLoader = overrideThreadContextClassLoader(this.classLoader); + try { + return invoke(operation, params); + } + finally { + overrideThreadContextClassLoader(previousClassLoader); + } + } + + private ClassLoader overrideThreadContextClassLoader(ClassLoader classLoader) { + if (classLoader != null) { + try { + return ClassUtils.overrideThreadContextClassLoader(classLoader); + } + catch (SecurityException ex) { + // can't set class loader, ignore it and proceed + } + } + return null; } private Object invoke(JmxOperation operation, Object[] params) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java index 97f71333dbb..33aa7e64c47 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java @@ -29,6 +29,7 @@ import javax.management.ObjectName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.JmxException; @@ -42,10 +43,13 @@ import org.springframework.util.Assert; * @author Phillip Webb * @since 2.0.0 */ -public class JmxEndpointExporter implements InitializingBean, DisposableBean { +public class JmxEndpointExporter + implements InitializingBean, DisposableBean, BeanClassLoaderAware { private static final Log logger = LogFactory.getLog(JmxEndpointExporter.class); + private ClassLoader classLoader; + private final MBeanServer mBeanServer; private final EndpointObjectNameFactory objectNameFactory; @@ -70,6 +74,11 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean { this.endpoints = Collections.unmodifiableCollection(endpoints); } + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + @Override public void afterPropertiesSet() { this.registered = register(); @@ -88,7 +97,8 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean { Assert.notNull(endpoint, "Endpoint must not be null"); try { ObjectName name = this.objectNameFactory.getObjectName(endpoint); - EndpointMBean mbean = new EndpointMBean(this.responseMapper, endpoint); + EndpointMBean mbean = new EndpointMBean(this.responseMapper, this.classLoader, + endpoint); this.mBeanServer.registerMBean(mbean, name); return name; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java index f851e3febc5..6813552892a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.endpoint.jmx; +import java.net.URL; +import java.net.URLClassLoader; + import javax.management.Attribute; import javax.management.AttributeList; import javax.management.AttributeNotFoundException; @@ -32,6 +35,7 @@ import reactor.core.publisher.Mono; import org.springframework.beans.FatalBeanException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.instanceOf; @@ -63,19 +67,19 @@ public class EndpointMBeanTests { public void createWhenResponseMapperIsNullShouldThrowException() { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("ResponseMapper must not be null"); - new EndpointMBean(null, mock(ExposableJmxEndpoint.class)); + new EndpointMBean(null, null, mock(ExposableJmxEndpoint.class)); } @Test public void createWhenEndpointIsNullShouldThrowException() { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Endpoint must not be null"); - new EndpointMBean(mock(JmxOperationResponseMapper.class), null); + new EndpointMBean(mock(JmxOperationResponseMapper.class), null, null); } @Test public void getMBeanInfoShouldReturnMBeanInfo() { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); MBeanInfo info = bean.getMBeanInfo(); assertThat(info.getDescription()).isEqualTo("MBean operations for endpoint test"); } @@ -83,7 +87,7 @@ public class EndpointMBeanTests { @Test public void invokeShouldInvokeJmxOperation() throws MBeanException, ReflectionException { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); assertThat(result).isEqualTo("result"); } @@ -95,7 +99,7 @@ public class EndpointMBeanTests { new TestJmxOperation((arguments) -> { throw new FatalBeanException("test failure"); })); - EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint); + EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); this.thrown.expect(MBeanException.class); this.thrown.expectCause(instanceOf(IllegalStateException.class)); this.thrown.expectMessage("test failure"); @@ -109,7 +113,7 @@ public class EndpointMBeanTests { new TestJmxOperation((arguments) -> { throw new UnsupportedOperationException("test failure"); })); - EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint); + EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); this.thrown.expect(MBeanException.class); this.thrown.expectCause(instanceOf(UnsupportedOperationException.class)); this.thrown.expectMessage("test failure"); @@ -119,13 +123,29 @@ public class EndpointMBeanTests { @Test public void invokeWhenActionNameIsNotAnOperationShouldThrowException() throws MBeanException, ReflectionException { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); this.thrown.expect(ReflectionException.class); this.thrown.expectCause(instanceOf(IllegalArgumentException.class)); this.thrown.expectMessage("no operation named missingOperation"); bean.invoke("missingOperation", NO_PARAMS, NO_SIGNATURE); } + @Test + public void invokeShouldInvokeJmxOperationWithBeanClassLoader() + throws ReflectionException, MBeanException { + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( + new TestJmxOperation((arguments) -> ClassUtils.getDefaultClassLoader())); + URLClassLoader beanClassLoader = new URLClassLoader(new URL[0], + getClass().getClassLoader()); + EndpointMBean bean = new EndpointMBean(this.responseMapper, beanClassLoader, + endpoint); + Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); + assertThat(result).isEqualTo(beanClassLoader); + assertThat(Thread.currentThread().getContextClassLoader()) + .isEqualTo(originalClassLoader); + } + @Test public void invokeWhenOperationIsInvalidShouldThrowException() throws MBeanException, ReflectionException { @@ -138,7 +158,7 @@ public class EndpointMBeanTests { }; TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(operation); - EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint); + EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); this.thrown.expect(ReflectionException.class); this.thrown.expectCause(instanceOf(IllegalArgumentException.class)); this.thrown.expectMessage("test failure"); @@ -150,7 +170,7 @@ public class EndpointMBeanTests { throws MBeanException, ReflectionException { TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( new TestJmxOperation((arguments) -> Mono.just("monoResult"))); - EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint); + EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); assertThat(result).isEqualTo("monoResult"); } @@ -159,7 +179,7 @@ public class EndpointMBeanTests { public void invokeShouldCallResponseMapper() throws MBeanException, ReflectionException { TestJmxOperationResponseMapper responseMapper = spy(this.responseMapper); - EndpointMBean bean = new EndpointMBean(responseMapper, this.endpoint); + EndpointMBean bean = new EndpointMBean(responseMapper, null, this.endpoint); bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); verify(responseMapper).mapResponseType(String.class); verify(responseMapper).mapResponse("result"); @@ -168,7 +188,7 @@ public class EndpointMBeanTests { @Test public void getAttributeShouldThrowException() throws AttributeNotFoundException, MBeanException, ReflectionException { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); this.thrown.expect(AttributeNotFoundException.class); this.thrown.expectMessage("EndpointMBeans do not support attributes"); bean.getAttribute("test"); @@ -177,7 +197,7 @@ public class EndpointMBeanTests { @Test public void setAttributeShouldThrowException() throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); this.thrown.expect(AttributeNotFoundException.class); this.thrown.expectMessage("EndpointMBeans do not support attributes"); bean.setAttribute(new Attribute("test", "test")); @@ -185,18 +205,22 @@ public class EndpointMBeanTests { @Test public void getAttributesShouldReturnEmptyAttributeList() { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); AttributeList attributes = bean.getAttributes(new String[] { "test" }); assertThat(attributes).isEmpty(); } @Test public void setAttributesShouldReturnEmptyAttributeList() { - EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + EndpointMBean bean = createEndpointMBean(); AttributeList sourceAttributes = new AttributeList(); sourceAttributes.add(new Attribute("test", "test")); AttributeList attributes = bean.setAttributes(sourceAttributes); assertThat(attributes).isEmpty(); } + private EndpointMBean createEndpointMBean() { + return new EndpointMBean(this.responseMapper, null, this.endpoint); + } + }