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:
Andy Wilkinson
2017-07-20 16:30:24 +01:00
parent 7bd285dd93
commit 4f2e4ada45
20 changed files with 1689 additions and 0 deletions

View File

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

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;
/**
* 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;
}
}

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
/**
* 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

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.
*/
/**
* Endpoint support.
*/
package org.springframework.boot.endpoint;

View File

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

View File

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