Add support for making endpoints accessible via JMX
This commit adds support to the new endpoint infrastructure for exposing endpoint operations via JMX. It also introduces support for JMX-specific extensions to a general-purpose endpoint. Such an extension is identified by the `@JmxEndpointExtension` annotation. See gh-9946
This commit is contained in:
committed by
Andy Wilkinson
parent
4f2e4ada45
commit
4592e071db
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.management.Attribute;
|
||||
import javax.management.AttributeList;
|
||||
import javax.management.AttributeNotFoundException;
|
||||
import javax.management.DynamicMBean;
|
||||
import javax.management.InvalidAttributeValueException;
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.ReflectionException;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* A {@link DynamicMBean} that invokes operations on an {@link EndpointInfo endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @see EndpointMBeanInfoAssembler
|
||||
*/
|
||||
public class EndpointMBean implements DynamicMBean {
|
||||
|
||||
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
|
||||
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
|
||||
|
||||
private final Function<Object, Object> operationResponseConverter;
|
||||
|
||||
private final EndpointMBeanInfo endpointInfo;
|
||||
|
||||
EndpointMBean(Function<Object, Object> operationResponseConverter,
|
||||
EndpointMBeanInfo endpointInfo) {
|
||||
this.operationResponseConverter = operationResponseConverter;
|
||||
this.endpointInfo = endpointInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the related endpoint.
|
||||
* @return the endpoint id
|
||||
*/
|
||||
public String getEndpointId() {
|
||||
return this.endpointInfo.getEndpointId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MBeanInfo getMBeanInfo() {
|
||||
return this.endpointInfo.getMbeanInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(String actionName, Object[] params, String[] signature)
|
||||
throws MBeanException, ReflectionException {
|
||||
JmxEndpointOperation operationInfo = this.endpointInfo.getOperations()
|
||||
.get(actionName);
|
||||
if (operationInfo != null) {
|
||||
Map<String, Object> arguments = new HashMap<>();
|
||||
List<JmxEndpointOperationParameterInfo> parameters = operationInfo
|
||||
.getParameters();
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
arguments.put(parameters.get(i).getName(), params[i]);
|
||||
}
|
||||
Object result = operationInfo.getOperationInvoker().invoke(arguments);
|
||||
if (REACTOR_PRESENT) {
|
||||
result = ReactiveHandler.handle(result);
|
||||
}
|
||||
return this.operationResponseConverter.apply(result);
|
||||
}
|
||||
throw new ReflectionException(new IllegalArgumentException(
|
||||
String.format("Endpoint with id '%s' has no operation named %s",
|
||||
this.endpointInfo.getEndpointId(), actionName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String attribute)
|
||||
throws AttributeNotFoundException, MBeanException, ReflectionException {
|
||||
throw new AttributeNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
|
||||
InvalidAttributeValueException, MBeanException, ReflectionException {
|
||||
throw new AttributeNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList getAttributes(String[] attributes) {
|
||||
return new AttributeList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList setAttributes(AttributeList attributes) {
|
||||
return new AttributeList();
|
||||
}
|
||||
|
||||
private static class ReactiveHandler {
|
||||
|
||||
public static Object handle(Object result) {
|
||||
if (result instanceof Mono) {
|
||||
return ((Mono<?>) result).block();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperation;
|
||||
|
||||
/**
|
||||
* The {@link MBeanInfo} for a particular {@link EndpointInfo endpoint}. Maps operation
|
||||
* names to an {@link EndpointOperation}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class EndpointMBeanInfo {
|
||||
|
||||
private final String endpointId;
|
||||
|
||||
private final MBeanInfo mBeanInfo;
|
||||
|
||||
private final Map<String, JmxEndpointOperation> operations;
|
||||
|
||||
public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
|
||||
Map<String, JmxEndpointOperation> operations) {
|
||||
this.endpointId = endpointId;
|
||||
this.mBeanInfo = mBeanInfo;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
public String getEndpointId() {
|
||||
return this.endpointId;
|
||||
}
|
||||
|
||||
public MBeanInfo getMbeanInfo() {
|
||||
return this.mBeanInfo;
|
||||
}
|
||||
|
||||
public Map<String, JmxEndpointOperation> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
import javax.management.modelmbean.ModelMBeanAttributeInfo;
|
||||
import javax.management.modelmbean.ModelMBeanConstructorInfo;
|
||||
import javax.management.modelmbean.ModelMBeanInfoSupport;
|
||||
import javax.management.modelmbean.ModelMBeanNotificationInfo;
|
||||
import javax.management.modelmbean.ModelMBeanOperationInfo;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
|
||||
/**
|
||||
* Gathers the management operations of a particular {@link EndpointInfo endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class EndpointMBeanInfoAssembler {
|
||||
|
||||
private final JmxOperationResponseMapper responseMapper;
|
||||
|
||||
EndpointMBeanInfoAssembler(JmxOperationResponseMapper responseMapper) {
|
||||
this.responseMapper = responseMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link EndpointMBeanInfo} for the specified {@link EndpointInfo
|
||||
* endpoint}.
|
||||
* @param endpointInfo the endpoint to handle
|
||||
* @return the mbean info for the endpoint
|
||||
*/
|
||||
EndpointMBeanInfo createEndpointMBeanInfo(
|
||||
EndpointInfo<JmxEndpointOperation> endpointInfo) {
|
||||
Map<String, OperationInfos> operationsMapping = getOperationInfo(endpointInfo);
|
||||
ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
|
||||
.stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
|
||||
.toArray(new ModelMBeanOperationInfo[] {});
|
||||
Map<String, JmxEndpointOperation> operationsInfo = new LinkedHashMap<>();
|
||||
operationsMapping.forEach((name, t) -> operationsInfo.put(name, t.operation));
|
||||
|
||||
MBeanInfo info = new ModelMBeanInfoSupport(EndpointMBean.class.getName(),
|
||||
getDescription(endpointInfo), new ModelMBeanAttributeInfo[0],
|
||||
new ModelMBeanConstructorInfo[0], operationsMBeanInfo,
|
||||
new ModelMBeanNotificationInfo[0]);
|
||||
return new EndpointMBeanInfo(endpointInfo.getId(), info, operationsInfo);
|
||||
}
|
||||
|
||||
private String getDescription(EndpointInfo<?> endpointInfo) {
|
||||
return "MBean operations for endpoint " + endpointInfo.getId();
|
||||
}
|
||||
|
||||
private Map<String, OperationInfos> getOperationInfo(
|
||||
EndpointInfo<JmxEndpointOperation> endpointInfo) {
|
||||
Map<String, OperationInfos> operationInfos = new HashMap<>();
|
||||
endpointInfo.getOperations().forEach((operationInfo) -> {
|
||||
String name = operationInfo.getOperationName();
|
||||
ModelMBeanOperationInfo mBeanOperationInfo = new ModelMBeanOperationInfo(
|
||||
operationInfo.getOperationName(), operationInfo.getDescription(),
|
||||
getMBeanParameterInfos(operationInfo), this.responseMapper
|
||||
.mapResponseType(operationInfo.getOutputType()).getName(),
|
||||
mapOperationType(operationInfo.getType()));
|
||||
operationInfos.put(name,
|
||||
new OperationInfos(mBeanOperationInfo, operationInfo));
|
||||
});
|
||||
return operationInfos;
|
||||
}
|
||||
|
||||
private MBeanParameterInfo[] getMBeanParameterInfos(JmxEndpointOperation operation) {
|
||||
return operation.getParameters().stream()
|
||||
.map((operationParameter) -> new MBeanParameterInfo(
|
||||
operationParameter.getName(),
|
||||
operationParameter.getType().getName(),
|
||||
operationParameter.getDescription()))
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(),
|
||||
(parameterInfos) -> parameterInfos
|
||||
.toArray(new MBeanParameterInfo[parameterInfos.size()])));
|
||||
}
|
||||
|
||||
private int mapOperationType(EndpointOperationType type) {
|
||||
if (type == EndpointOperationType.READ) {
|
||||
return MBeanOperationInfo.INFO;
|
||||
}
|
||||
if (type == EndpointOperationType.WRITE) {
|
||||
return MBeanOperationInfo.ACTION;
|
||||
}
|
||||
return MBeanOperationInfo.UNKNOWN;
|
||||
}
|
||||
|
||||
private static class OperationInfos {
|
||||
|
||||
private final ModelMBeanOperationInfo mBeanOperationInfo;
|
||||
|
||||
private final JmxEndpointOperation operation;
|
||||
|
||||
OperationInfos(ModelMBeanOperationInfo mBeanOperationInfo,
|
||||
JmxEndpointOperation operation) {
|
||||
this.mBeanOperationInfo = mBeanOperationInfo;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanRegistrationException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.jmx.JmxException;
|
||||
import org.springframework.jmx.export.MBeanExportException;
|
||||
import org.springframework.jmx.export.MBeanExporter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* JMX Registrar for {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
* @see EndpointObjectNameFactory
|
||||
*/
|
||||
public class EndpointMBeanRegistrar {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(EndpointMBeanRegistrar.class);
|
||||
|
||||
private final MBeanServer mBeanServer;
|
||||
|
||||
private final EndpointObjectNameFactory objectNameFactory;
|
||||
|
||||
/**
|
||||
* Create a new instance with the {@link MBeanExporter} and
|
||||
* {@link EndpointObjectNameFactory} to use.
|
||||
* @param mBeanServer the mbean exporter
|
||||
* @param objectNameFactory the {@link ObjectName} factory
|
||||
*/
|
||||
public EndpointMBeanRegistrar(MBeanServer mBeanServer,
|
||||
EndpointObjectNameFactory objectNameFactory) {
|
||||
Assert.notNull(mBeanServer, "MBeanServer must not be null");
|
||||
Assert.notNull(objectNameFactory, "ObjectNameFactory must not be null");
|
||||
this.mBeanServer = mBeanServer;
|
||||
this.objectNameFactory = objectNameFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the specified {@link EndpointMBean} and return its {@link ObjectName}.
|
||||
* @param endpoint the endpoint to register
|
||||
* @return the {@link ObjectName} used to register the {@code endpoint}
|
||||
*/
|
||||
public ObjectName registerEndpointMBean(EndpointMBean endpoint) {
|
||||
Assert.notNull(endpoint, "Endpoint must not be null");
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format(
|
||||
"Registering endpoint with id '%s' to " + "the JMX domain",
|
||||
endpoint.getEndpointId()));
|
||||
}
|
||||
ObjectName objectName = this.objectNameFactory.generate(endpoint);
|
||||
this.mBeanServer.registerMBean(endpoint, objectName);
|
||||
return objectName;
|
||||
}
|
||||
catch (MalformedObjectNameException ex) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Invalid ObjectName for " + "endpoint with id '%s'",
|
||||
endpoint.getEndpointId()),
|
||||
ex);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new MBeanExportException(
|
||||
String.format("Failed to register MBean for endpoint with id '%s'",
|
||||
endpoint.getEndpointId()),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the specified {@link ObjectName} if necessary.
|
||||
* @param objectName the {@link ObjectName} of the endpoint to unregister
|
||||
* @return {@code true} if the endpoint was unregistered, {@code false} if no such
|
||||
* endpoint was found
|
||||
*/
|
||||
public boolean unregisterEndpointMbean(ObjectName objectName) {
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Unregister endpoint with ObjectName '%s' "
|
||||
+ "from the JMX domain", objectName));
|
||||
}
|
||||
this.mBeanServer.unregisterMBean(objectName);
|
||||
return true;
|
||||
}
|
||||
catch (InstanceNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
catch (MBeanRegistrationException ex) {
|
||||
throw new JmxException(
|
||||
String.format("Failed to unregister MBean with" + "ObjectName '%s'",
|
||||
objectName),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
/**
|
||||
* A factory to create an {@link ObjectName} for an {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EndpointObjectNameFactory {
|
||||
|
||||
/**
|
||||
* Generate an {@link ObjectName} for the specified {@link EndpointMBean endpoint}.
|
||||
* @param mBean the endpoint to handle
|
||||
* @return the {@link ObjectName} to use for the endpoint
|
||||
* @throws MalformedObjectNameException if the object name is invalid
|
||||
*/
|
||||
ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.endpoint.AnnotationEndpointDiscoverer;
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.CachingOperationInvoker;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
import org.springframework.boot.endpoint.EndpointType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
import org.springframework.boot.endpoint.OperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
|
||||
import org.springframework.jmx.export.metadata.ManagedOperation;
|
||||
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
|
||||
* {@link JmxEndpointExtension JMX extensions} applied to them.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxAnnotationEndpointDiscoverer
|
||||
extends AnnotationEndpointDiscoverer<JmxEndpointOperation, String> {
|
||||
|
||||
private static final AnnotationJmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
|
||||
|
||||
/**
|
||||
* Creates a new {@link JmxAnnotationEndpointDiscoverer} that will discover
|
||||
* {@link Endpoint endpoints} and {@link JmxEndpointExtension jmx extensions} using
|
||||
* the given {@link ApplicationContext}.
|
||||
*
|
||||
* @param applicationContext the application context
|
||||
* @param parameterMapper the {@link OperationParameterMapper} used to convert
|
||||
* arguments when an operation is invoked
|
||||
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
|
||||
*/
|
||||
public JmxAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
OperationParameterMapper parameterMapper,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
super(applicationContext, new JmxEndpointOperationFactory(parameterMapper),
|
||||
JmxEndpointOperation::getOperationName, cachingConfigurationFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EndpointInfo<JmxEndpointOperation>> discoverEndpoints() {
|
||||
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors = discoverEndpointsWithExtension(
|
||||
JmxEndpointExtension.class, EndpointType.JMX);
|
||||
verifyThatOperationsHaveDistinctName(endpointDescriptors);
|
||||
return endpointDescriptors.stream().map(EndpointInfoDescriptor::getEndpointInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void verifyThatOperationsHaveDistinctName(
|
||||
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors) {
|
||||
List<List<JmxEndpointOperation>> clashes = new ArrayList<>();
|
||||
endpointDescriptors.forEach((descriptor) -> clashes
|
||||
.addAll(descriptor.findDuplicateOperations().values()));
|
||||
if (!clashes.isEmpty()) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append(
|
||||
String.format("Found multiple JMX operations with the same name:%n"));
|
||||
clashes.forEach((clash) -> {
|
||||
message.append(" ").append(clash.get(0).getOperationName())
|
||||
.append(String.format(":%n"));
|
||||
clash.forEach((operation) -> message.append(" ")
|
||||
.append(String.format("%s%n", operation)));
|
||||
});
|
||||
throw new IllegalStateException(message.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class JmxEndpointOperationFactory
|
||||
implements EndpointOperationFactory<JmxEndpointOperation> {
|
||||
|
||||
private final OperationParameterMapper parameterMapper;
|
||||
|
||||
JmxEndpointOperationFactory(OperationParameterMapper parameterMapper) {
|
||||
this.parameterMapper = parameterMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JmxEndpointOperation createOperation(String endpointId,
|
||||
AnnotationAttributes operationAttributes, Object target, Method method,
|
||||
EndpointOperationType type, long timeToLive) {
|
||||
String operationName = method.getName();
|
||||
Class<?> outputType = mapParameterType(method.getReturnType());
|
||||
String description = getDescription(method,
|
||||
() -> "Invoke " + operationName + " for endpoint " + endpointId);
|
||||
List<JmxEndpointOperationParameterInfo> parameters = getParameters(method);
|
||||
OperationInvoker invoker = new ReflectiveOperationInvoker(
|
||||
this.parameterMapper, target, method);
|
||||
if (timeToLive > 0) {
|
||||
invoker = new CachingOperationInvoker(invoker, timeToLive);
|
||||
}
|
||||
return new JmxEndpointOperation(type, invoker, operationName, outputType,
|
||||
description, parameters);
|
||||
}
|
||||
|
||||
private String getDescription(Method method, Supplier<String> fallback) {
|
||||
ManagedOperation managedOperation = jmxAttributeSource
|
||||
.getManagedOperation(method);
|
||||
if (managedOperation != null
|
||||
&& StringUtils.hasText(managedOperation.getDescription())) {
|
||||
return managedOperation.getDescription();
|
||||
}
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
private List<JmxEndpointOperationParameterInfo> getParameters(Method method) {
|
||||
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
|
||||
Parameter[] methodParameters = method.getParameters();
|
||||
if (methodParameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
ManagedOperationParameter[] managedOperationParameters = jmxAttributeSource
|
||||
.getManagedOperationParameters(method);
|
||||
if (managedOperationParameters.length > 0) {
|
||||
for (int i = 0; i < managedOperationParameters.length; i++) {
|
||||
ManagedOperationParameter mBeanParameter = managedOperationParameters[i];
|
||||
Parameter methodParameter = methodParameters[i];
|
||||
parameters.add(new JmxEndpointOperationParameterInfo(
|
||||
mBeanParameter.getName(),
|
||||
mapParameterType(methodParameter.getType()),
|
||||
mBeanParameter.getDescription()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (Parameter parameter : methodParameters) {
|
||||
parameters.add(
|
||||
new JmxEndpointOperationParameterInfo(parameter.getName(),
|
||||
mapParameterType(parameter.getType()), null));
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private Class<?> mapParameterType(Class<?> parameter) {
|
||||
if (parameter.isEnum()) {
|
||||
return String.class;
|
||||
}
|
||||
if (Date.class.isAssignableFrom(parameter)) {
|
||||
return String.class;
|
||||
}
|
||||
if (parameter.getName().startsWith("java.")) {
|
||||
return parameter;
|
||||
}
|
||||
if (parameter.equals(Void.TYPE)) {
|
||||
return parameter;
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
|
||||
/**
|
||||
* Identifies a type as being a JMX-specific extension of an {@link Endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
* @see Endpoint
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface JmxEndpointExtension {
|
||||
|
||||
/**
|
||||
* The {@link Endpoint endpoint} class to which this JMX extension relates.
|
||||
* @return the endpoint class
|
||||
*/
|
||||
Class<?> endpoint();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
|
||||
/**
|
||||
* A factory for creating JMX MBeans for endpoint operations.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointMBeanFactory {
|
||||
|
||||
private final EndpointMBeanInfoAssembler assembler;
|
||||
|
||||
private final JmxOperationResponseMapper resultMapper;
|
||||
|
||||
/**
|
||||
* Create a new {@link JmxEndpointMBeanFactory} instance that will use the given
|
||||
* {@code responseMapper} to convert an operation's response to a JMX-friendly form.
|
||||
* @param responseMapper the response mapper
|
||||
*/
|
||||
public JmxEndpointMBeanFactory(JmxOperationResponseMapper responseMapper) {
|
||||
this.assembler = new EndpointMBeanInfoAssembler(responseMapper);
|
||||
this.resultMapper = responseMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates MBeans for the given {@code endpoints}.
|
||||
* @param endpoints the endpoints
|
||||
* @return the MBeans
|
||||
*/
|
||||
public Collection<EndpointMBean> createMBeans(
|
||||
Collection<EndpointInfo<JmxEndpointOperation>> endpoints) {
|
||||
return endpoints.stream().map((endpointInfo) -> {
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.assembler
|
||||
.createEndpointMBeanInfo(endpointInfo);
|
||||
return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointOperation;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
|
||||
/**
|
||||
* An operation on a JMX endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointOperation extends EndpointOperation {
|
||||
|
||||
private final String operationName;
|
||||
|
||||
private final Class<?> outputType;
|
||||
|
||||
private final String description;
|
||||
|
||||
private final List<JmxEndpointOperationParameterInfo> parameters;
|
||||
|
||||
/**
|
||||
* Creates a new {@code JmxEndpointOperation} for an operation of the given
|
||||
* {@code type}. The operation can be performed using the given {@code invoker}.
|
||||
* @param type the type of the operation
|
||||
* @param invoker used to perform the operation
|
||||
* @param operationName the name of the operation
|
||||
* @param outputType the type of the output of the operation
|
||||
* @param description the description of the operation
|
||||
* @param parameters the parameters of the operation
|
||||
*/
|
||||
public JmxEndpointOperation(EndpointOperationType type, OperationInvoker invoker,
|
||||
String operationName, Class<?> outputType, String description,
|
||||
List<JmxEndpointOperationParameterInfo> parameters) {
|
||||
super(type, invoker, true);
|
||||
this.operationName = operationName;
|
||||
this.outputType = outputType;
|
||||
this.description = description;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the operation.
|
||||
* @return the operation name
|
||||
*/
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the output of the operation.
|
||||
* @return the output type
|
||||
*/
|
||||
public Class<?> getOutputType() {
|
||||
return this.outputType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the operation.
|
||||
* @return the operation description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters of the operation.
|
||||
* @return the operation parameters
|
||||
*/
|
||||
public List<JmxEndpointOperationParameterInfo> getParameters() {
|
||||
return Collections.unmodifiableList(this.parameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
/**
|
||||
* Describes the parameters of an operation on a JMX endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointOperationParameterInfo {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
private final String description;
|
||||
|
||||
public JmxEndpointOperationParameterInfo(String name, Class<?> type,
|
||||
String description) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the description of the parameter or {@code null} if none is available.
|
||||
* @return the description or {@code null}
|
||||
*/
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
/**
|
||||
* A {@code JmxOperationResponseMapper} maps an operation's response to a JMX-friendly
|
||||
* form.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface JmxOperationResponseMapper {
|
||||
|
||||
/**
|
||||
* Map the operation's response so that it can be consumed by a JMX compliant client.
|
||||
* @param response the operation's response
|
||||
* @return the {@code response}, in a JMX compliant format
|
||||
*/
|
||||
Object mapResponse(Object response);
|
||||
|
||||
/**
|
||||
* Map the response type to its JMX compliant counterpart.
|
||||
* @param responseType the operation's response type
|
||||
* @return the JMX compliant type
|
||||
*/
|
||||
Class<?> mapResponseType(Class<?> responseType);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JMX endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint.jmx;
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBeanInfoAssembler}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanInfoAssemblerTests {
|
||||
|
||||
private final EndpointMBeanInfoAssembler mBeanInfoAssembler = new EndpointMBeanInfoAssembler(
|
||||
new DummyOperationResponseMapper());
|
||||
|
||||
@Test
|
||||
public void exposeSimpleReadOperation() {
|
||||
JmxEndpointOperation operation = new JmxEndpointOperation(
|
||||
EndpointOperationType.READ, new DummyOperationInvoker(), "getAll",
|
||||
Object.class, "Test operation", Collections.emptyList());
|
||||
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("test", true,
|
||||
Collections.singletonList(operation));
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
|
||||
.createEndpointMBeanInfo(endpoint);
|
||||
assertThat(endpointMBeanInfo).isNotNull();
|
||||
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("test");
|
||||
assertThat(endpointMBeanInfo.getOperations())
|
||||
.containsOnly(entry("getAll", operation));
|
||||
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
|
||||
assertThat(mbeanInfo).isNotNull();
|
||||
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
|
||||
assertThat(mbeanInfo.getDescription())
|
||||
.isEqualTo("MBean operations for endpoint test");
|
||||
assertThat(mbeanInfo.getAttributes()).isEmpty();
|
||||
assertThat(mbeanInfo.getNotifications()).isEmpty();
|
||||
assertThat(mbeanInfo.getConstructors()).isEmpty();
|
||||
assertThat(mbeanInfo.getOperations()).hasSize(1);
|
||||
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
|
||||
assertThat(mBeanOperationInfo.getName()).isEqualTo("getAll");
|
||||
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
|
||||
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.INFO);
|
||||
assertThat(mBeanOperationInfo.getSignature()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposeSimpleWriteOperation() {
|
||||
JmxEndpointOperation operation = new JmxEndpointOperation(
|
||||
EndpointOperationType.WRITE, new DummyOperationInvoker(), "update",
|
||||
Object.class, "Update operation",
|
||||
Collections.singletonList(new JmxEndpointOperationParameterInfo("test",
|
||||
String.class, "Test argument")));
|
||||
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("another", true,
|
||||
Collections.singletonList(operation));
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
|
||||
.createEndpointMBeanInfo(endpoint);
|
||||
assertThat(endpointMBeanInfo).isNotNull();
|
||||
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("another");
|
||||
assertThat(endpointMBeanInfo.getOperations())
|
||||
.containsOnly(entry("update", operation));
|
||||
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
|
||||
assertThat(mbeanInfo).isNotNull();
|
||||
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
|
||||
assertThat(mbeanInfo.getDescription())
|
||||
.isEqualTo("MBean operations for endpoint another");
|
||||
assertThat(mbeanInfo.getAttributes()).isEmpty();
|
||||
assertThat(mbeanInfo.getNotifications()).isEmpty();
|
||||
assertThat(mbeanInfo.getConstructors()).isEmpty();
|
||||
assertThat(mbeanInfo.getOperations()).hasSize(1);
|
||||
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
|
||||
assertThat(mBeanOperationInfo.getName()).isEqualTo("update");
|
||||
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
|
||||
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.ACTION);
|
||||
assertThat(mBeanOperationInfo.getSignature()).hasSize(1);
|
||||
MBeanParameterInfo mBeanParameterInfo = mBeanOperationInfo.getSignature()[0];
|
||||
assertThat(mBeanParameterInfo.getName()).isEqualTo("test");
|
||||
assertThat(mBeanParameterInfo.getType()).isEqualTo(String.class.getName());
|
||||
assertThat(mBeanParameterInfo.getDescription()).isEqualTo("Test argument");
|
||||
}
|
||||
|
||||
private static class DummyOperationInvoker implements OperationInvoker {
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DummyOperationResponseMapper
|
||||
implements JmxOperationResponseMapper {
|
||||
|
||||
@Override
|
||||
public Object mapResponse(Object response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> mapResponseType(Class<?> responseType) {
|
||||
return responseType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanRegistrationException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.jmx.JmxException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBeanRegistrar}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanRegistrarTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private MBeanServer mBeanServer = mock(MBeanServer.class);
|
||||
|
||||
@Test
|
||||
public void mBeanServerMustNotBeNull() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new EndpointMBeanRegistrar(null, (e) -> new ObjectName("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectNameFactoryMustNotBeNull() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new EndpointMBeanRegistrar(this.mBeanServer, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMustNotBeNull() {
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
(e) -> new ObjectName("foo"));
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Endpoint must not be null");
|
||||
registrar.registerEndpointMBean(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointInvokesObjectNameFactory()
|
||||
throws MalformedObjectNameException {
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
given(factory.generate(endpointMBean)).willReturn(objectName);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
ObjectName actualObjectName = registrar.registerEndpointMBean(endpointMBean);
|
||||
assertThat(actualObjectName).isSameAs(objectName);
|
||||
verify(factory).generate(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointInvalidObjectName() throws MalformedObjectNameException {
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
given(endpointMBean.getEndpointId()).willReturn("test");
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
given(factory.generate(endpointMBean))
|
||||
.willThrow(new MalformedObjectNameException());
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid ObjectName for endpoint with id 'test'");
|
||||
registrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointFailure() throws Exception {
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
given(endpointMBean.getEndpointId()).willReturn("test");
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
given(factory.generate(endpointMBean)).willReturn(objectName);
|
||||
given(this.mBeanServer.registerMBean(endpointMBean, objectName))
|
||||
.willThrow(MBeanRegistrationException.class);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
this.thrown.expect(JmxException.class);
|
||||
this.thrown.expectMessage("Failed to register MBean for endpoint with id 'test'");
|
||||
registrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterEndpoint() throws Exception {
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
mock(EndpointObjectNameFactory.class));
|
||||
assertThat(registrar.unregisterEndpointMbean(objectName)).isTrue();
|
||||
verify(this.mBeanServer).unregisterMBean(objectName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterUnknownEndpoint() throws Exception {
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
willThrow(InstanceNotFoundException.class).given(this.mBeanServer)
|
||||
.unregisterMBean(objectName);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
mock(EndpointObjectNameFactory.class));
|
||||
assertThat(registrar.unregisterEndpointMbean(objectName)).isFalse();
|
||||
verify(this.mBeanServer).unregisterMBean(objectName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.management.Attribute;
|
||||
import javax.management.AttributeList;
|
||||
import javax.management.AttributeNotFoundException;
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MBeanServerFactory;
|
||||
import javax.management.ObjectName;
|
||||
import javax.management.ReflectionException;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanTests {
|
||||
|
||||
private final JmxEndpointMBeanFactory jmxEndpointMBeanFactory = new JmxEndpointMBeanFactory(
|
||||
new TestJmxOperationResponseMapper());
|
||||
|
||||
private MBeanServer server;
|
||||
|
||||
private EndpointMBeanRegistrar endpointMBeanRegistrar;
|
||||
|
||||
private EndpointObjectNameFactory objectNameFactory = (endpoint) -> new ObjectName(
|
||||
String.format("org.springframework.boot.test:type=Endpoint,name=%s",
|
||||
UUID.randomUUID().toString()));
|
||||
|
||||
@Before
|
||||
public void createMBeanServer() {
|
||||
this.server = MBeanServerFactory.createMBeanServer();
|
||||
this.endpointMBeanRegistrar = new EndpointMBeanRegistrar(this.server,
|
||||
this.objectNameFactory);
|
||||
}
|
||||
|
||||
@After
|
||||
public void disposeMBeanServer() {
|
||||
if (this.server != null) {
|
||||
MBeanServerFactory.releaseMBeanServer(this.server);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeSimpleEndpoint() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
// getAll
|
||||
Object allResponse = this.server.invoke(objectName, "getAll",
|
||||
new Object[0], new String[0]);
|
||||
assertThat(allResponse).isEqualTo("[ONE, TWO]");
|
||||
|
||||
// getOne
|
||||
Object oneResponse = this.server.invoke(objectName, "getOne",
|
||||
new Object[] { "one" }, new String[] { String.class.getName() });
|
||||
assertThat(oneResponse).isEqualTo("ONE");
|
||||
|
||||
// update
|
||||
Object updateResponse = this.server.invoke(objectName, "update",
|
||||
new Object[] { "one", "1" },
|
||||
new String[] { String.class.getName(), String.class.getName() });
|
||||
assertThat(updateResponse).isNull();
|
||||
|
||||
// getOne validation after update
|
||||
Object updatedOneResponse = this.server.invoke(objectName, "getOne",
|
||||
new Object[] { "one" }, new String[] { String.class.getName() });
|
||||
assertThat(updatedOneResponse).isEqualTo("1");
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke method on FooEndpoint", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxTypesAreProperlyMapped() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
MBeanInfo mBeanInfo = this.server.getMBeanInfo(objectName);
|
||||
Map<String, MBeanOperationInfo> operations = mapOperations(mBeanInfo);
|
||||
assertThat(operations).containsOnlyKeys("getAll", "getOne", "update");
|
||||
assertOperation(operations.get("getAll"), String.class,
|
||||
MBeanOperationInfo.INFO, new Class<?>[0]);
|
||||
assertOperation(operations.get("getOne"), String.class,
|
||||
MBeanOperationInfo.INFO, new Class<?>[] { String.class });
|
||||
assertOperation(operations.get("update"), Void.TYPE,
|
||||
MBeanOperationInfo.ACTION,
|
||||
new Class<?>[] { String.class, String.class });
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to retrieve MBeanInfo of FooEndpoint",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOperation(MBeanOperationInfo operation, Class<?> returnType,
|
||||
int impact, Class<?>[] types) {
|
||||
assertThat(operation.getReturnType()).isEqualTo(returnType.getName());
|
||||
assertThat(operation.getImpact()).isEqualTo(impact);
|
||||
MBeanParameterInfo[] signature = operation.getSignature();
|
||||
assertThat(signature).hasSize(types.length);
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
assertThat(signature[i].getType()).isEqualTo(types[0].getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeReactiveOperation() {
|
||||
load(ReactiveEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "reactive");
|
||||
try {
|
||||
Object allResponse = this.server.invoke(objectName, "getInfo",
|
||||
new Object[0], new String[0]);
|
||||
assertThat(allResponse).isInstanceOf(String.class);
|
||||
assertThat(allResponse).isEqualTo("HELLO WORLD");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke getInfo method", ex);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeUnknownOperation() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.invoke(objectName, "doesNotExist", new Object[0],
|
||||
new String[0]);
|
||||
throw new AssertionError(
|
||||
"Should have failed to invoke unknown operation");
|
||||
}
|
||||
catch (ReflectionException ex) {
|
||||
assertThat(ex.getCause()).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(ex.getCause().getMessage()).contains("doesNotExist", "foo");
|
||||
}
|
||||
catch (MBeanException | InstanceNotFoundException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotReadAttribute() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.getAttribute(objectName, "foo");
|
||||
throw new AssertionError("Should have failed to read attribute foo");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
assertThat(ex).isInstanceOf(AttributeNotFoundException.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotWriteAttribute() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.setAttribute(objectName, new Attribute("foo", "bar"));
|
||||
throw new AssertionError("Should have failed to write attribute foo");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
assertThat(ex).isInstanceOf(AttributeNotFoundException.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotReadAttributes() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
AttributeList attributes = this.server.getAttributes(objectName,
|
||||
new String[] { "foo", "bar" });
|
||||
assertThat(attributes).isNotNull();
|
||||
assertThat(attributes).isEmpty();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke getAttributes", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotWriteAttributes() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
AttributeList attributes = new AttributeList();
|
||||
attributes.add(new Attribute("foo", 1));
|
||||
attributes.add(new Attribute("bar", 42));
|
||||
AttributeList attributesSet = this.server.setAttributes(objectName,
|
||||
attributes);
|
||||
assertThat(attributesSet).isNotNull();
|
||||
assertThat(attributesSet).isEmpty();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke setAttributes", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ObjectName registerEndpoint(JmxAnnotationEndpointDiscoverer discoverer,
|
||||
String endpointId) {
|
||||
Collection<EndpointMBean> mBeans = this.jmxEndpointMBeanFactory
|
||||
.createMBeans(discoverer.discoverEndpoints());
|
||||
assertThat(mBeans).hasSize(1);
|
||||
EndpointMBean endpointMBean = mBeans.iterator().next();
|
||||
assertThat(endpointMBean.getEndpointId()).isEqualTo(endpointId);
|
||||
return this.endpointMBeanRegistrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
private Map<String, MBeanOperationInfo> mapOperations(MBeanInfo info) {
|
||||
Map<String, MBeanOperationInfo> operations = new HashMap<>();
|
||||
for (MBeanOperationInfo mBeanOperationInfo : info.getOperations()) {
|
||||
operations.put(mBeanOperationInfo.getName(), mBeanOperationInfo);
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration)) {
|
||||
consumer.accept(new JmxAnnotationEndpointDiscoverer(context,
|
||||
new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance()),
|
||||
(id) -> new CachingConfiguration(0)));
|
||||
}
|
||||
}
|
||||
|
||||
@Endpoint(id = "foo")
|
||||
static class FooEndpoint {
|
||||
|
||||
private final Map<FooName, Foo> all = new LinkedHashMap<>();
|
||||
|
||||
FooEndpoint() {
|
||||
this.all.put(FooName.ONE, new Foo("one"));
|
||||
this.all.put(FooName.TWO, new Foo("two"));
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Collection<Foo> getAll() {
|
||||
return this.all.values();
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Foo getOne(FooName name) {
|
||||
return this.all.get(name);
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(FooName name, String value) {
|
||||
this.all.put(name, new Foo(value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "reactive")
|
||||
static class ReactiveEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Mono<String> getInfo() {
|
||||
return Mono.defer(() -> Mono.just("Hello World"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum FooName {
|
||||
|
||||
ONE, TWO, THREE
|
||||
|
||||
}
|
||||
|
||||
static class Foo {
|
||||
|
||||
private final String name;
|
||||
|
||||
Foo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestJmxOperationResponseMapper
|
||||
implements JmxOperationResponseMapper {
|
||||
|
||||
@Override
|
||||
public Object mapResponse(Object response) {
|
||||
return (response != null ? response.toString().toUpperCase() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> mapResponseType(Class<?> responseType) {
|
||||
if (responseType == Void.TYPE) {
|
||||
return Void.TYPE;
|
||||
}
|
||||
return String.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
* Copyright 2012-2017 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.boot.endpoint.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.CachingOperationInvoker;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointType;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JmxAnnotationEndpointDiscoverer}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class JmxAnnotationEndpointDiscovererTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void discoveryWorksWhenThereAreNoEndpoints() {
|
||||
load(EmptyConfiguration.class,
|
||||
(discoverer) -> assertThat(discoverer.discoverEndpoints()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardEndpointIsDiscovered() {
|
||||
load(TestEndpoint.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getDescription())
|
||||
.isEqualTo("Invoke getAll for endpoint test");
|
||||
assertThat(getAll.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAll.getParameters()).isEmpty();
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(ReflectiveOperationInvoker.class);
|
||||
JmxEndpointOperation getSomething = operationByName.get("getSomething");
|
||||
assertThat(getSomething.getDescription())
|
||||
.isEqualTo("Invoke getSomething for endpoint test");
|
||||
assertThat(getSomething.getOutputType()).isEqualTo(String.class);
|
||||
assertThat(getSomething.getParameters()).hasSize(1);
|
||||
hasDefaultParameter(getSomething, 0, String.class);
|
||||
JmxEndpointOperation update = operationByName.get("update");
|
||||
assertThat(update.getDescription())
|
||||
.isEqualTo("Invoke update for endpoint test");
|
||||
assertThat(update.getOutputType()).isEqualTo(Void.TYPE);
|
||||
assertThat(update.getParameters()).hasSize(2);
|
||||
hasDefaultParameter(update, 0, String.class);
|
||||
hasDefaultParameter(update, 1, String.class);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyJmxEndpointsAreDiscovered() {
|
||||
load(MultipleEndpointsConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test", "jmx");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxExtensionMustHaveEndpoint() {
|
||||
load(TestJmxEndpointExtension.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(TestJmxEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage("no endpoint found");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxEndpointOverridesStandardEndpoint() {
|
||||
load(OverriddenOperationJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
assertJmxTestEndpoint(endpoints.get("test"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxEndpointAddsExtraOperation() {
|
||||
load(AdditionalOperationJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update", "getAnother");
|
||||
JmxEndpointOperation getAnother = operationByName.get("getAnother");
|
||||
assertThat(getAnother.getDescription()).isEqualTo("Get another thing");
|
||||
assertThat(getAnother.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAnother.getParameters()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsCachedWithMatchingId() {
|
||||
load(TestEndpoint.class, (id) -> new CachingConfiguration(500), (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAll.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extraReadOperationsAreCached() {
|
||||
load(AdditionalOperationJmxEndpointConfiguration.class,
|
||||
(id) -> new CachingConfiguration(500), (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update", "getAnother");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAll.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
JmxEndpointOperation getAnother = operationByName.get("getAnother");
|
||||
assertThat(getAnother.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(
|
||||
((CachingOperationInvoker) getAnother.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoExtensionsHaveTheSameEndpointType() {
|
||||
load(ClashingJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two extensions for the same endpoint");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
this.thrown.expectMessage(TestJmxEndpointExtension.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoStandardEndpointsHaveTheSameId() {
|
||||
load(ClashingStandardEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two endpoints with the id 'test': ");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenEndpointHasTwoOperationsWithTheSameName() {
|
||||
load(ClashingOperationsEndpoint.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found multiple JMX operations with the same name");
|
||||
this.thrown.expectMessage("getAll");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenExtensionHasTwoOperationsWithTheSameName() {
|
||||
load(AdditionalClashingOperationsConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found multiple JMX operations with the same name");
|
||||
this.thrown.expectMessage("getAll");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenExtensionIsNotCompatibleWithTheEndpointType() {
|
||||
load(InvalidJmxExtensionConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(NonJmxJmxEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage(NonJmxEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
private void assertJmxTestEndpoint(EndpointInfo<JmxEndpointOperation> endpoint) {
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoint.getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getDescription()).isEqualTo("Get all the things");
|
||||
assertThat(getAll.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAll.getParameters()).isEmpty();
|
||||
JmxEndpointOperation getSomething = operationByName.get("getSomething");
|
||||
assertThat(getSomething.getDescription())
|
||||
.isEqualTo("Get something based on a timeUnit");
|
||||
assertThat(getSomething.getOutputType()).isEqualTo(String.class);
|
||||
assertThat(getSomething.getParameters()).hasSize(1);
|
||||
hasDocumentedParameter(getSomething, 0, "unitMs", Long.class,
|
||||
"Number of milliseconds");
|
||||
JmxEndpointOperation update = operationByName.get("update");
|
||||
assertThat(update.getDescription()).isEqualTo("Update something based on bar");
|
||||
assertThat(update.getOutputType()).isEqualTo(Void.TYPE);
|
||||
assertThat(update.getParameters()).hasSize(2);
|
||||
hasDocumentedParameter(update, 0, "foo", String.class, "Foo identifier");
|
||||
hasDocumentedParameter(update, 1, "bar", String.class, "Bar value");
|
||||
}
|
||||
|
||||
private void hasDefaultParameter(JmxEndpointOperation operation, int index,
|
||||
Class<?> type) {
|
||||
assertThat(index).isLessThan(operation.getParameters().size());
|
||||
JmxEndpointOperationParameterInfo parameter = operation.getParameters()
|
||||
.get(index);
|
||||
assertThat(parameter.getType()).isEqualTo(type);
|
||||
assertThat(parameter.getDescription()).isNull();
|
||||
}
|
||||
|
||||
private void hasDocumentedParameter(JmxEndpointOperation operation, int index,
|
||||
String name, Class<?> type, String description) {
|
||||
assertThat(index).isLessThan(operation.getParameters().size());
|
||||
JmxEndpointOperationParameterInfo parameter = operation.getParameters()
|
||||
.get(index);
|
||||
assertThat(parameter.getName()).isEqualTo(name);
|
||||
assertThat(parameter.getType()).isEqualTo(type);
|
||||
assertThat(parameter.getDescription()).isEqualTo(description);
|
||||
}
|
||||
|
||||
private Map<String, EndpointInfo<JmxEndpointOperation>> discover(
|
||||
JmxAnnotationEndpointDiscoverer discoverer) {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpointsById = new HashMap<>();
|
||||
discoverer.discoverEndpoints()
|
||||
.forEach((endpoint) -> endpointsById.put(endpoint.getId(), endpoint));
|
||||
return endpointsById;
|
||||
}
|
||||
|
||||
private Map<String, JmxEndpointOperation> mapOperations(
|
||||
Collection<JmxEndpointOperation> operations) {
|
||||
Map<String, JmxEndpointOperation> operationByName = new HashMap<>();
|
||||
operations.forEach((operation) -> operationByName
|
||||
.put(operation.getOperationName(), operation));
|
||||
return operationByName;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
load(configuration, (id) -> null, consumer);
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration)) {
|
||||
consumer.accept(new JmxAnnotationEndpointDiscoverer(context,
|
||||
new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance()),
|
||||
cachingConfigurationFactory));
|
||||
}
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
private static class TestEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public String getSomething(TimeUnit timeUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "jmx", types = EndpointType.JMX)
|
||||
private static class TestJmxEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
private static class TestJmxEndpointExtension {
|
||||
|
||||
@ManagedOperation(description = "Get all the things")
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
@ManagedOperation(description = "Get something based on a timeUnit")
|
||||
@ManagedOperationParameters({
|
||||
@ManagedOperationParameter(name = "unitMs", description = "Number of milliseconds") })
|
||||
public String getSomething(Long timeUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
@ManagedOperation(description = "Update something based on bar")
|
||||
@ManagedOperationParameters({
|
||||
@ManagedOperationParameter(name = "foo", description = "Foo identifier"),
|
||||
@ManagedOperationParameter(name = "bar", description = "Bar value") })
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
private static class AdditionalOperationJmxEndpointExtension {
|
||||
|
||||
@ManagedOperation(description = "Get another thing")
|
||||
@ReadOperation
|
||||
public Object getAnother() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class ClashingOperationsEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class ClashingOperationsJmxEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nonjmx", types = EndpointType.WEB)
|
||||
private static class NonJmxEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = NonJmxEndpoint.class)
|
||||
private static class NonJmxJmxEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getSomething() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MultipleEndpointsConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpoint testJmxEndpoint() {
|
||||
return new TestJmxEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonJmxEndpoint nonJmxEndpoint() {
|
||||
return new NonJmxEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class OverriddenOperationJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testJmxEndpointExtension() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AdditionalOperationJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AdditionalOperationJmxEndpointExtension additionalOperationJmxEndpointExtension() {
|
||||
return new AdditionalOperationJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AdditionalClashingOperationsConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClashingOperationsJmxEndpointExtension clashingOperationsJmxEndpointExtension() {
|
||||
return new ClashingOperationsJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testExtensionOne() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testExtensionTwo() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingStandardEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointTwo() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointOne() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class InvalidJmxExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public NonJmxEndpoint nonJmxEndpoint() {
|
||||
return new NonJmxEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonJmxJmxEndpointExtension nonJmxJmxEndpointExtension() {
|
||||
return new NonJmxJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user