Commit f934e00e authored by Stephane Nicoll's avatar Stephane Nicoll

Merge pull request #12209 from Dav1dde:bugfix-12088

* pr/12209:
  Polish "Set classloader for JMX endpoints to application classloader"
  Set classloader for JMX endpoints to application classloader
parents c14b3a37 2be1c8f5
...@@ -53,22 +53,26 @@ public class EndpointMBean implements DynamicMBean { ...@@ -53,22 +53,26 @@ public class EndpointMBean implements DynamicMBean {
private final JmxOperationResponseMapper responseMapper; private final JmxOperationResponseMapper responseMapper;
private final ClassLoader classLoader;
private final ExposableJmxEndpoint endpoint; private final ExposableJmxEndpoint endpoint;
private final MBeanInfo info; private final MBeanInfo info;
private final Map<String, JmxOperation> operations; private final Map<String, JmxOperation> operations;
EndpointMBean(JmxOperationResponseMapper responseMapper, EndpointMBean(JmxOperationResponseMapper responseMapper, ClassLoader classLoader,
ExposableJmxEndpoint endpoint) { ExposableJmxEndpoint endpoint) {
Assert.notNull(responseMapper, "ResponseMapper must not be null"); Assert.notNull(responseMapper, "ResponseMapper must not be null");
Assert.notNull(endpoint, "Endpoint must not be null"); Assert.notNull(endpoint, "Endpoint must not be null");
this.responseMapper = responseMapper; this.responseMapper = responseMapper;
this.classLoader = classLoader;
this.endpoint = endpoint; this.endpoint = endpoint;
this.info = new MBeanInfoFactory(responseMapper).getMBeanInfo(endpoint); this.info = new MBeanInfoFactory(responseMapper).getMBeanInfo(endpoint);
this.operations = getOperations(endpoint); this.operations = getOperations(endpoint);
} }
private Map<String, JmxOperation> getOperations(ExposableJmxEndpoint endpoint) { private Map<String, JmxOperation> getOperations(ExposableJmxEndpoint endpoint) {
Map<String, JmxOperation> operations = new HashMap<>(); Map<String, JmxOperation> operations = new HashMap<>();
endpoint.getOperations() endpoint.getOperations()
...@@ -90,7 +94,25 @@ public class EndpointMBean implements DynamicMBean { ...@@ -90,7 +94,25 @@ public class EndpointMBean implements DynamicMBean {
+ "' has no operation named " + actionName; + "' has no operation named " + actionName;
throw new ReflectionException(new IllegalArgumentException(message), message); 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) private Object invoke(JmxOperation operation, Object[] params)
......
...@@ -29,6 +29,7 @@ import javax.management.ObjectName; ...@@ -29,6 +29,7 @@ import javax.management.ObjectName;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.jmx.JmxException; import org.springframework.jmx.JmxException;
...@@ -42,10 +43,13 @@ import org.springframework.util.Assert; ...@@ -42,10 +43,13 @@ import org.springframework.util.Assert;
* @author Phillip Webb * @author Phillip Webb
* @since 2.0.0 * @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 static final Log logger = LogFactory.getLog(JmxEndpointExporter.class);
private ClassLoader classLoader;
private final MBeanServer mBeanServer; private final MBeanServer mBeanServer;
private final EndpointObjectNameFactory objectNameFactory; private final EndpointObjectNameFactory objectNameFactory;
...@@ -70,6 +74,11 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean { ...@@ -70,6 +74,11 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean {
this.endpoints = Collections.unmodifiableCollection(endpoints); this.endpoints = Collections.unmodifiableCollection(endpoints);
} }
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override @Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
this.registered = register(); this.registered = register();
...@@ -88,7 +97,8 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean { ...@@ -88,7 +97,8 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean {
Assert.notNull(endpoint, "Endpoint must not be null"); Assert.notNull(endpoint, "Endpoint must not be null");
try { try {
ObjectName name = this.objectNameFactory.getObjectName(endpoint); 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); this.mBeanServer.registerMBean(mbean, name);
return name; return name;
} }
......
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
package org.springframework.boot.actuate.endpoint.jmx; package org.springframework.boot.actuate.endpoint.jmx;
import java.net.URL;
import java.net.URLClassLoader;
import javax.management.Attribute; import javax.management.Attribute;
import javax.management.AttributeList; import javax.management.AttributeList;
import javax.management.AttributeNotFoundException; import javax.management.AttributeNotFoundException;
...@@ -32,6 +35,7 @@ import reactor.core.publisher.Mono; ...@@ -32,6 +35,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.FatalBeanException; import org.springframework.beans.FatalBeanException;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.instanceOf;
...@@ -63,19 +67,19 @@ public class EndpointMBeanTests { ...@@ -63,19 +67,19 @@ public class EndpointMBeanTests {
public void createWhenResponseMapperIsNullShouldThrowException() { public void createWhenResponseMapperIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ResponseMapper must not be null"); this.thrown.expectMessage("ResponseMapper must not be null");
new EndpointMBean(null, mock(ExposableJmxEndpoint.class)); new EndpointMBean(null, null, mock(ExposableJmxEndpoint.class));
} }
@Test @Test
public void createWhenEndpointIsNullShouldThrowException() { public void createWhenEndpointIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Endpoint must not be null"); this.thrown.expectMessage("Endpoint must not be null");
new EndpointMBean(mock(JmxOperationResponseMapper.class), null); new EndpointMBean(mock(JmxOperationResponseMapper.class), null, null);
} }
@Test @Test
public void getMBeanInfoShouldReturnMBeanInfo() { public void getMBeanInfoShouldReturnMBeanInfo() {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
MBeanInfo info = bean.getMBeanInfo(); MBeanInfo info = bean.getMBeanInfo();
assertThat(info.getDescription()).isEqualTo("MBean operations for endpoint test"); assertThat(info.getDescription()).isEqualTo("MBean operations for endpoint test");
} }
...@@ -83,7 +87,7 @@ public class EndpointMBeanTests { ...@@ -83,7 +87,7 @@ public class EndpointMBeanTests {
@Test @Test
public void invokeShouldInvokeJmxOperation() public void invokeShouldInvokeJmxOperation()
throws MBeanException, ReflectionException { throws MBeanException, ReflectionException {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE);
assertThat(result).isEqualTo("result"); assertThat(result).isEqualTo("result");
} }
...@@ -95,7 +99,7 @@ public class EndpointMBeanTests { ...@@ -95,7 +99,7 @@ public class EndpointMBeanTests {
new TestJmxOperation((arguments) -> { new TestJmxOperation((arguments) -> {
throw new FatalBeanException("test failure"); 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.expect(MBeanException.class);
this.thrown.expectCause(instanceOf(IllegalStateException.class)); this.thrown.expectCause(instanceOf(IllegalStateException.class));
this.thrown.expectMessage("test failure"); this.thrown.expectMessage("test failure");
...@@ -109,7 +113,7 @@ public class EndpointMBeanTests { ...@@ -109,7 +113,7 @@ public class EndpointMBeanTests {
new TestJmxOperation((arguments) -> { new TestJmxOperation((arguments) -> {
throw new UnsupportedOperationException("test failure"); 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.expect(MBeanException.class);
this.thrown.expectCause(instanceOf(UnsupportedOperationException.class)); this.thrown.expectCause(instanceOf(UnsupportedOperationException.class));
this.thrown.expectMessage("test failure"); this.thrown.expectMessage("test failure");
...@@ -119,13 +123,29 @@ public class EndpointMBeanTests { ...@@ -119,13 +123,29 @@ public class EndpointMBeanTests {
@Test @Test
public void invokeWhenActionNameIsNotAnOperationShouldThrowException() public void invokeWhenActionNameIsNotAnOperationShouldThrowException()
throws MBeanException, ReflectionException { throws MBeanException, ReflectionException {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
this.thrown.expect(ReflectionException.class); this.thrown.expect(ReflectionException.class);
this.thrown.expectCause(instanceOf(IllegalArgumentException.class)); this.thrown.expectCause(instanceOf(IllegalArgumentException.class));
this.thrown.expectMessage("no operation named missingOperation"); this.thrown.expectMessage("no operation named missingOperation");
bean.invoke("missingOperation", NO_PARAMS, NO_SIGNATURE); 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 @Test
public void invokeWhenOperationIsInvalidShouldThrowException() public void invokeWhenOperationIsInvalidShouldThrowException()
throws MBeanException, ReflectionException { throws MBeanException, ReflectionException {
...@@ -138,7 +158,7 @@ public class EndpointMBeanTests { ...@@ -138,7 +158,7 @@ public class EndpointMBeanTests {
}; };
TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(operation); 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.expect(ReflectionException.class);
this.thrown.expectCause(instanceOf(IllegalArgumentException.class)); this.thrown.expectCause(instanceOf(IllegalArgumentException.class));
this.thrown.expectMessage("test failure"); this.thrown.expectMessage("test failure");
...@@ -150,7 +170,7 @@ public class EndpointMBeanTests { ...@@ -150,7 +170,7 @@ public class EndpointMBeanTests {
throws MBeanException, ReflectionException { throws MBeanException, ReflectionException {
TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(
new TestJmxOperation((arguments) -> Mono.just("monoResult"))); 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); Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE);
assertThat(result).isEqualTo("monoResult"); assertThat(result).isEqualTo("monoResult");
} }
...@@ -159,7 +179,7 @@ public class EndpointMBeanTests { ...@@ -159,7 +179,7 @@ public class EndpointMBeanTests {
public void invokeShouldCallResponseMapper() public void invokeShouldCallResponseMapper()
throws MBeanException, ReflectionException { throws MBeanException, ReflectionException {
TestJmxOperationResponseMapper responseMapper = spy(this.responseMapper); 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); bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE);
verify(responseMapper).mapResponseType(String.class); verify(responseMapper).mapResponseType(String.class);
verify(responseMapper).mapResponse("result"); verify(responseMapper).mapResponse("result");
...@@ -168,7 +188,7 @@ public class EndpointMBeanTests { ...@@ -168,7 +188,7 @@ public class EndpointMBeanTests {
@Test @Test
public void getAttributeShouldThrowException() public void getAttributeShouldThrowException()
throws AttributeNotFoundException, MBeanException, ReflectionException { throws AttributeNotFoundException, MBeanException, ReflectionException {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
this.thrown.expect(AttributeNotFoundException.class); this.thrown.expect(AttributeNotFoundException.class);
this.thrown.expectMessage("EndpointMBeans do not support attributes"); this.thrown.expectMessage("EndpointMBeans do not support attributes");
bean.getAttribute("test"); bean.getAttribute("test");
...@@ -177,7 +197,7 @@ public class EndpointMBeanTests { ...@@ -177,7 +197,7 @@ public class EndpointMBeanTests {
@Test @Test
public void setAttributeShouldThrowException() throws AttributeNotFoundException, public void setAttributeShouldThrowException() throws AttributeNotFoundException,
InvalidAttributeValueException, MBeanException, ReflectionException { InvalidAttributeValueException, MBeanException, ReflectionException {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
this.thrown.expect(AttributeNotFoundException.class); this.thrown.expect(AttributeNotFoundException.class);
this.thrown.expectMessage("EndpointMBeans do not support attributes"); this.thrown.expectMessage("EndpointMBeans do not support attributes");
bean.setAttribute(new Attribute("test", "test")); bean.setAttribute(new Attribute("test", "test"));
...@@ -185,18 +205,22 @@ public class EndpointMBeanTests { ...@@ -185,18 +205,22 @@ public class EndpointMBeanTests {
@Test @Test
public void getAttributesShouldReturnEmptyAttributeList() { public void getAttributesShouldReturnEmptyAttributeList() {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
AttributeList attributes = bean.getAttributes(new String[] { "test" }); AttributeList attributes = bean.getAttributes(new String[] { "test" });
assertThat(attributes).isEmpty(); assertThat(attributes).isEmpty();
} }
@Test @Test
public void setAttributesShouldReturnEmptyAttributeList() { public void setAttributesShouldReturnEmptyAttributeList() {
EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); EndpointMBean bean = createEndpointMBean();
AttributeList sourceAttributes = new AttributeList(); AttributeList sourceAttributes = new AttributeList();
sourceAttributes.add(new Attribute("test", "test")); sourceAttributes.add(new Attribute("test", "test"));
AttributeList attributes = bean.setAttributes(sourceAttributes); AttributeList attributes = bean.setAttributes(sourceAttributes);
assertThat(attributes).isEmpty(); assertThat(attributes).isEmpty();
} }
private EndpointMBean createEndpointMBean() {
return new EndpointMBean(this.responseMapper, null, this.endpoint);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment