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:
Stephane Nicoll
2017-07-20 16:33:19 +01:00
committed by Andy Wilkinson
parent 4f2e4ada45
commit 4592e071db
16 changed files with 2164 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}