Polish "Set classloader for JMX endpoints to application classloader"
Closes gh-12209
This commit is contained in:
parent
e44c81672f
commit
2be1c8f527
|
@ -29,11 +29,8 @@ import javax.management.MBeanException;
|
||||||
import javax.management.MBeanInfo;
|
import javax.management.MBeanInfo;
|
||||||
import javax.management.ReflectionException;
|
import javax.management.ReflectionException;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
|
||||||
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.boot.actuate.endpoint.SecurityContext;
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
|
@ -49,37 +46,32 @@ import org.springframework.util.ClassUtils;
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class EndpointMBean implements DynamicMBean, BeanClassLoaderAware {
|
public class EndpointMBean implements DynamicMBean {
|
||||||
|
|
||||||
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
|
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
|
||||||
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
|
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
|
||||||
|
|
||||||
private static final Log logger = LogFactory.getLog(EndpointMBean.class);
|
|
||||||
|
|
||||||
private ClassLoader classLoader;
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
|
||||||
this.classLoader = classLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<>();
|
||||||
|
@ -102,26 +94,23 @@ public class EndpointMBean implements DynamicMBean, BeanClassLoaderAware {
|
||||||
+ "' has no operation named " + actionName;
|
+ "' has no operation named " + actionName;
|
||||||
throw new ReflectionException(new IllegalArgumentException(message), message);
|
throw new ReflectionException(new IllegalArgumentException(message), message);
|
||||||
}
|
}
|
||||||
ClassLoader previousClassLoader = overrideThreadContextClassLoaderSafe(this.classLoader);
|
ClassLoader previousClassLoader = overrideThreadContextClassLoader(this.classLoader);
|
||||||
try {
|
try {
|
||||||
return invoke(operation, params);
|
return invoke(operation, params);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
overrideThreadContextClassLoaderSafe(previousClassLoader);
|
overrideThreadContextClassLoader(previousClassLoader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClassLoader overrideThreadContextClassLoaderSafe(ClassLoader classLoader) {
|
private ClassLoader overrideThreadContextClassLoader(ClassLoader classLoader) {
|
||||||
if (classLoader == null) {
|
if (classLoader != null) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ClassUtils.overrideThreadContextClassLoader(classLoader);
|
return ClassUtils.overrideThreadContextClassLoader(classLoader);
|
||||||
}
|
}
|
||||||
catch (SecurityException exc) {
|
catch (SecurityException ex) {
|
||||||
// can't set class loader, ignore it and proceed
|
// can't set class loader, ignore it and proceed
|
||||||
logger.warn("Unable to override class loader for JMX endpoint.");
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,8 @@ 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, BeanClassLoaderAware {
|
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);
|
||||||
|
|
||||||
|
@ -96,8 +97,8 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean, Be
|
||||||
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,
|
||||||
mbean.setBeanClassLoader(this.classLoader);
|
endpoint);
|
||||||
this.mBeanServer.registerMBean(mbean, name);
|
this.mBeanServer.registerMBean(mbean, name);
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,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");
|
||||||
}
|
}
|
||||||
|
@ -87,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");
|
||||||
}
|
}
|
||||||
|
@ -99,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");
|
||||||
|
@ -113,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");
|
||||||
|
@ -123,7 +123,7 @@ 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");
|
||||||
|
@ -133,13 +133,17 @@ public class EndpointMBeanTests {
|
||||||
@Test
|
@Test
|
||||||
public void invokeShouldInvokeJmxOperationWithBeanClassLoader()
|
public void invokeShouldInvokeJmxOperationWithBeanClassLoader()
|
||||||
throws ReflectionException, MBeanException {
|
throws ReflectionException, MBeanException {
|
||||||
|
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(
|
TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(
|
||||||
new TestJmxOperation((arguments) -> ClassUtils.getDefaultClassLoader()));
|
new TestJmxOperation((arguments) -> ClassUtils.getDefaultClassLoader()));
|
||||||
URLClassLoader beanClassLoader = new URLClassLoader(new URL[]{}, getClass().getClassLoader());
|
URLClassLoader beanClassLoader = new URLClassLoader(new URL[0],
|
||||||
EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint);
|
getClass().getClassLoader());
|
||||||
bean.setBeanClassLoader(beanClassLoader);
|
EndpointMBean bean = new EndpointMBean(this.responseMapper, beanClassLoader,
|
||||||
|
endpoint);
|
||||||
Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE);
|
Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE);
|
||||||
assertThat(result).isEqualTo(beanClassLoader);
|
assertThat(result).isEqualTo(beanClassLoader);
|
||||||
|
assertThat(Thread.currentThread().getContextClassLoader())
|
||||||
|
.isEqualTo(originalClassLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -154,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");
|
||||||
|
@ -166,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");
|
||||||
}
|
}
|
||||||
|
@ -175,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");
|
||||||
|
@ -184,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");
|
||||||
|
@ -193,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"));
|
||||||
|
@ -201,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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue