Introduce the foundations of the new endpoint infrastructure
This commit introduces a new annotation-based programming model for implementing endpoints. An endpoint is identified by a type-level `@Endpoint` annotation. Method-level `@ReadOperation` and `@WriteOperation` are used to identify methods on an endpoint class that implement an operation and should be exposed (via JMX for exmaple) to remote clients. See gh-9946
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A base {@link EndpointDiscoverer} implementation that discovers {@link Endpoint} beans
|
||||
* in an application context.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @param <K> the type of the operation key
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public abstract class AnnotationEndpointDiscoverer<T extends EndpointOperation, K>
|
||||
implements EndpointDiscoverer<T> {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final EndpointOperationFactory<T> operationFactory;
|
||||
|
||||
private final Function<T, K> operationKeyFactory;
|
||||
|
||||
private final Function<String, CachingConfiguration> cachingConfigurationFactory;
|
||||
|
||||
protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
EndpointOperationFactory<T> operationFactory,
|
||||
Function<T, K> operationKeyFactory,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.operationFactory = operationFactory;
|
||||
this.operationKeyFactory = operationKeyFactory;
|
||||
this.cachingConfigurationFactory = cachingConfigurationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform endpoint discovery, including discovery and merging of extensions.
|
||||
* @param extensionType the annotation type of the extension
|
||||
* @param endpointType the {@link EndpointType} that should be considered
|
||||
* @return the list of {@link EndpointInfo EndpointInfos} that describes the
|
||||
* discovered endpoints matching the specified {@link EndpointType}
|
||||
*/
|
||||
protected Collection<EndpointInfoDescriptor<T, K>> discoverEndpointsWithExtension(
|
||||
Class<? extends Annotation> extensionType, EndpointType endpointType) {
|
||||
Map<Class<?>, EndpointInfo<T>> endpoints = discoverGenericEndpoints(endpointType);
|
||||
Map<Class<?>, EndpointExtensionInfo<T>> extensions = discoverExtensions(endpoints,
|
||||
extensionType, endpointType);
|
||||
Collection<EndpointInfoDescriptor<T, K>> result = new ArrayList<>();
|
||||
endpoints.forEach((endpointClass, endpointInfo) -> {
|
||||
EndpointExtensionInfo<T> extension = extensions.remove(endpointClass);
|
||||
result.add(createDescriptor(endpointClass, endpointInfo, extension));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private EndpointInfoDescriptor<T, K> createDescriptor(Class<?> endpointType,
|
||||
EndpointInfo<T> endpoint, EndpointExtensionInfo<T> extension) {
|
||||
Map<OperationKey<K>, List<T>> endpointOperations = indexOperations(
|
||||
endpoint.getId(), endpointType, endpoint.getOperations());
|
||||
if (extension != null) {
|
||||
endpointOperations.putAll(indexOperations(endpoint.getId(),
|
||||
extension.getEndpointExtensionType(), extension.getOperations()));
|
||||
return new EndpointInfoDescriptor<>(mergeEndpoint(endpoint, extension),
|
||||
endpointOperations);
|
||||
}
|
||||
else {
|
||||
return new EndpointInfoDescriptor<>(endpoint, endpointOperations);
|
||||
}
|
||||
}
|
||||
|
||||
private EndpointInfo<T> mergeEndpoint(EndpointInfo<T> endpoint,
|
||||
EndpointExtensionInfo<T> extension) {
|
||||
Map<K, T> operations = new HashMap<>();
|
||||
Consumer<T> operationConsumer = (operation) -> operations
|
||||
.put(this.operationKeyFactory.apply(operation), operation);
|
||||
endpoint.getOperations().forEach(operationConsumer);
|
||||
extension.getOperations().forEach(operationConsumer);
|
||||
return new EndpointInfo<>(endpoint.getId(), endpoint.isEnabledByDefault(),
|
||||
operations.values());
|
||||
}
|
||||
|
||||
private Map<OperationKey<K>, List<T>> indexOperations(String endpointId,
|
||||
Class<?> target, Collection<T> operations) {
|
||||
LinkedMultiValueMap<OperationKey<K>, T> operationByKey = new LinkedMultiValueMap<>();
|
||||
operations
|
||||
.forEach(
|
||||
(operation) -> operationByKey.add(
|
||||
new OperationKey<>(endpointId, target,
|
||||
this.operationKeyFactory.apply(operation)),
|
||||
operation));
|
||||
return operationByKey;
|
||||
}
|
||||
|
||||
private Map<Class<?>, EndpointInfo<T>> discoverGenericEndpoints(
|
||||
EndpointType endpointType) {
|
||||
String[] endpointBeanNames = this.applicationContext
|
||||
.getBeanNamesForAnnotation(Endpoint.class);
|
||||
Map<String, EndpointInfo<T>> endpointsById = new HashMap<>();
|
||||
Map<Class<?>, EndpointInfo<T>> endpointsByClass = new HashMap<>();
|
||||
for (String beanName : endpointBeanNames) {
|
||||
Class<?> beanType = this.applicationContext.getType(beanName);
|
||||
AnnotationAttributes endpointAttributes = AnnotatedElementUtils
|
||||
.findMergedAnnotationAttributes(beanType, Endpoint.class, true, true);
|
||||
String endpointId = endpointAttributes.getString("id");
|
||||
if (matchEndpointType(endpointAttributes, endpointType)) {
|
||||
EndpointInfo<T> endpointInfo = createEndpointInfo(endpointsById, beanName,
|
||||
beanType, endpointAttributes, endpointId);
|
||||
endpointsByClass.put(beanType, endpointInfo);
|
||||
}
|
||||
}
|
||||
return endpointsByClass;
|
||||
}
|
||||
|
||||
private EndpointInfo<T> createEndpointInfo(Map<String, EndpointInfo<T>> endpointsById,
|
||||
String beanName, Class<?> beanType, AnnotationAttributes endpointAttributes,
|
||||
String endpointId) {
|
||||
Map<Method, T> operationMethods = discoverOperations(endpointId, beanName,
|
||||
beanType);
|
||||
EndpointInfo<T> endpointInfo = new EndpointInfo<>(endpointId,
|
||||
endpointAttributes.getBoolean("enabledByDefault"),
|
||||
operationMethods.values());
|
||||
EndpointInfo<T> previous = endpointsById.putIfAbsent(endpointInfo.getId(),
|
||||
endpointInfo);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Found two endpoints with the id '"
|
||||
+ endpointInfo.getId() + "': " + endpointInfo + " and " + previous);
|
||||
}
|
||||
return endpointInfo;
|
||||
}
|
||||
|
||||
private Map<Class<?>, EndpointExtensionInfo<T>> discoverExtensions(
|
||||
Map<Class<?>, EndpointInfo<T>> endpoints,
|
||||
Class<? extends Annotation> extensionType, EndpointType endpointType) {
|
||||
if (extensionType == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
String[] extensionBeanNames = this.applicationContext
|
||||
.getBeanNamesForAnnotation(extensionType);
|
||||
Map<Class<?>, EndpointExtensionInfo<T>> extensionsByEndpoint = new HashMap<>();
|
||||
for (String beanName : extensionBeanNames) {
|
||||
Class<?> beanType = this.applicationContext.getType(beanName);
|
||||
AnnotationAttributes extensionAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(beanType, extensionType);
|
||||
Class<?> endpointClass = (Class<?>) extensionAttributes.get("endpoint");
|
||||
AnnotationAttributes endpointAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(endpointClass, Endpoint.class);
|
||||
if (!matchEndpointType(endpointAttributes, endpointType)) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Invalid extension %s': "
|
||||
+ "endpoint '%s' does not support such extension",
|
||||
beanType.getName(), endpointClass.getName()));
|
||||
}
|
||||
EndpointInfo<T> endpoint = endpoints.get(endpointClass);
|
||||
if (endpoint == null) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Invalid extension '%s': no endpoint found with type '%s'",
|
||||
beanType.getName(), endpointClass.getName()));
|
||||
}
|
||||
Map<Method, T> operationMethods = discoverOperations(endpoint.getId(),
|
||||
beanName, beanType);
|
||||
EndpointExtensionInfo<T> extension = new EndpointExtensionInfo<>(beanType,
|
||||
operationMethods.values());
|
||||
EndpointExtensionInfo<T> previous = extensionsByEndpoint
|
||||
.putIfAbsent(endpointClass, extension);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException(
|
||||
"Found two extensions for the same endpoint '"
|
||||
+ endpointClass.getName() + "': "
|
||||
+ extension.getEndpointExtensionType().getName() + " and "
|
||||
+ previous.getEndpointExtensionType().getName());
|
||||
}
|
||||
}
|
||||
return extensionsByEndpoint;
|
||||
}
|
||||
|
||||
private boolean matchEndpointType(AnnotationAttributes endpointAttributes,
|
||||
EndpointType endpointType) {
|
||||
if (endpointType == null) {
|
||||
return true;
|
||||
}
|
||||
Object types = endpointAttributes.get("types");
|
||||
if (ObjectUtils.isEmpty(types)) {
|
||||
return true;
|
||||
}
|
||||
return Arrays.stream((EndpointType[]) types)
|
||||
.anyMatch((t) -> t.equals(endpointType));
|
||||
}
|
||||
|
||||
private Map<Method, T> discoverOperations(String endpointId, String beanName,
|
||||
Class<?> beanType) {
|
||||
return MethodIntrospector.selectMethods(beanType,
|
||||
(MethodIntrospector.MetadataLookup<T>) (
|
||||
method) -> createOperationIfPossible(endpointId, beanName,
|
||||
method));
|
||||
}
|
||||
|
||||
private T createOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
T readOperation = createReadOperationIfPossible(endpointId, beanName, method);
|
||||
return readOperation != null ? readOperation
|
||||
: createWriteOperationIfPossible(endpointId, beanName, method);
|
||||
}
|
||||
|
||||
private T createReadOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
return createOperationIfPossible(endpointId, beanName, method,
|
||||
ReadOperation.class, EndpointOperationType.READ);
|
||||
}
|
||||
|
||||
private T createWriteOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
return createOperationIfPossible(endpointId, beanName, method,
|
||||
WriteOperation.class, EndpointOperationType.WRITE);
|
||||
}
|
||||
|
||||
private T createOperationIfPossible(String endpointId, String beanName, Method method,
|
||||
Class<? extends Annotation> operationAnnotation,
|
||||
EndpointOperationType operationType) {
|
||||
AnnotationAttributes operationAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(method, operationAnnotation);
|
||||
if (operationAttributes == null) {
|
||||
return null;
|
||||
}
|
||||
CachingConfiguration cachingConfiguration = this.cachingConfigurationFactory
|
||||
.apply(endpointId);
|
||||
return this.operationFactory.createOperation(endpointId, operationAttributes,
|
||||
this.applicationContext.getBean(beanName), method, operationType,
|
||||
determineTimeToLive(cachingConfiguration, operationType, method));
|
||||
}
|
||||
|
||||
private long determineTimeToLive(CachingConfiguration cachingConfiguration,
|
||||
EndpointOperationType operationType, Method method) {
|
||||
if (cachingConfiguration != null && cachingConfiguration.getTimeToLive() > 0
|
||||
&& operationType == EndpointOperationType.READ
|
||||
&& method.getParameters().length == 0) {
|
||||
return cachingConfiguration.getTimeToLive();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EndpointOperationFactory} creates an {@link EndpointOperation} for an
|
||||
* operation on an endpoint.
|
||||
*
|
||||
* @param <T> the {@link EndpointOperation} type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
protected interface EndpointOperationFactory<T extends EndpointOperation> {
|
||||
|
||||
/**
|
||||
* Creates an {@code EndpointOperation} for an operation on an endpoint.
|
||||
* @param endpointId the id of the endpoint
|
||||
* @param operationAttributes the annotation attributes for the operation
|
||||
* @param target the target that implements the operation
|
||||
* @param operationMethod the method on the bean that implements the operation
|
||||
* @param operationType the type of the operation
|
||||
* @param timeToLive the caching period in milliseconds
|
||||
* @return the operation info that describes the operation
|
||||
*/
|
||||
T createOperation(String endpointId, AnnotationAttributes operationAttributes,
|
||||
Object target, Method operationMethod,
|
||||
EndpointOperationType operationType, long timeToLive);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a tech-specific extension of an endpoint.
|
||||
* @param <T> the type of the operation
|
||||
*/
|
||||
private static final class EndpointExtensionInfo<T extends EndpointOperation> {
|
||||
|
||||
private final Class<?> endpointExtensionType;
|
||||
|
||||
private final Collection<T> operations;
|
||||
|
||||
private EndpointExtensionInfo(Class<?> endpointExtensionType,
|
||||
Collection<T> operations) {
|
||||
this.endpointExtensionType = endpointExtensionType;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
private Class<?> getEndpointExtensionType() {
|
||||
return this.endpointExtensionType;
|
||||
}
|
||||
|
||||
private Collection<T> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an {@link EndpointInfo endpoint} and whether or not it is valid.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @param <K> the type of the operation key
|
||||
*/
|
||||
protected static class EndpointInfoDescriptor<T extends EndpointOperation, K> {
|
||||
|
||||
private final EndpointInfo<T> endpointInfo;
|
||||
|
||||
private final Map<OperationKey<K>, List<T>> operations;
|
||||
|
||||
protected EndpointInfoDescriptor(EndpointInfo<T> endpointInfo,
|
||||
Map<OperationKey<K>, List<T>> operations) {
|
||||
this.endpointInfo = endpointInfo;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
public EndpointInfo<T> getEndpointInfo() {
|
||||
return this.endpointInfo;
|
||||
}
|
||||
|
||||
public Map<OperationKey<K>, List<T>> findDuplicateOperations() {
|
||||
Map<OperationKey<K>, List<T>> duplicateOperations = new HashMap<>();
|
||||
this.operations.forEach((k, list) -> {
|
||||
if (list.size() > 1) {
|
||||
duplicateOperations.put(k, list);
|
||||
}
|
||||
});
|
||||
return duplicateOperations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the key of an operation in the context of an operation's implementation.
|
||||
*
|
||||
* @param <K> the type of the key
|
||||
*/
|
||||
protected static final class OperationKey<K> {
|
||||
|
||||
private final String endpointId;
|
||||
|
||||
private final Class<?> endpointType;
|
||||
|
||||
private final K key;
|
||||
|
||||
public OperationKey(String endpointId, Class<?> endpointType, K key) {
|
||||
this.endpointId = endpointId;
|
||||
this.endpointType = endpointType;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OperationKey<?> that = (OperationKey<?>) o;
|
||||
|
||||
if (!this.endpointId.equals(that.endpointId)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.endpointType.equals(that.endpointType)) {
|
||||
return false;
|
||||
}
|
||||
return this.key.equals(that.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.endpointId.hashCode();
|
||||
result = 31 * result + this.endpointType.hashCode();
|
||||
result = 31 * result + this.key.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* The caching configuration of an endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CachingConfiguration {
|
||||
|
||||
private final long timeToLive;
|
||||
|
||||
/**
|
||||
* Create a new instance with the given {@code timeToLive}.
|
||||
* @param timeToLive the time to live of an operation result in milliseconds
|
||||
*/
|
||||
public CachingConfiguration(long timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time to live of a cached operation result.
|
||||
* @return the time to live of an operation result
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OperationInvoker} that caches the response of an operation with a
|
||||
* configurable time to live.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CachingOperationInvoker implements OperationInvoker {
|
||||
|
||||
private final OperationInvoker target;
|
||||
|
||||
private final long timeToLive;
|
||||
|
||||
private volatile CachedResponse cachedResponse;
|
||||
|
||||
/**
|
||||
* Create a new instance with the target {@link OperationInvoker} to use to compute
|
||||
* the response and the time to live for the cache.
|
||||
* @param target the {@link OperationInvoker} this instance wraps
|
||||
* @param timeToLive the maximum time in milliseconds that a response can be cached
|
||||
*/
|
||||
public CachingOperationInvoker(OperationInvoker target, long timeToLive) {
|
||||
Assert.state(timeToLive > 0, "TimeToLive must be strictly positive");
|
||||
this.target = target;
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum time in milliseconds that a response can be cached.
|
||||
* @return the time to live of a response
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
long accessTime = System.currentTimeMillis();
|
||||
CachedResponse cached = this.cachedResponse;
|
||||
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
|
||||
Object response = this.target.invoke(arguments);
|
||||
this.cachedResponse = new CachedResponse(response, accessTime);
|
||||
return response;
|
||||
}
|
||||
return cached.getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* A cached response that encapsulates the response itself and the time at which it
|
||||
* was created.
|
||||
*/
|
||||
static class CachedResponse {
|
||||
|
||||
private final Object response;
|
||||
|
||||
private final long creationTime;
|
||||
|
||||
CachedResponse(Object response, long creationTime) {
|
||||
this.response = response;
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
public boolean isStale(long accessTime, long timeToLive) {
|
||||
return (accessTime - this.creationTime) >= timeToLive;
|
||||
}
|
||||
|
||||
public Object getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
/**
|
||||
* {@link OperationParameterMapper} that uses a {@link ConversionService} to map parameter
|
||||
* values if necessary.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ConversionServiceOperationParameterMapper
|
||||
implements OperationParameterMapper {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
/**
|
||||
* Create a new instance with the {@link ConversionService} to use.
|
||||
* @param conversionService the conversion service
|
||||
*/
|
||||
public ConversionServiceOperationParameterMapper(
|
||||
ConversionService conversionService) {
|
||||
this.conversionService = new BinderConversionService(conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T mapParameter(Object input, Class<T> parameterType) {
|
||||
if (input == null || parameterType.isAssignableFrom(input.getClass())) {
|
||||
return parameterType.cast(input);
|
||||
}
|
||||
try {
|
||||
return this.conversionService.convert(input, parameterType);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ParameterMappingException(input, parameterType, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a type as being an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @see EndpointDiscoverer
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Endpoint {
|
||||
|
||||
/**
|
||||
* The id of the endpoint.
|
||||
* @return the id
|
||||
*/
|
||||
String id();
|
||||
|
||||
/**
|
||||
* Defines the endpoint {@link EndpointType types} that should be exposed. By default,
|
||||
* all types are exposed.
|
||||
* @return the endpoint types to expose
|
||||
*/
|
||||
EndpointType[] types() default {};
|
||||
|
||||
/**
|
||||
* Whether or not the endpoint is enabled by default.
|
||||
* @return {@code true} if the endpoint is enabled by default, otherwise {@code false}
|
||||
*/
|
||||
boolean enabledByDefault() default true;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Discovers endpoints and provides an {@link EndpointInfo} for each of them.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EndpointDiscoverer<T extends EndpointOperation> {
|
||||
|
||||
/**
|
||||
* Perform endpoint discovery.
|
||||
* @return the discovered endpoints
|
||||
*/
|
||||
Collection<EndpointInfo<T>> discoverEndpoints();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Information describing an endpoint.
|
||||
*
|
||||
* @param <T> the type of the endpoint's operations
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class EndpointInfo<T extends EndpointOperation> {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final boolean enabledByDefault;
|
||||
|
||||
private final Collection<T> operations;
|
||||
|
||||
/**
|
||||
* Creates a new {@code EndpointInfo} describing an endpoint with the given {@code id}
|
||||
* and {@code operations}.
|
||||
* @param id the id of the endpoint
|
||||
* @param enabledByDefault whether or not the endpoint is enabled by default
|
||||
* @param operations the operations of the endpoint
|
||||
*/
|
||||
public EndpointInfo(String id, boolean enabledByDefault, Collection<T> operations) {
|
||||
this.id = id;
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the endpoint.
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this endpoint is enabled by default.
|
||||
* @return {@code true} if it is enabled by default, otherwise {@code false}
|
||||
*/
|
||||
public boolean isEnabledByDefault() {
|
||||
return this.enabledByDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operations of the endpoint.
|
||||
* @return the operations
|
||||
*/
|
||||
public Collection<T> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An operation on an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class EndpointOperation {
|
||||
|
||||
private final EndpointOperationType type;
|
||||
|
||||
private final OperationInvoker operationInvoker;
|
||||
|
||||
private final boolean blocking;
|
||||
|
||||
/**
|
||||
* Creates a new {@code EndpointOperation} for an operation of the given {@code type}.
|
||||
* The operation can be performed using the given {@code operationInvoker}.
|
||||
* @param type the type of the operation
|
||||
* @param operationInvoker used to perform the operation
|
||||
* @param blocking whether or not this is a blocking operation
|
||||
*/
|
||||
public EndpointOperation(EndpointOperationType type,
|
||||
OperationInvoker operationInvoker, boolean blocking) {
|
||||
this.type = type;
|
||||
this.operationInvoker = operationInvoker;
|
||||
this.blocking = blocking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link EndpointOperationType type} of the operation.
|
||||
* @return the type
|
||||
*/
|
||||
public EndpointOperationType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code OperationInvoker} that can be used to invoke this endpoint
|
||||
* operation.
|
||||
* @return the operation invoker
|
||||
*/
|
||||
public OperationInvoker getOperationInvoker() {
|
||||
return this.operationInvoker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this is a blocking operation.
|
||||
*
|
||||
* @return {@code true} if it is a blocking operation, otherwise {@code false}.
|
||||
*/
|
||||
public boolean isBlocking() {
|
||||
return this.blocking;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An enumeration of the different types of operation supported by an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum EndpointOperationType {
|
||||
|
||||
/**
|
||||
* A read operation.
|
||||
*/
|
||||
READ,
|
||||
|
||||
/**
|
||||
* A write operation.
|
||||
*/
|
||||
WRITE
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An enumeration of the available {@link Endpoint} types.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum EndpointType {
|
||||
|
||||
/**
|
||||
* Expose the endpoint as a JMX MBean.
|
||||
*/
|
||||
JMX,
|
||||
|
||||
/**
|
||||
* Expose the endpoint as a Web endpoint.
|
||||
*/
|
||||
WEB
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@code OperationInvoker} is used to invoke an operation on an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperationInvoker {
|
||||
|
||||
/**
|
||||
* Invoke the underlying operation using the given {@code arguments}.
|
||||
* @param arguments the arguments to pass to the operation
|
||||
* @return the result of the operation, may be {@code null}
|
||||
*/
|
||||
Object invoke(Map<String, Object> arguments);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* An {@code OperationParameterMapper} is used to map parameters to the required type when
|
||||
* invoking an endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperationParameterMapper {
|
||||
|
||||
/**
|
||||
* Map the specified {@code input} parameter to the given {@code parameterType}.
|
||||
* @param input a parameter value
|
||||
* @param parameterType the required type of the parameter
|
||||
* @return a value suitable for that parameter
|
||||
* @param <T> the actual type of the parameter
|
||||
* @throws ParameterMappingException when a mapping failure occurs
|
||||
*/
|
||||
<T> T mapParameter(Object input, Class<T> parameterType);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A {@code ParameterMappingException} is thrown when a failure occurs during
|
||||
* {@link OperationParameterMapper#mapParameter(Object, Class) operation parameter
|
||||
* mapping}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ParameterMappingException extends RuntimeException {
|
||||
|
||||
private final Object input;
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ParameterMappingException} for a failure that occurred when
|
||||
* trying to map the given {@code input} to the given {@code type}.
|
||||
*
|
||||
* @param input the input that was being mapped
|
||||
* @param type the type that was being mapped to
|
||||
* @param cause the cause of the mapping failure
|
||||
*/
|
||||
public ParameterMappingException(Object input, Class<?> type, Throwable cause) {
|
||||
super("Failed to map " + input + " of type " + input.getClass() + " to type "
|
||||
+ type, cause);
|
||||
this.input = input;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the input that was to be mapped.
|
||||
*
|
||||
* @return the input
|
||||
*/
|
||||
public Object getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type to be mapped to.
|
||||
*
|
||||
* @return the type
|
||||
*/
|
||||
public Class<?> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a method on an {@link Endpoint} as being a read operation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ReadOperation {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* An {@code OperationInvoker} that invokes an operation using reflection.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ReflectiveOperationInvoker implements OperationInvoker {
|
||||
|
||||
private final OperationParameterMapper parameterMapper;
|
||||
|
||||
private final Object target;
|
||||
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates a new {code ReflectiveOperationInvoker} that will invoke the given
|
||||
* {@code method} on the given {@code target}. The given {@code parameterMapper} will
|
||||
* be used to map parameters to the required types.
|
||||
*
|
||||
* @param parameterMapper the parameter mapper
|
||||
* @param target the target of the reflective call
|
||||
* @param method the method to call
|
||||
*/
|
||||
public ReflectiveOperationInvoker(OperationParameterMapper parameterMapper,
|
||||
Object target, Method method) {
|
||||
this.parameterMapper = parameterMapper;
|
||||
this.target = target;
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
return ReflectionUtils.invokeMethod(this.method, this.target,
|
||||
resolveArguments(arguments));
|
||||
}
|
||||
|
||||
private Object[] resolveArguments(Map<String, Object> arguments) {
|
||||
return Stream.of(this.method.getParameters())
|
||||
.map((parameter) -> resolveArgument(parameter, arguments))
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(),
|
||||
(list) -> list.toArray(new Object[list.size()])));
|
||||
}
|
||||
|
||||
private Object resolveArgument(Parameter parameter, Map<String, Object> arguments) {
|
||||
Object resolved = arguments.get(parameter.getName());
|
||||
return this.parameterMapper.mapParameter(resolved, parameter.getType());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A {@code Selector} can be used on a parameter of an {@link Endpoint} method to indicate
|
||||
* that the parameter is used to select a subset of the endpoint's data.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Selector {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a method on an {@link Endpoint} as being a write operation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface WriteOperation {
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint;
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationEndpointDiscoverer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class AnnotationEndpointDiscovererTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void discoverWorksWhenThereAreNoEndpoints() {
|
||||
load(EmptyConfiguration.class,
|
||||
(context) -> assertThat(new TestAnnotationEndpointDiscoverer(context)
|
||||
.discoverEndpoints().isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointIsDiscovered() {
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
assertThat(operations).containsKeys(
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getAll"),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getOne",
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class,
|
||||
String.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subclassedEndpointIsDiscovered() {
|
||||
load(TestEndpointSubclassConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(4);
|
||||
assertThat(operations).containsKeys(
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getAll"),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getOne",
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class,
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpointSubclass.class,
|
||||
"updateWithMoreArguments", String.class, String.class,
|
||||
String.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoEndpointsHaveTheSameId() {
|
||||
load(ClashingEndpointConfiguration.class, (context) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two endpoints with the id 'test': ");
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsNotCachedWithTtlSetToZero() {
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
(endpointId) -> new CachingConfiguration(0))
|
||||
.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
operations.values()
|
||||
.forEach(operation -> assertThat(operation.getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsNotCachedWithNonMatchingId() {
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory = (
|
||||
endpointId) -> (endpointId.equals("foo") ? new CachingConfiguration(500)
|
||||
: new CachingConfiguration(0));
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
cachingConfigurationFactory).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
operations.values()
|
||||
.forEach(operation -> assertThat(operation.getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsCachedWithMatchingId() {
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory = (
|
||||
endpointId) -> (endpointId.equals("test") ? new CachingConfiguration(500)
|
||||
: new CachingConfiguration(0));
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
cachingConfigurationFactory).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
OperationInvoker getAllOperationInvoker = operations
|
||||
.get(ReflectionUtils.findMethod(TestEndpoint.class, "getAll"))
|
||||
.getOperationInvoker();
|
||||
assertThat(getAllOperationInvoker)
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAllOperationInvoker).getTimeToLive())
|
||||
.isEqualTo(500);
|
||||
assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class,
|
||||
"getOne", String.class)).getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class,
|
||||
"update", String.class, String.class)).getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, EndpointInfo<TestEndpointOperation>> mapEndpoints(
|
||||
Collection<EndpointInfo<TestEndpointOperation>> endpoints) {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpointById = new LinkedHashMap<>();
|
||||
endpoints.forEach((endpoint) -> {
|
||||
EndpointInfo<TestEndpointOperation> existing = endpointById
|
||||
.put(endpoint.getId(), endpoint);
|
||||
if (existing != null) {
|
||||
throw new AssertionError(String.format(
|
||||
"Found endpoints with duplicate id '%s'", endpoint.getId()));
|
||||
}
|
||||
});
|
||||
return endpointById;
|
||||
}
|
||||
|
||||
private Map<Method, TestEndpointOperation> mapOperations(
|
||||
EndpointInfo<TestEndpointOperation> endpoint) {
|
||||
Map<Method, TestEndpointOperation> operationByMethod = new HashMap<>();
|
||||
endpoint.getOperations().forEach((operation) -> {
|
||||
EndpointOperation existing = operationByMethod
|
||||
.put(operation.getOperationMethod(), operation);
|
||||
if (existing != null) {
|
||||
throw new AssertionError(String.format(
|
||||
"Found endpoint with duplicate operation method '%s'",
|
||||
operation.getOperationMethod()));
|
||||
}
|
||||
});
|
||||
return operationByMethod;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<AnnotationConfigApplicationContext> consumer) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration);
|
||||
try {
|
||||
consumer.accept(context);
|
||||
}
|
||||
finally {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class TestEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getOne(@Selector String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
public void someOtherMethod() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestEndpointSubclass extends TestEndpoint {
|
||||
|
||||
@WriteOperation
|
||||
public void updateWithMoreArguments(String foo, String bar, String baz) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestEndpointSubclassConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpointSubclass testEndpointSubclass() {
|
||||
return new TestEndpointSubclass();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointTwo() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointOne() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestEndpointOperation extends EndpointOperation {
|
||||
|
||||
private final Method operationMethod;
|
||||
|
||||
private TestEndpointOperation(EndpointOperationType type,
|
||||
OperationInvoker operationInvoker, Method operationMethod) {
|
||||
super(type, operationInvoker, true);
|
||||
this.operationMethod = operationMethod;
|
||||
}
|
||||
|
||||
private Method getOperationMethod() {
|
||||
return this.operationMethod;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestAnnotationEndpointDiscoverer
|
||||
extends AnnotationEndpointDiscoverer<TestEndpointOperation, Method> {
|
||||
|
||||
TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
super(applicationContext, endpointOperationFactory(),
|
||||
TestEndpointOperation::getOperationMethod,
|
||||
cachingConfigurationFactory);
|
||||
}
|
||||
|
||||
TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext) {
|
||||
this(applicationContext, (id) -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EndpointInfo<TestEndpointOperation>> discoverEndpoints() {
|
||||
return discoverEndpointsWithExtension(null, null).stream()
|
||||
.map(EndpointInfoDescriptor::getEndpointInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static EndpointOperationFactory<TestEndpointOperation> endpointOperationFactory() {
|
||||
return new EndpointOperationFactory<TestEndpointOperation>() {
|
||||
|
||||
@Override
|
||||
public TestEndpointOperation createOperation(String endpointId,
|
||||
AnnotationAttributes operationAttributes, Object target,
|
||||
Method operationMethod, EndpointOperationType operationType,
|
||||
long timeToLive) {
|
||||
return new TestEndpointOperation(operationType,
|
||||
createOperationInvoker(timeToLive), operationMethod);
|
||||
}
|
||||
|
||||
private OperationInvoker createOperationInvoker(long timeToLive) {
|
||||
OperationInvoker invoker = (arguments) -> null;
|
||||
if (timeToLive > 0) {
|
||||
return new CachingOperationInvoker(invoker, timeToLive);
|
||||
}
|
||||
else {
|
||||
return invoker;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link CachingOperationInvoker}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class CachingOperationInvokerTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void createInstanceWithTllSetToZero() {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("TimeToLive");
|
||||
new CachingOperationInvoker(mock(OperationInvoker.class), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheInTtlRange() {
|
||||
Object expected = new Object();
|
||||
OperationInvoker target = mock(OperationInvoker.class);
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
given(target.invoke(parameters)).willReturn(expected);
|
||||
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
|
||||
Object response = invoker.invoke(parameters);
|
||||
assertThat(response).isSameAs(expected);
|
||||
verify(target, times(1)).invoke(parameters);
|
||||
Object cachedResponse = invoker.invoke(parameters);
|
||||
assertThat(cachedResponse).isSameAs(response);
|
||||
verifyNoMoreInteractions(target);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void targetInvokedWhenCacheExpires() throws InterruptedException {
|
||||
OperationInvoker target = mock(OperationInvoker.class);
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
given(target.invoke(parameters)).willReturn(new Object());
|
||||
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L);
|
||||
invoker.invoke(parameters);
|
||||
Thread.sleep(55);
|
||||
invoker.invoke(parameters);
|
||||
verify(target, times(2)).invoke(parameters);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user