Overhaul actuator endpoint code

Refactor several areas of the actuator endpoint code in order to make
future extensions easier. The primary goal is to introduce the concept
of an `ExposableEndpoint` that has technology specific subclasses and
can carry additional data for filters to use. Many other changes have
been made along the way including:

* A new EndpointSupplier interface that allows cleaner separation of
  supplying vs discovering endpoints. This allows cleaner class names
  and allows for better auto-configuration since a user can choose to
  provide their own supplier entirely.

* A `DiscoveredEndpoint` interface that allows the `EndpointFilter`
  to be greatly simplified. A filter now doesn't need to know about
  discovery concerns unless absolutely necessary.

* Improved naming and package structure. Many technology specific
  concerns are now grouped in a better way. Related concerns are
  co-located and concepts from one area no longer leakage into another.

* Simplified `HandlerMapping` implementations. Many common concerns have
  been pulled up helping to create simpler subclasses.

* Simplified JMX adapters. Many of the intermediary `Info` classes have
  been removed. The `DiscoveredJmxOperation` is now responsible for
  mapping methods to operations.

* A specific @`HealthEndpointCloudFoundryExtension` for Cloud Foundry.
  The extension logic used to create a "full" health endpoint extension
  has been made explicit.

Fixes gh-11428
Fixes gh-11581
pull/11626/merge
Phillip Webb 7 years ago
parent dc935fba48
commit 1d39feffea

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -40,19 +40,19 @@ public enum AccessLevel {
public static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel";
private final List<String> endpointPaths;
private final List<String> endpointIds;
AccessLevel(String... endpointPaths) {
this.endpointPaths = Arrays.asList(endpointPaths);
AccessLevel(String... endpointIds) {
this.endpointIds = Arrays.asList(endpointIds);
}
/**
* Returns if the access level should allow access to the specified endpoint path.
* @param endpointPath the endpoint path
* @param endpointId the endpoint ID to check
* @return {@code true} if access is allowed
*/
public boolean isAccessAllowed(String endpointPath) {
return this.endpointPaths.isEmpty() || this.endpointPaths.contains(endpointPath);
public boolean isAccessAllowed(String endpointId) {
return this.endpointIds.isEmpty() || this.endpointIds.contains(endpointId);
}
}

@ -16,22 +16,19 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.annotation.DiscovererEndpointFilter;
/**
* {@link EndpointFilter} for endpoints discovered by
* {@link CloudFoundryWebAnnotationEndpointDiscoverer}.
* {@link CloudFoundryWebEndpointDiscoverer}.
*
* @author Madhura Bhave
*/
public class CloudFoundryEndpointFilter implements EndpointFilter<WebOperation> {
class CloudFoundryEndpointFilter extends DiscovererEndpointFilter {
@Override
public boolean match(EndpointInfo<WebOperation> info, EndpointDiscoverer<WebOperation> discoverer) {
return (discoverer instanceof CloudFoundryWebAnnotationEndpointDiscoverer);
protected CloudFoundryEndpointFilter() {
super(CloudFoundryWebEndpointDiscoverer.class);
}
}

@ -1,66 +0,0 @@
/*
* Copyright 2012-2018 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.actuate.autoconfigure.cloudfoundry;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.context.ApplicationContext;
/**
* {@link WebAnnotationEndpointDiscoverer} for Cloud Foundry that uses Cloud Foundry
* specific extensions for the {@link HealthEndpoint}.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class CloudFoundryWebAnnotationEndpointDiscoverer
extends WebAnnotationEndpointDiscoverer {
private final Class<?> requiredExtensionType;
public CloudFoundryWebAnnotationEndpointDiscoverer(
ApplicationContext applicationContext, ParameterMapper parameterMapper,
EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<WebOperation>> filters,
Class<?> requiredExtensionType) {
super(applicationContext, parameterMapper, endpointMediaTypes,
endpointPathResolver, invokerAdvisors, filters);
this.requiredExtensionType = requiredExtensionType;
}
@Override
protected boolean isExtensionExposed(Class<?> endpointType, Class<?> extensionType,
EndpointInfo<WebOperation> endpointInfo) {
if (HealthEndpoint.class.equals(endpointType)
&& !this.requiredExtensionType.equals(extensionType)) {
return false;
}
return super.isExtensionExposed(endpointType, extensionType, endpointInfo);
}
}

@ -0,0 +1,85 @@
/*
* Copyright 2012-2018 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.actuate.autoconfigure.cloudfoundry;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
/**
* {@link WebEndpointDiscoverer} for Cloud Foundry that uses Cloud Foundry specific
* extensions for the {@link HealthEndpoint}.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
/**
* Create a new {@link WebEndpointDiscoverer} instance.
* @param applicationContext the source application context
* @param parameterValueMapper the parameter value mapper
* @param endpointMediaTypes the endpoint media types
* @param endpointPathResolver the endpoint path resolver
* @param invokerAdvisors invoker advisors to apply
* @param filters filters to apply
*/
public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
ParameterValueMapper parameterValueMapper,
EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver,
Collection<OperationInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
super(applicationContext, parameterValueMapper, endpointMediaTypes,
endpointPathResolver, invokerAdvisors, filters);
}
@Override
protected boolean isExtensionExposed(Object extensionBean) {
if (isHealthEndpointExtension(extensionBean)
&& !isCloudFoundryHealthEndpointExtension(extensionBean)) {
// Filter regular health endpoint extensions so a CF version can replace them
return false;
}
return true;
}
private boolean isHealthEndpointExtension(Object extensionBean) {
AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(extensionBean.getClass(),
EndpointWebExtension.class);
Class<?> endpoint = (attributes == null ? null : attributes.getClass("endpoint"));
return (endpoint != null && HealthEndpoint.class.isAssignableFrom(endpoint));
}
private boolean isCloudFoundryHealthEndpointExtension(Object extensionBean) {
return AnnotatedElementUtils.hasAnnotation(extensionBean.getClass(),
HealthEndpointCloudFoundryExtension.class);
}
}

@ -0,0 +1,41 @@
/*
* Copyright 2012-2018 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.actuate.autoconfigure.cloudfoundry;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
import org.springframework.boot.actuate.health.HealthEndpoint;
/**
* Identifies a type as being a Cloud Foundry specific extension for the
* {@link HealthEndpoint}.
*
* @author Phillip Webb
* @since 2.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class)
public @interface HealthEndpointCloudFoundryExtension {
}

@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryEndpointFilter;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.HealthEndpointCloudFoundryExtension;
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
@ -33,7 +33,7 @@ import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtensio
* @author Madhura Bhave
* @since 2.0.0
*/
@EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class)
@HealthEndpointCloudFoundryExtension
public class CloudFoundryReactiveHealthEndpointWebExtension {
private final ReactiveHealthEndpointWebExtension delegate;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -35,10 +35,10 @@ import org.springframework.web.server.ServerWebExchange;
*
* @author Madhura Bhave
*/
class ReactiveCloudFoundrySecurityInterceptor {
class CloudFoundrySecurityInterceptor {
private static final Log logger = LogFactory
.getLog(ReactiveCloudFoundrySecurityInterceptor.class);
.getLog(CloudFoundrySecurityInterceptor.class);
private final ReactiveTokenValidator tokenValidator;
@ -48,7 +48,7 @@ class ReactiveCloudFoundrySecurityInterceptor {
private static Mono<SecurityResponse> SUCCESS = Mono.just(SecurityResponse.success());
ReactiveCloudFoundrySecurityInterceptor(ReactiveTokenValidator tokenValidator,
CloudFoundrySecurityInterceptor(ReactiveTokenValidator tokenValidator,
ReactiveCloudFoundrySecurityService cloudFoundrySecurityService,
String applicationId) {
this.tokenValidator = tokenValidator;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,10 +16,8 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
@ -29,27 +27,18 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.reflect.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
@ -58,45 +47,33 @@ import org.springframework.web.server.ServerWebExchange;
* Cloud Foundry specific URLs over HTTP using Spring WebFlux.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class CloudFoundryWebFluxEndpointHandlerMapping
extends AbstractWebFluxEndpointHandlerMapping {
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
private final CloudFoundrySecurityInterceptor securityInterceptor;
private final Method handleWrite = ReflectionUtils.findMethod(
WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class);
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver();
private final Method links = ReflectionUtils.findMethod(getClass(), "links",
ServerWebExchange.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
@Override
protected Method getLinks() {
return this.links;
CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
CloudFoundrySecurityInterceptor securityInterceptor) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
@Override
protected void registerMappingForOperation(WebOperation operation) {
OperationType operationType = operation.getType();
OperationInvoker operationInvoker = operation.getInvoker();
if (operation.isBlocking()) {
operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker);
}
Object handler = (operationType == OperationType.WRITE
? new WriteOperationHandler(operationInvoker, operation.getId())
: new ReadOperationHandler(operationInvoker, operation.getId()));
Method method = (operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead);
registerMapping(createRequestMappingInfo(operation), handler, method);
protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint,
WebOperation operation, ReactiveWebOperation reactiveWebOperation) {
return new SecureReactiveWebOperation(reactiveWebOperation,
this.securityInterceptor, endpoint.getId());
}
@Override
@ResponseBody
private Publisher<ResponseEntity<Object>> links(ServerWebExchange exchange) {
protected Publisher<ResponseEntity<Object>> links(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
return this.securityInterceptor.preHandle(exchange, "")
.map((securityResponse) -> {
@ -105,7 +82,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping
}
AccessLevel accessLevel = exchange
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.endpointLinksResolver
Map<String, Link> links = this.linksResolver
.resolveLinks(getEndpoints(), request.getURI().toString());
return new ResponseEntity<>(
Collections.singletonMap("_links",
@ -126,117 +103,37 @@ class CloudFoundryWebFluxEndpointHandlerMapping
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
* @param securityInterceptor the Security Interceptor
* {@link ReactiveWebOperation} wrapper to add security.
*/
CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebOperation>> webEndpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
private static class SecureReactiveWebOperation implements ReactiveWebOperation {
/**
* Base class for handlers for endpoint operations.
*/
abstract class AbstractOperationHandler {
private final ReactiveWebOperation delegate;
private final OperationInvoker operationInvoker;
private final CloudFoundrySecurityInterceptor securityInterceptor;
private final String endpointId;
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
AbstractOperationHandler(OperationInvoker operationInvoker, String endpointId,
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
this.operationInvoker = operationInvoker;
this.endpointId = endpointId;
SecureReactiveWebOperation(ReactiveWebOperation delegate,
CloudFoundrySecurityInterceptor securityInterceptor, String endpointId) {
this.delegate = delegate;
this.securityInterceptor = securityInterceptor;
this.endpointId = endpointId;
}
Publisher<ResponseEntity<Object>> doHandle(ServerWebExchange exchange,
@Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange,
Map<String, String> body) {
return this.securityInterceptor.preHandle(exchange, this.endpointId)
.flatMap((securityResponse) -> flatMapResponse(exchange, body,
securityResponse));
}
private Mono<? extends ResponseEntity<Object>> flatMapResponse(
ServerWebExchange exchange, Map<String, String> body,
SecurityResponse securityResponse) {
private Mono<ResponseEntity<Object>> flatMapResponse(ServerWebExchange exchange,
Map<String, String> body, SecurityResponse securityResponse) {
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
return Mono.just(new ResponseEntity<>(securityResponse.getStatus()));
}
Map<String, Object> arguments = new HashMap<>(exchange
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments
.put(name, (values.size() == 1 ? values.get(0) : values)));
return handleResult((Publisher<?>) this.operationInvoker.invoke(arguments),
exchange.getRequest().getMethod());
}
private Mono<ResponseEntity<Object>> handleResult(Publisher<?> result,
HttpMethod httpMethod) {
return Mono.from(result).map(this::toResponseEntity)
.onErrorReturn(ParametersMissingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.onErrorReturn(ParameterMappingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.defaultIfEmpty(new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT));
}
private ResponseEntity<Object> toResponseEntity(Object response) {
if (!(response instanceof WebEndpointResponse)) {
return new ResponseEntity<>(response, HttpStatus.OK);
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return new ResponseEntity<>(webEndpointResponse.getBody(),
HttpStatus.valueOf(webEndpointResponse.getStatus()));
}
}
/**
* A handler for an endpoint write operation.
*/
final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId,
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return doHandle(exchange, body);
}
}
/**
* A handler for an endpoint write operation.
*/
final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId,
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange) {
return doHandle(exchange, null);
return this.delegate.handle(exchange, body);
}
}

@ -21,10 +21,10 @@ import java.util.Collections;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.health.HealthEndpoint;
@ -84,27 +84,27 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
@Bean
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping(
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
WebClient.Builder webClientBuilder) {
CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer(
CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null,
CloudFoundryReactiveHealthEndpointWebExtension.class);
ReactiveCloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
EndpointPathResolver.useEndpointId(), Collections.emptyList(),
Collections.emptyList());
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
webClientBuilder, this.applicationContext.getEnvironment());
return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
endpointDiscoverer.getEndpoints(), endpointMediaTypes,
getCorsConfiguration(), securityInterceptor);
}
private ReactiveCloudFoundrySecurityInterceptor getSecurityInterceptor(
private CloudFoundrySecurityInterceptor getSecurityInterceptor(
WebClient.Builder restTemplateBuilder, Environment environment) {
ReactiveCloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
restTemplateBuilder, environment);
ReactiveTokenValidator tokenValidator = new ReactiveTokenValidator(
cloudfoundrySecurityService);
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator,
return new CloudFoundrySecurityInterceptor(tokenValidator,
cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
}

@ -17,12 +17,13 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.health.HealthEndpoint;
@ -85,18 +86,18 @@ public class CloudFoundryActuatorAutoConfiguration {
@Bean
public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
RestTemplateBuilder restTemplateBuilder) {
CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer(
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null,
CloudFoundryHealthEndpointWebExtension.class);
EndpointPathResolver.useEndpointId(), Collections.emptyList(),
Collections.emptyList());
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
restTemplateBuilder, this.applicationContext.getEnvironment());
return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
getCorsConfiguration(), securityInterceptor);
discoverer.getEndpoints(), endpointMediaTypes, getCorsConfiguration(),
securityInterceptor);
}
private CloudFoundrySecurityInterceptor getSecurityInterceptor(

@ -16,7 +16,7 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryEndpointFilter;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.HealthEndpointCloudFoundryExtension;
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
@ -31,7 +31,7 @@ import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
* @author Madhura Bhave
* @since 2.0.0
*/
@EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class)
@HealthEndpointCloudFoundryExtension
public class CloudFoundryHealthEndpointWebExtension {
private final HealthEndpointWebExtension delegate;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -88,12 +88,12 @@ class CloudFoundrySecurityInterceptor {
return SecurityResponse.success();
}
private void check(HttpServletRequest request, String path) throws Exception {
private void check(HttpServletRequest request, String endpointId) throws Exception {
Token token = getToken(request);
this.tokenValidator.validate(token);
AccessLevel accessLevel = this.cloudFoundrySecurityService
.getAccessLevel(token.toString(), this.applicationId);
if (!accessLevel.isAccessAllowed(path)) {
if (!accessLevel.isAccessAllowed(endpointId)) {
throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
"Access denied");
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,11 +16,8 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
@ -28,30 +25,19 @@ import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.reflect.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
/**
@ -59,39 +45,33 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
* Cloud Foundry specific URLs over HTTP using Spring MVC.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class CloudFoundryWebEndpointServletHandlerMapping
extends AbstractWebMvcEndpointHandlerMapping {
private final Method handle = ReflectionUtils.findMethod(OperationHandler.class,
"handle", HttpServletRequest.class, Map.class);
private final Method links = ReflectionUtils.findMethod(
CloudFoundryWebEndpointServletHandlerMapping.class, "links",
HttpServletRequest.class, HttpServletResponse.class);
private static final Log logger = LogFactory
.getLog(CloudFoundryWebEndpointServletHandlerMapping.class);
private final CloudFoundrySecurityInterceptor securityInterceptor;
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver();
CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebOperation>> webEndpoints,
Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
CloudFoundrySecurityInterceptor securityInterceptor) {
super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration);
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
@Override
protected Method getLinks() {
return this.links;
protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint,
WebOperation operation, ServletWebOperation servletWebOperation) {
return new SecureServletWebOperation(servletWebOperation,
this.securityInterceptor, endpoint.getId());
}
@Override
@ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request,
protected Map<String, Map<String, Link>> links(HttpServletRequest request,
HttpServletResponse response) {
SecurityResponse securityResponse = this.securityInterceptor.preHandle(request,
"");
@ -100,7 +80,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
}
AccessLevel accessLevel = (AccessLevel) request
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(),
Map<String, Link> links = this.linksResolver.resolveLinks(getEndpoints(),
request.getRequestURL().toString());
Map<String, Link> filteredLinks = new LinkedHashMap<>();
if (accessLevel == null) {
@ -120,83 +100,38 @@ class CloudFoundryWebEndpointServletHandlerMapping
securityResponse.getMessage());
}
catch (Exception ex) {
logger.debug("Failed to send error response", ex);
this.logger.debug("Failed to send error response", ex);
}
}
@Override
protected void registerMappingForOperation(WebOperation operation) {
registerMapping(createRequestMappingInfo(operation),
new OperationHandler(operation.getInvoker(), operation.getId(),
this.securityInterceptor),
this.handle);
}
/**
* Handler which has the handler method and security interceptor.
* {@link ServletWebOperation} wrapper to add security.
*/
final class OperationHandler {
private final OperationInvoker operationInvoker;
private static class SecureServletWebOperation implements ServletWebOperation {
private final String endpointId;
private final ServletWebOperation delegate;
private final CloudFoundrySecurityInterceptor securityInterceptor;
OperationHandler(OperationInvoker operationInvoker, String id,
CloudFoundrySecurityInterceptor securityInterceptor) {
this.operationInvoker = operationInvoker;
this.endpointId = id;
private final String endpointId;
SecureServletWebOperation(ServletWebOperation delegate,
CloudFoundrySecurityInterceptor securityInterceptor, String endpointId) {
this.delegate = delegate;
this.securityInterceptor = securityInterceptor;
this.endpointId = endpointId;
}
@SuppressWarnings("unchecked")
@ResponseBody
public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) {
@Override
public Object handle(HttpServletRequest request, Map<String, String> body) {
SecurityResponse securityResponse = this.securityInterceptor
.preHandle(request, this.endpointId);
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
return failureResponse(securityResponse);
return new ResponseEntity<Object>(securityResponse.getMessage(),
securityResponse.getStatus());
}
Map<String, Object> arguments = new HashMap<>((Map<String, String>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
if (body != null && HttpMethod.POST == httpMethod) {
arguments.putAll(body);
}
request.getParameterMap().forEach((name, values) -> arguments.put(name,
values.length == 1 ? values[0] : Arrays.asList(values)));
try {
return handleResult(this.operationInvoker.invoke(arguments), httpMethod);
}
catch (ParametersMissingException | ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private Object failureResponse(SecurityResponse response) {
return handleResult(new WebEndpointResponse<>(response.getMessage(),
response.getStatus().value()));
}
private Object handleResult(Object result) {
return handleResult(result, null);
}
private Object handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return result;
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
return this.delegate.handle(request, body);
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,9 +17,9 @@
package org.springframework.boot.actuate.autoconfigure.endpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@ -38,8 +38,9 @@ import org.springframework.core.env.Environment;
public class EndpointAutoConfiguration {
@Bean
public ParameterMapper endpointOperationParameterMapper() {
return new ConversionServiceParameterMapper();
@ConditionalOnMissingBean
public ParameterValueMapper endpointOperationParameterMapper() {
return new ConversionServiceParameterValueMapper();
}
@Bean

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -19,7 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint;
import java.time.Duration;
import java.util.function.Function;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -24,10 +24,8 @@ import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.env.Environment;
@ -37,14 +35,14 @@ import org.springframework.util.Assert;
* {@link EndpointFilter} that will filter endpoints based on {@code expose} and
* {@code exclude} properties.
*
* @param <T> The operation type
* @param <E> The endpoint type
* @author Phillip Webb
* @since 2.0.0
*/
public class ExposeExcludePropertyEndpointFilter<T extends Operation>
implements EndpointFilter<T> {
public class ExposeExcludePropertyEndpointFilter<E extends ExposableEndpoint<?>>
implements EndpointFilter<E> {
private final Class<? extends EndpointDiscoverer<T>> discovererType;
private final Class<E> endpointType;
private final Set<String> expose;
@ -52,25 +50,23 @@ public class ExposeExcludePropertyEndpointFilter<T extends Operation>
private final Set<String> exposeDefaults;
public ExposeExcludePropertyEndpointFilter(
Class<? extends EndpointDiscoverer<T>> discovererType,
public ExposeExcludePropertyEndpointFilter(Class<E> endpointType,
Environment environment, String prefix, String... exposeDefaults) {
Assert.notNull(discovererType, "Discoverer Type must not be null");
Assert.notNull(endpointType, "EndpointType must not be null");
Assert.notNull(environment, "Environment must not be null");
Assert.hasText(prefix, "Prefix must not be empty");
Binder binder = Binder.get(environment);
this.discovererType = discovererType;
this.endpointType = endpointType;
this.expose = bind(binder, prefix + ".expose");
this.exclude = bind(binder, prefix + ".exclude");
this.exposeDefaults = asSet(Arrays.asList(exposeDefaults));
}
public ExposeExcludePropertyEndpointFilter(
Class<? extends EndpointDiscoverer<T>> discovererType,
public ExposeExcludePropertyEndpointFilter(Class<E> endpointType,
Collection<String> expose, Collection<String> exclude,
String... exposeDefaults) {
Assert.notNull(discovererType, "Discoverer Type must not be null");
this.discovererType = discovererType;
Assert.notNull(endpointType, "EndpointType Type must not be null");
this.endpointType = endpointType;
this.expose = asSet(expose);
this.exclude = asSet(exclude);
this.exposeDefaults = asSet(Arrays.asList(exposeDefaults));
@ -90,30 +86,30 @@ public class ExposeExcludePropertyEndpointFilter<T extends Operation>
}
@Override
public boolean match(EndpointInfo<T> info, EndpointDiscoverer<T> discoverer) {
if (this.discovererType.isInstance(discoverer)) {
return isExposed(info) && !isExcluded(info);
public boolean match(E endpoint) {
if (this.endpointType.isInstance(endpoint)) {
return isExposed(endpoint) && !isExcluded(endpoint);
}
return true;
}
private boolean isExposed(EndpointInfo<T> info) {
private boolean isExposed(ExposableEndpoint<?> endpoint) {
if (this.expose.isEmpty()) {
return this.exposeDefaults.contains("*")
|| contains(this.exposeDefaults, info);
|| contains(this.exposeDefaults, endpoint);
}
return this.expose.contains("*") || contains(this.expose, info);
return this.expose.contains("*") || contains(this.expose, endpoint);
}
private boolean isExcluded(EndpointInfo<T> info) {
private boolean isExcluded(ExposableEndpoint<?> endpoint) {
if (this.exclude.isEmpty()) {
return false;
}
return this.exclude.contains("*") || contains(this.exclude, info);
return this.exclude.contains("*") || contains(this.exclude, endpoint);
}
private boolean contains(Set<String> items, EndpointInfo<T> info) {
return items.contains(info.getId().toLowerCase());
private boolean contains(Set<String> items, ExposableEndpoint<?> endpoint) {
return items.contains(endpoint.getId().toLowerCase());
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,14 +16,12 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.jmx;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBean;
import org.springframework.boot.actuate.endpoint.jmx.EndpointObjectNameFactory;
import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@ -50,15 +48,18 @@ class DefaultEndpointObjectNameFactory implements EndpointObjectNameFactory {
}
@Override
public ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException {
String baseObjectName = this.properties.getDomain() + ":type=Endpoint" + ",name="
+ StringUtils.capitalize(mBean.getEndpointId());
StringBuilder builder = new StringBuilder(baseObjectName);
if (this.mBeanServer != null && hasMBean(baseObjectName)) {
builder.append(",context=").append(this.contextId);
public ObjectName getObjectName(ExposableJmxEndpoint endpoint)
throws MalformedObjectNameException {
StringBuilder builder = new StringBuilder(this.properties.getDomain());
builder.append(":type=Endpoint");
builder.append(",name=" + StringUtils.capitalize(endpoint.getId()));
String baseName = builder.toString();
if (this.mBeanServer != null && hasMBean(baseName)) {
builder.append(",context=" + this.contextId);
}
if (this.properties.isUniqueNames()) {
builder.append(",identity=").append(ObjectUtils.getIdentityHexString(mBean));
String identity = ObjectUtils.getIdentityHexString(endpoint);
builder.append(",identity=" + identity);
}
builder.append(getStaticNames());
return ObjectNameManager.getInstance(builder.toString());
@ -74,10 +75,8 @@ class DefaultEndpointObjectNameFactory implements EndpointObjectNameFactory {
return "";
}
StringBuilder builder = new StringBuilder();
for (Map.Entry<Object, Object> name : this.properties.getStaticNames()
.entrySet()) {
builder.append(",").append(name.getKey()).append("=").append(name.getValue());
}
this.properties.getStaticNames()
.forEach((name, value) -> builder.append("," + name + "=" + value));
return builder.toString();
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,6 +17,8 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.jmx;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import javax.management.MBeanServer;
@ -26,14 +28,18 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanRegistrar;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.jmx.EndpointObjectNameFactory;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
import org.springframework.boot.actuate.endpoint.jmx.JacksonJmxOperationResponseMapper;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper;
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
@ -66,34 +72,37 @@ public class JmxEndpointAutoConfiguration {
}
@Bean
public JmxAnnotationEndpointDiscoverer jmxAnnotationEndpointDiscoverer(
ParameterMapper parameterMapper,
Collection<OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<JmxOperation>> filters) {
return new JmxAnnotationEndpointDiscoverer(this.applicationContext,
parameterMapper, invokerAdvisors, filters);
@ConditionalOnMissingBean(JmxEndpointsSupplier.class)
public JmxEndpointDiscoverer jmxAnnotationEndpointDiscoverer(
ParameterValueMapper parameterValueMapper,
ObjectProvider<Collection<OperationInvokerAdvisor>> invokerAdvisors,
ObjectProvider<Collection<EndpointFilter<ExposableJmxEndpoint>>> filters) {
return new JmxEndpointDiscoverer(this.applicationContext, parameterValueMapper,
invokerAdvisors.getIfAvailable(Collections::emptyList),
filters.getIfAvailable(Collections::emptyList));
}
@Bean
@ConditionalOnSingleCandidate(MBeanServer.class)
public JmxEndpointExporter jmxMBeanExporter(
JmxAnnotationEndpointDiscoverer jmxAnnotationEndpointDiscoverer,
MBeanServer mBeanServer, JmxAnnotationEndpointDiscoverer endpointDiscoverer,
ObjectProvider<ObjectMapper> objectMapper) {
public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer,
ObjectProvider<ObjectMapper> objectMapper,
JmxEndpointsSupplier jmxEndpointsSupplier) {
String contextId = ObjectUtils.getIdentityHexString(this.applicationContext);
EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(
this.properties, mBeanServer,
ObjectUtils.getIdentityHexString(this.applicationContext));
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(mBeanServer,
objectNameFactory);
return new JmxEndpointExporter(jmxAnnotationEndpointDiscoverer, registrar,
objectMapper.getIfAvailable(ObjectMapper::new));
this.properties, mBeanServer, contextId);
JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper(
objectMapper.getIfAvailable());
return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper,
jmxEndpointsSupplier.getEndpoints());
}
@Bean
public ExposeExcludePropertyEndpointFilter<JmxOperation> jmxIncludeExcludePropertyEndpointFilter() {
return new ExposeExcludePropertyEndpointFilter<>(
JmxAnnotationEndpointDiscoverer.class, this.properties.getExpose(),
this.properties.getExclude(), "*");
public ExposeExcludePropertyEndpointFilter<ExposableJmxEndpoint> jmxIncludeExcludePropertyEndpointFilter() {
Set<String> expose = this.properties.getExpose();
Set<String> exclude = this.properties.getExclude();
return new ExposeExcludePropertyEndpointFilter<>(ExposableJmxEndpoint.class,
expose, exclude, "*");
}
}

@ -1,132 +0,0 @@
/*
* 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.actuate.autoconfigure.endpoint.jmx;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanRegistrar;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointMBeanFactory;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper;
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxAnnotationEndpointDiscoverer;
/**
* Exports all available {@link Endpoint} to a configurable {@link MBeanServer}.
*
* @author Stephane Nicoll
*/
class JmxEndpointExporter implements InitializingBean, DisposableBean {
private final JmxAnnotationEndpointDiscoverer endpointDiscoverer;
private final EndpointMBeanRegistrar endpointMBeanRegistrar;
private final JmxEndpointMBeanFactory mBeanFactory;
private Collection<ObjectName> registeredObjectNames;
JmxEndpointExporter(JmxAnnotationEndpointDiscoverer endpointDiscoverer,
EndpointMBeanRegistrar endpointMBeanRegistrar, ObjectMapper objectMapper) {
this.endpointDiscoverer = endpointDiscoverer;
this.endpointMBeanRegistrar = endpointMBeanRegistrar;
DataConverter dataConverter = new DataConverter(objectMapper);
this.mBeanFactory = new JmxEndpointMBeanFactory(dataConverter);
}
@Override
public void afterPropertiesSet() {
this.registeredObjectNames = registerEndpointMBeans();
}
private Collection<ObjectName> registerEndpointMBeans() {
Collection<EndpointInfo<JmxOperation>> endpoints = this.endpointDiscoverer
.discoverEndpoints();
return this.mBeanFactory.createMBeans(endpoints).stream()
.map(this.endpointMBeanRegistrar::registerEndpointMBean)
.collect(Collectors.toCollection(ArrayList::new));
}
@Override
public void destroy() throws Exception {
unregisterEndpointMBeans(this.registeredObjectNames);
}
private void unregisterEndpointMBeans(Collection<ObjectName> objectNames) {
objectNames.forEach(this.endpointMBeanRegistrar::unregisterEndpointMbean);
}
static class DataConverter implements JmxOperationResponseMapper {
private final ObjectMapper objectMapper;
private final JavaType listObject;
private final JavaType mapStringObject;
DataConverter(ObjectMapper objectMapper) {
this.objectMapper = (objectMapper == null ? new ObjectMapper()
: objectMapper);
this.listObject = this.objectMapper.getTypeFactory()
.constructParametricType(List.class, Object.class);
this.mapStringObject = this.objectMapper.getTypeFactory()
.constructParametricType(Map.class, String.class, Object.class);
}
@Override
public Object mapResponse(Object response) {
if (response == null) {
return null;
}
if (response instanceof String) {
return response;
}
if (response.getClass().isArray() || response instanceof Collection) {
return this.objectMapper.convertValue(response, this.listObject);
}
return this.objectMapper.convertValue(response, this.mapStringObject);
}
@Override
public Class<?> mapResponseType(Class<?> responseType) {
if (responseType.equals(String.class)) {
return String.class;
}
if (responseType.isArray()
|| Collection.class.isAssignableFrom(responseType)) {
return List.class;
}
return Map.class;
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,13 +16,12 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.util.Assert;
/**
@ -35,33 +34,28 @@ public class DefaultEndpointPathProvider implements EndpointPathProvider {
private final String basePath;
private final EndpointDiscoverer<WebOperation> endpointDiscoverer;
private final Collection<ExposableWebEndpoint> endpoints;
public DefaultEndpointPathProvider(
EndpointDiscoverer<WebOperation> endpointDiscoverer,
WebEndpointProperties webEndpointProperties) {
this.endpointDiscoverer = endpointDiscoverer;
public DefaultEndpointPathProvider(WebEndpointProperties webEndpointProperties,
Collection<? extends ExposableWebEndpoint> endpoints) {
this.basePath = webEndpointProperties.getBasePath();
this.endpoints = Collections.unmodifiableCollection(endpoints);
}
@Override
public List<String> getPaths() {
return getEndpoints().map(this::getPath).collect(Collectors.toList());
return this.endpoints.stream().map(this::getPath).collect(Collectors.toList());
}
@Override
public String getPath(String id) {
Assert.notNull(id, "ID must not be null");
return getEndpoints().filter((info) -> id.equals(info.getId())).findFirst()
.map(this::getPath).orElse(null);
return this.endpoints.stream().filter((info) -> id.equals(info.getId()))
.findFirst().map(this::getPath).orElse(null);
}
private Stream<EndpointInfo<WebOperation>> getEndpoints() {
return this.endpointDiscoverer.discoverEndpoints().stream();
}
private String getPath(EndpointInfo<WebOperation> endpointInfo) {
return this.basePath + "/" + endpointInfo.getId();
private String getPath(ExposableWebEndpoint endpoint) {
return this.basePath + "/" + endpoint.getId();
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -18,20 +18,23 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -78,34 +81,39 @@ public class WebEndpointAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public WebAnnotationEndpointDiscoverer webAnnotationEndpointDiscoverer(
ParameterMapper parameterMapper, EndpointPathResolver endpointPathResolver,
Collection<OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<WebOperation>> filters) {
return new WebAnnotationEndpointDiscoverer(this.applicationContext,
parameterMapper, endpointMediaTypes(), endpointPathResolver,
invokerAdvisors, filters);
public EndpointMediaTypes endpointMediaTypes() {
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
}
@Bean
@ConditionalOnMissingBean
public EndpointMediaTypes endpointMediaTypes() {
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
@ConditionalOnMissingBean(WebEndpointsSupplier.class)
public WebEndpointDiscoverer webEndpointDiscoverer(
ParameterValueMapper parameterValueMapper,
EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver,
ObjectProvider<Collection<OperationInvokerAdvisor>> invokerAdvisors,
ObjectProvider<Collection<EndpointFilter<ExposableWebEndpoint>>> filters) {
return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper,
endpointMediaTypes, endpointPathResolver,
invokerAdvisors.getIfAvailable(Collections::emptyList),
filters.getIfAvailable(Collections::emptyList));
}
@Bean
@ConditionalOnMissingBean
public EndpointPathProvider endpointPathProvider(
EndpointDiscoverer<WebOperation> endpointDiscoverer,
WebEndpointsSupplier webEndpointsSupplier,
WebEndpointProperties webEndpointProperties) {
return new DefaultEndpointPathProvider(endpointDiscoverer, webEndpointProperties);
return new DefaultEndpointPathProvider(webEndpointProperties,
webEndpointsSupplier.getEndpoints());
}
@Bean
public ExposeExcludePropertyEndpointFilter<WebOperation> webIncludeExcludePropertyEndpointFilter() {
return new ExposeExcludePropertyEndpointFilter<>(
WebAnnotationEndpointDiscoverer.class, this.properties.getExpose(),
this.properties.getExclude(), "info", "health");
public ExposeExcludePropertyEndpointFilter<ExposableWebEndpoint> webIncludeExcludePropertyEndpointFilter() {
Set<String> expose = this.properties.getExpose();
Set<String> exclude = this.properties.getExclude();
return new ExposeExcludePropertyEndpointFilter<>(ExposableWebEndpoint.class,
expose, exclude, "info", "health");
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,6 +16,8 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import org.glassfish.jersey.server.ResourceConfig;
@ -24,7 +26,8 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -45,19 +48,25 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnBean({ ResourceConfig.class, WebAnnotationEndpointDiscoverer.class })
@ConditionalOnBean({ ResourceConfig.class, WebEndpointsSupplier.class })
@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet")
class JerseyWebEndpointManagementContextConfiguration {
@Bean
public ResourceConfigCustomizer webEndpointRegistrar(
WebAnnotationEndpointDiscoverer endpointDiscoverer,
WebEndpointsSupplier webEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes,
WebEndpointProperties webEndpointProperties) {
return (resourceConfig) -> resourceConfig.registerResources(
new HashSet<>(new JerseyEndpointResourceFactory().createEndpointResources(
new EndpointMapping(webEndpointProperties.getBasePath()),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes)));
return (resourceConfig) -> {
JerseyEndpointResourceFactory resourceFactory = new JerseyEndpointResourceFactory();
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
Collection<ExposableWebEndpoint> endpoints = Collections
.unmodifiableCollection(webEndpointsSupplier.getEndpoints());
resourceConfig.registerResources(
new HashSet<>(resourceFactory.createEndpointResources(endpointMapping,
endpoints, endpointMediaTypes)));
};
}
}

@ -21,7 +21,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -44,19 +44,20 @@ import org.springframework.web.reactive.DispatcherHandler;
@ManagementContextConfiguration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnBean(WebAnnotationEndpointDiscoverer.class)
@ConditionalOnBean(WebEndpointsSupplier.class)
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebFluxEndpointManagementContextConfiguration {
@Bean
@ConditionalOnMissingBean
public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(
WebAnnotationEndpointDiscoverer endpointDiscoverer,
WebEndpointsSupplier webEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) {
return new WebFluxEndpointHandlerMapping(
new EndpointMapping(webEndpointProperties.getBasePath()),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath());
return new WebFluxEndpointHandlerMapping(endpointMapping,
webEndpointsSupplier.getEndpoints(), endpointMediaTypes,
corsProperties.toCorsConfiguration());
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -21,7 +21,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -40,25 +40,24 @@ import org.springframework.web.servlet.DispatcherServlet;
* @author Phillip Webb
* @since 2.0.0
*/
@ManagementContextConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnBean({ DispatcherServlet.class, WebAnnotationEndpointDiscoverer.class })
@ConditionalOnBean({ DispatcherServlet.class, WebEndpointsSupplier.class })
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebMvcEndpointManagementContextConfiguration {
@Bean
@ConditionalOnMissingBean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(
WebAnnotationEndpointDiscoverer endpointDiscoverer,
WebEndpointsSupplier webEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) {
WebMvcEndpointHandlerMapping handlerMapping = new WebMvcEndpointHandlerMapping(
new EndpointMapping(webEndpointProperties.getBasePath()),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath());
return new WebMvcEndpointHandlerMapping(endpointMapping,
webEndpointsSupplier.getEndpoints(), endpointMediaTypes,
corsProperties.toCorsConfiguration());
return handlerMapping;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,22 +16,13 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.context.ApplicationContext;
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredEndpoint;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CloudFoundryEndpointFilter}.
@ -40,38 +31,22 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class CloudFoundryEndpointFilterTests {
private CloudFoundryEndpointFilter filter;
@Before
public void setUp() {
this.filter = new CloudFoundryEndpointFilter();
}
private CloudFoundryEndpointFilter filter = new CloudFoundryEndpointFilter();
@Test
public void matchIfDiscovererCloudFoundryShouldReturnFalse() {
CloudFoundryWebAnnotationEndpointDiscoverer discoverer = Mockito
.mock(CloudFoundryWebAnnotationEndpointDiscoverer.class);
assertThat(this.filter.match(null, discoverer)).isTrue();
DiscoveredEndpoint<?> endpoint = mock(DiscoveredEndpoint.class);
given(endpoint.wasDiscoveredBy(CloudFoundryWebEndpointDiscoverer.class))
.willReturn(true);
assertThat(this.filter.match(endpoint)).isTrue();
}
@Test
public void matchIfDiscovererNotCloudFoundryShouldReturnFalse() {
WebAnnotationEndpointDiscoverer discoverer = Mockito
.mock(WebAnnotationEndpointDiscoverer.class);
assertThat(this.filter.match(null, discoverer)).isFalse();
}
static class TestEndpointDiscoverer extends WebAnnotationEndpointDiscoverer {
TestEndpointDiscoverer(ApplicationContext applicationContext,
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<WebOperation>> filters) {
super(applicationContext, parameterMapper, endpointMediaTypes,
endpointPathResolver, invokerAdvisors, filters);
}
DiscoveredEndpoint<?> endpoint = mock(DiscoveredEndpoint.class);
given(endpoint.wasDiscoveredBy(CloudFoundryWebEndpointDiscoverer.class))
.willReturn(false);
assertThat(this.filter.match(endpoint)).isFalse();
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -23,59 +23,66 @@ import java.util.function.Function;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CloudFoundryWebAnnotationEndpointDiscoverer}.
* Tests for {@link CloudFoundryWebEndpointDiscoverer}.
*
* @author Madhura Bhave
*/
public class CloudFoundryWebAnnotationEndpointDiscovererTests {
public class CloudFoundryWebEndpointDiscovererTests {
@Test
public void discovererShouldAddSuppliedExtensionForHealthEndpoint() {
load(TestConfiguration.class, (endpointDiscoverer) -> {
Collection<EndpointInfo<WebOperation>> endpoints = endpointDiscoverer
.discoverEndpoints();
public void getEndpointsShouldAddCloudFoundryHealthExtension() {
load(TestConfiguration.class, (discoverer) -> {
Collection<ExposableWebEndpoint> endpoints = discoverer.getEndpoints();
assertThat(endpoints.size()).isEqualTo(2);
for (ExposableWebEndpoint endpoint : endpoints) {
if (endpoint.getId().equals("health")) {
WebOperation operation = endpoint.getOperations().iterator().next();
assertThat(operation.invoke(Collections.emptyMap())).isEqualTo("cf");
}
}
});
}
private void load(Class<?> configuration,
Consumer<CloudFoundryWebAnnotationEndpointDiscoverer> consumer) {
Consumer<CloudFoundryWebEndpointDiscoverer> consumer) {
this.load((id) -> null, (id) -> id, configuration, consumer);
}
private void load(Function<String, Long> timeToLive,
EndpointPathResolver endpointPathResolver, Class<?> configuration,
Consumer<CloudFoundryWebAnnotationEndpointDiscoverer> consumer) {
Consumer<CloudFoundryWebEndpointDiscoverer> consumer) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
configuration);
try {
ConversionServiceParameterMapper parameterMapper = new ConversionServiceParameterMapper(
ConversionServiceParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
DefaultConversionService.getSharedInstance());
EndpointMediaTypes mediaTypes = new EndpointMediaTypes(
Collections.singletonList("application/json"),
Collections.singletonList("application/json"));
CloudFoundryWebAnnotationEndpointDiscoverer discoverer = new CloudFoundryWebAnnotationEndpointDiscoverer(
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
context, parameterMapper, mediaTypes, endpointPathResolver,
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)),
null, HealthWebEndpointExtension.class);
Collections.emptyList());
consumer.accept(discoverer);
}
finally {
@ -92,29 +99,29 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
}
@Bean
public HealthEndpoint healthEndpoint() {
return new HealthEndpoint(null);
public TestEndpointWebExtension testEndpointWebExtension() {
return new TestEndpointWebExtension();
}
@Bean
public TestWebEndpointExtension testEndpointExtension() {
return new TestWebEndpointExtension();
public HealthEndpoint healthEndpoint() {
return new HealthEndpoint(mock(HealthIndicator.class));
}
@Bean
public HealthWebEndpointExtension healthEndpointExtension() {
return new HealthWebEndpointExtension();
public HealthEndpointWebExtension healthEndpointWebExtension() {
return new HealthEndpointWebExtension();
}
@Bean
public OtherHealthWebEndpointExtension otherHealthEndpointExtension() {
return new OtherHealthWebEndpointExtension();
public TestHealthEndpointCloudFoundryExtension testHealthEndpointCloudFoundryExtension() {
return new TestHealthEndpointCloudFoundryExtension();
}
}
@EndpointWebExtension(endpoint = TestEndpoint.class)
static class TestWebEndpointExtension {
@Endpoint(id = "test")
static class TestEndpoint {
@ReadOperation
public Object getAll() {
@ -123,8 +130,8 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
}
@Endpoint(id = "test")
static class TestEndpoint {
@EndpointWebExtension(endpoint = TestEndpoint.class)
static class TestEndpointWebExtension {
@ReadOperation
public Object getAll() {
@ -134,7 +141,7 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
}
@EndpointWebExtension(endpoint = HealthEndpoint.class)
static class HealthWebEndpointExtension {
static class HealthEndpointWebExtension {
@ReadOperation
public Object getAll() {
@ -143,12 +150,12 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
}
@EndpointWebExtension(endpoint = HealthEndpoint.class)
static class OtherHealthWebEndpointExtension {
@HealthEndpointCloudFoundryExtension
static class TestHealthEndpointCloudFoundryExtension {
@ReadOperation
public Object getAll() {
return null;
return "cf";
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -28,17 +28,15 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
@ -205,9 +203,9 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
private int port;
@Bean
public ReactiveCloudFoundrySecurityInterceptor interceptor() {
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator,
securityService, "app-id");
public CloudFoundrySecurityInterceptor interceptor() {
return new CloudFoundrySecurityInterceptor(tokenValidator, securityService,
"app-id");
}
@Bean
@ -218,27 +216,27 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
@Bean
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
EndpointDiscoverer<WebOperation> webEndpointDiscoverer,
WebEndpointDiscoverer webEndpointDiscoverer,
EndpointMediaTypes endpointMediaTypes,
ReactiveCloudFoundrySecurityInterceptor interceptor) {
CloudFoundrySecurityInterceptor interceptor) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration, interceptor);
}
@Bean
public WebAnnotationEndpointDiscoverer webEndpointDiscoverer(
public WebEndpointDiscoverer webEndpointDiscoverer(
ApplicationContext applicationContext,
EndpointMediaTypes endpointMediaTypes) {
ParameterMapper parameterMapper = new ConversionServiceParameterMapper(
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
DefaultConversionService.getSharedInstance());
return new WebAnnotationEndpointDiscoverer(applicationContext,
parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null);
return new WebEndpointDiscoverer(applicationContext, parameterMapper,
endpointMediaTypes, EndpointPathResolver.useEndpointId(),
Collections.emptyList(), Collections.emptyList());
}
@Bean

@ -29,11 +29,11 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.reflect.ReflectiveOperationInvoker;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
@ -199,9 +199,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
this.context.register(TestConfiguration.class);
this.context.refresh();
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping();
List<EndpointInfo<WebOperation>> endpoints = (List<EndpointInfo<WebOperation>>) handlerMapping
.getEndpoints();
List<String> endpointIds = endpoints.stream().map(EndpointInfo::getId)
Collection<ExposableWebEndpoint> endpoints = handlerMapping.getEndpoints();
List<String> endpointIds = endpoints.stream().map(ExposableEndpoint::getId)
.collect(Collectors.toList());
assertThat(endpointIds).contains("test");
}
@ -212,9 +211,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
this.context.register(TestConfiguration.class);
this.context.refresh();
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping();
List<EndpointInfo<WebOperation>> endpoints = (List<EndpointInfo<WebOperation>>) handlerMapping
.getEndpoints();
EndpointInfo<WebOperation> endpoint = endpoints.stream()
Collection<ExposableWebEndpoint> endpoints = handlerMapping.getEndpoints();
ExposableWebEndpoint endpoint = endpoints.stream()
.filter((candidate) -> "test".equals(candidate.getId())).findFirst()
.get();
assertThat(endpoint.getOperations()).hasSize(1);
@ -226,12 +224,10 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
public void healthEndpointInvokerShouldBeCloudFoundryWebExtension() {
setupContextWithCloudEnabled();
this.context.refresh();
Collection<EndpointInfo<WebOperation>> endpoints = getHandlerMapping()
.getEndpoints();
EndpointInfo<WebOperation> endpointInfo = endpoints.iterator().next();
WebOperation webOperation = endpointInfo.getOperations().iterator().next();
ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation
.getInvoker();
Collection<ExposableWebEndpoint> endpoints = getHandlerMapping().getEndpoints();
ExposableWebEndpoint endpoint = endpoints.iterator().next();
WebOperation webOperation = endpoint.getOperations().iterator().next();
Object invoker = ReflectionTestUtils.getField(webOperation, "invoker");
assertThat(ReflectionTestUtils.getField(invoker, "target"))
.isInstanceOf(CloudFoundryReactiveHealthEndpointWebExtension.class);
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -37,7 +37,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ReactiveCloudFoundrySecurityInterceptor}.
* Tests for {@link CloudFoundrySecurityInterceptor}.
*
* @author Madhura Bhave
*/
@ -49,12 +49,12 @@ public class ReactiveCloudFoundrySecurityInterceptorTests {
@Mock
private ReactiveCloudFoundrySecurityService securityService;
private ReactiveCloudFoundrySecurityInterceptor interceptor;
private CloudFoundrySecurityInterceptor interceptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
this.interceptor = new CloudFoundrySecurityInterceptor(
this.tokenValidator, this.securityService, "my-app-id");
}
@ -90,7 +90,7 @@ public class ReactiveCloudFoundrySecurityInterceptorTests {
@Test
public void preHandleWhenApplicationIdIsNullShouldReturnError() {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
this.interceptor = new CloudFoundrySecurityInterceptor(
this.tokenValidator, this.securityService, null);
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
@ -105,7 +105,7 @@ public class ReactiveCloudFoundrySecurityInterceptorTests {
@Test
public void preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnError() {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
this.interceptor = new CloudFoundrySecurityInterceptor(
this.tokenValidator, null, "my-app-id");
MockServerWebExchange request = MockServerWebExchange.from(MockServerHttpRequest
.get("/a").header(HttpHeaders.AUTHORIZATION, mockAccessToken()).build());

@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.After;
import org.junit.Before;
@ -29,11 +28,10 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.reflect.ReflectiveOperationInvoker;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
@ -43,6 +41,7 @@ import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfigu
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -99,8 +98,9 @@ public class CloudFoundryActuatorAutoConfigurationTests {
@Test
public void cloudFoundryPlatformActive() {
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
assertThat(handlerMapping.getEndpointMapping().getPath())
.isEqualTo("/cloudfoundryapplication");
EndpointMapping endpointMapping = (EndpointMapping) ReflectionTestUtils
.getField(handlerMapping, "endpointMapping");
assertThat(endpointMapping.getPath()).isEqualTo("/cloudfoundryapplication");
CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils
.getField(handlerMapping, "corsConfiguration");
assertThat(corsConfiguration.getAllowedOrigins()).contains("*");
@ -217,8 +217,7 @@ public class CloudFoundryActuatorAutoConfigurationTests {
this.context.register(TestConfiguration.class);
this.context.refresh();
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
List<EndpointInfo<WebOperation>> endpoints = (List<EndpointInfo<WebOperation>>) handlerMapping
.getEndpoints();
Collection<ExposableWebEndpoint> endpoints = handlerMapping.getEndpoints();
assertThat(endpoints.stream()
.filter((candidate) -> "test".equals(candidate.getId())).findFirst())
.isNotEmpty();
@ -231,9 +230,8 @@ public class CloudFoundryActuatorAutoConfigurationTests {
this.context.register(TestConfiguration.class);
this.context.refresh();
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
List<EndpointInfo<WebOperation>> endpoints = (List<EndpointInfo<WebOperation>>) handlerMapping
.getEndpoints();
EndpointInfo<WebOperation> endpoint = endpoints.stream()
Collection<ExposableWebEndpoint> endpoints = handlerMapping.getEndpoints();
ExposableWebEndpoint endpoint = endpoints.stream()
.filter((candidate) -> "test".equals(candidate.getId())).findFirst()
.get();
Collection<WebOperation> operations = endpoint.getOperations();
@ -249,14 +247,13 @@ public class CloudFoundryActuatorAutoConfigurationTests {
"vcap.application.cf_api:http://my-cloud-controller.com")
.applyTo(this.context);
this.context.refresh();
Collection<EndpointInfo<WebOperation>> endpoints = this.context
Collection<ExposableWebEndpoint> endpoints = this.context
.getBean("cloudFoundryWebEndpointServletHandlerMapping",
CloudFoundryWebEndpointServletHandlerMapping.class)
.getEndpoints();
EndpointInfo<WebOperation> endpointInfo = endpoints.iterator().next();
WebOperation webOperation = endpointInfo.getOperations().iterator().next();
ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation
.getInvoker();
ExposableWebEndpoint endpoint = endpoints.iterator().next();
WebOperation webOperation = endpoint.getOperations().iterator().next();
Object invoker = ReflectionTestUtils.getField(webOperation, "invoker");
assertThat(ReflectionTestUtils.getField(invoker, "target"))
.isInstanceOf(CloudFoundryHealthEndpointWebExtension.class);
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -27,17 +27,15 @@ import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
@ -204,7 +202,7 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
@Bean
public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
EndpointDiscoverer<WebOperation> webEndpointDiscoverer,
WebEndpointDiscoverer webEndpointDiscoverer,
EndpointMediaTypes endpointMediaTypes,
CloudFoundrySecurityInterceptor interceptor) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
@ -212,19 +210,19 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration, interceptor);
}
@Bean
public WebAnnotationEndpointDiscoverer webEndpointDiscoverer(
public WebEndpointDiscoverer webEndpointDiscoverer(
ApplicationContext applicationContext,
EndpointMediaTypes endpointMediaTypes) {
ParameterMapper parameterMapper = new ConversionServiceParameterMapper(
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
DefaultConversionService.getSharedInstance());
return new WebAnnotationEndpointDiscoverer(applicationContext,
parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null);
return new WebEndpointDiscoverer(applicationContext, parameterMapper,
endpointMediaTypes, EndpointPathResolver.useEndpointId(),
Collections.emptyList(), Collections.emptyList());
}
@Bean

@ -16,22 +16,20 @@
package org.springframework.boot.actuate.autoconfigure.endpoint;
import java.util.Collections;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ExposeExcludePropertyEndpointFilter}.
@ -43,12 +41,7 @@ public class ExposeExcludePropertyEndpointFilterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private MockEnvironment environment = new MockEnvironment();
private EndpointFilter<Operation> filter;
@Mock
private TestEndpointDiscoverer discoverer;
private ExposeExcludePropertyEndpointFilter<?> filter;
@Before
public void setup() {
@ -56,34 +49,33 @@ public class ExposeExcludePropertyEndpointFilterTests {
}
@Test
public void createWhenDiscovererTypeIsNullShouldThrowException() {
public void createWhenEndpointTypeIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Discoverer Type must not be null");
new ExposeExcludePropertyEndpointFilter<>(null, this.environment, "foo");
this.thrown.expectMessage("EndpointType must not be null");
new ExposeExcludePropertyEndpointFilter<>(null, new MockEnvironment(), "foo");
}
@Test
public void createWhenEnvironmentIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Environment must not be null");
new ExposeExcludePropertyEndpointFilter<>(TestEndpointDiscoverer.class, null,
"foo");
new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class, null, "foo");
}
@Test
public void createWhenPrefixIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Prefix must not be empty");
new ExposeExcludePropertyEndpointFilter<>(TestEndpointDiscoverer.class,
this.environment, null);
new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class,
new MockEnvironment(), null);
}
@Test
public void createWhenPrefixIsEmptyShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Prefix must not be empty");
new ExposeExcludePropertyEndpointFilter<>(TestEndpointDiscoverer.class,
this.environment, "");
new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class,
new MockEnvironment(), "");
}
@Test
@ -130,10 +122,11 @@ public class ExposeExcludePropertyEndpointFilterTests {
@Test
public void matchWhenDiscovererDoesNotMatchShouldMatch() {
this.environment.setProperty("foo.expose", "bar");
this.environment.setProperty("foo.exclude", "");
MockEnvironment environment = new MockEnvironment();
environment.setProperty("foo.expose", "bar");
environment.setProperty("foo.exclude", "");
this.filter = new ExposeExcludePropertyEndpointFilter<>(
DifferentTestEndpointDiscoverer.class, this.environment, "foo");
DifferentTestExposableWebEndpoint.class, environment, "foo");
assertThat(match("baz")).isTrue();
}
@ -154,25 +147,27 @@ public class ExposeExcludePropertyEndpointFilterTests {
}
private void setupFilter(String expose, String exclude) {
this.environment.setProperty("foo.expose", expose);
this.environment.setProperty("foo.exclude", exclude);
MockEnvironment environment = new MockEnvironment();
environment.setProperty("foo.expose", expose);
environment.setProperty("foo.exclude", exclude);
this.filter = new ExposeExcludePropertyEndpointFilter<>(
TestEndpointDiscoverer.class, this.environment, "foo", "def");
TestExposableWebEndpoint.class, environment, "foo", "def");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private boolean match(String id) {
EndpointInfo<Operation> info = new EndpointInfo<>(id, true,
Collections.emptyList());
return this.filter.match(info, this.discoverer);
ExposableEndpoint<?> endpoint = mock(TestExposableWebEndpoint.class);
given(endpoint.getId()).willReturn(id);
return ((EndpointFilter) this.filter).match(endpoint);
}
private abstract static class TestEndpointDiscoverer
implements EndpointDiscoverer<Operation> {
private abstract static class TestExposableWebEndpoint
implements ExposableWebEndpoint {
}
private abstract static class DifferentTestEndpointDiscoverer
implements EndpointDiscoverer<Operation> {
private abstract static class DifferentTestExposableWebEndpoint
implements ExposableWebEndpoint {
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -18,10 +18,8 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@ -124,11 +122,10 @@ public class ConditionalOnEnabledEndpointTests {
}
static class TestFilter implements EndpointFilter<Operation> {
static class TestFilter implements EndpointFilter<ExposableEndpoint<?>> {
@Override
public boolean match(EndpointInfo<Operation> info,
EndpointDiscoverer<Operation> discoverer) {
public boolean match(ExposableEndpoint<?> endpoint) {
return true;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -24,7 +24,7 @@ import javax.management.ObjectName;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBean;
import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.ObjectUtils;
@ -73,7 +73,7 @@ public class DefaultEndpointObjectNameFactoryTests {
@Test
public void generateObjectNameWithUniqueNames() {
this.properties.setUniqueNames(true);
EndpointMBean endpoint = endpoint("test");
ExposableJmxEndpoint endpoint = endpoint("test");
String id = ObjectUtils.getIdentityHexString(endpoint);
ObjectName objectName = generateObjectName(endpoint);
assertThat(objectName.toString()).isEqualTo(
@ -105,20 +105,20 @@ public class DefaultEndpointObjectNameFactoryTests {
}
private ObjectName generateObjectName(EndpointMBean endpointMBean) {
private ObjectName generateObjectName(ExposableJmxEndpoint endpoint) {
try {
return new DefaultEndpointObjectNameFactory(this.properties, this.mBeanServer,
this.contextId).generate(endpointMBean);
this.contextId).getObjectName(endpoint);
}
catch (MalformedObjectNameException ex) {
throw new AssertionError("Invalid object name", ex);
}
}
private EndpointMBean endpoint(String id) {
EndpointMBean endpointMBean = mock(EndpointMBean.class);
given(endpointMBean.getEndpointId()).willReturn(id);
return endpointMBean;
private ExposableJmxEndpoint endpoint(String id) {
ExposableJmxEndpoint endpoint = mock(ExposableJmxEndpoint.class);
given(endpoint.getId()).willReturn(id);
return endpoint;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -18,19 +18,16 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultEndpointPathProvider}.
@ -39,9 +36,6 @@ import static org.mockito.BDDMockito.given;
*/
public class DefaultEndpointPathProviderTests {
@Mock
private EndpointDiscoverer<WebOperation> discoverer;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
@ -78,13 +72,18 @@ public class DefaultEndpointPathProviderTests {
}
private DefaultEndpointPathProvider createProvider(String contextPath) {
Collection<EndpointInfo<WebOperation>> endpoints = new ArrayList<>();
endpoints.add(new EndpointInfo<>("foo", true, Collections.emptyList()));
endpoints.add(new EndpointInfo<>("bar", true, Collections.emptyList()));
given(this.discoverer.discoverEndpoints()).willReturn(endpoints);
WebEndpointProperties webEndpointProperties = new WebEndpointProperties();
webEndpointProperties.setBasePath(contextPath);
return new DefaultEndpointPathProvider(this.discoverer, webEndpointProperties);
Collection<ExposableWebEndpoint> endpoints = new ArrayList<>();
endpoints.add(mockEndpoint("foo"));
endpoints.add(mockEndpoint("bar"));
WebEndpointProperties properties = new WebEndpointProperties();
properties.setBasePath(contextPath);
return new DefaultEndpointPathProvider(properties, endpoints);
}
private ExposableWebEndpoint mockEndpoint(String id) {
ExposableWebEndpoint endpoint = mock(ExposableWebEndpoint.class);
given(endpoint.getId()).willReturn(id);
return endpoint;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.

@ -0,0 +1,72 @@
/*
* Copyright 2012-2018 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.actuate.endpoint;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
/**
* Abstract base class for {@link ExposableEndpoint} implementations.
*
* @param <O> The operation type.
* @author Phillip Webb
* @since 2.0.0
*/
public abstract class AbstractExposableEndpoint<O extends Operation>
implements ExposableEndpoint<O> {
private final String id;
private boolean enabledByDefault;
private List<O> operations;
/**
* Create a new {@link AbstractExposableEndpoint} instance.
* @param id the endpoint id
* @param enabledByDefault if the endpoint is enabled by default
* @param operations the endpoint operations
*/
public AbstractExposableEndpoint(String id, boolean enabledByDefault,
Collection<? extends O> operations) {
Assert.notNull(id, "ID must not be null");
Assert.notNull(operations, "Operations must not be null");
this.id = id;
this.enabledByDefault = enabledByDefault;
this.operations = Collections.unmodifiableList(new ArrayList<>(operations));
}
@Override
public String getId() {
return this.id;
}
@Override
public boolean isEnableByDefault() {
return this.enabledByDefault;
}
@Override
public Collection<O> getOperations() {
return this.operations;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,21 +17,20 @@
package org.springframework.boot.actuate.endpoint;
/**
* Strategy class that can be used to filter discovered endpoints.
* Strategy class that can be used to filter {@link ExposableEndpoint endpoints}.
*
* @author Phillip Webb
* @param <T> the type of the endpoint's operations
* @param <E> the endpoint type
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointFilter<T extends Operation> {
public interface EndpointFilter<E extends ExposableEndpoint<?>> {
/**
* Return {@code true} if the filter matches.
* @param info the endpoint info
* @param discoverer the endpoint discoverer
* @param endpoint the endpoint to check
* @return {@code true} if the filter matches
*/
boolean match(EndpointInfo<T> info, EndpointDiscoverer<T> discoverer);
boolean match(E endpoint);
}

@ -1,78 +0,0 @@
/*
* 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.actuate.endpoint;
import java.util.Collection;
import java.util.Collections;
import org.springframework.util.Assert;
/**
* 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 Operation> {
private final String id;
private final boolean enableByDefault;
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 enableByDefault if the endpoint is enabled by default
* @param operations the operations of the endpoint
*/
public EndpointInfo(String id, boolean enableByDefault, Collection<T> operations) {
Assert.hasText(id, "ID must not be empty");
Assert.notNull(operations, "Operations must not be null");
this.id = id;
this.enableByDefault = enableByDefault;
this.operations = Collections.unmodifiableCollection(operations);
}
/**
* Returns the id of the endpoint.
* @return the id
*/
public String getId() {
return this.id;
}
/**
* Returns if the endpoint is enabled by default.
* @return if the endpoint is enabled by default
*/
public boolean isEnableByDefault() {
return this.enableByDefault;
}
/**
* Returns the operations of the endpoint.
* @return the operations
*/
public Collection<T> getOperations() {
return this.operations;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -19,20 +19,20 @@ package org.springframework.boot.actuate.endpoint;
import java.util.Collection;
/**
* Discovers endpoints and provides an {@link EndpointInfo} for each of them.
* Provides access to a collection of {@link ExposableEndpoint endpoints}.
*
* @param <T> the type of the operation
* @param <E> the endpoint type
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointDiscoverer<T extends Operation> {
public interface EndpointsSupplier<E extends ExposableEndpoint<?>> {
/**
* Perform endpoint discovery.
* @return the discovered endpoints
* Return the provided endpoints.
* @return the endpoints
*/
Collection<EndpointInfo<T>> discoverEndpoints();
Collection<E> getEndpoints();
}

@ -0,0 +1,49 @@
/*
* Copyright 2012-2018 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.actuate.endpoint;
import java.util.Collection;
/**
* Information describing an endpoint that can be exposed in some technology specific way.
*
* @param <O> the type of the endpoint's operations
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public interface ExposableEndpoint<O extends Operation> {
/**
* Returns the id of the endpoint.
* @return the id
*/
String getId();
/**
* Returns if the endpoint is enabled by default.
* @return if the endpoint is enabled by default
*/
boolean isEnableByDefault();
/**
* Returns the operations of the endpoint.
* @return the operations
*/
Collection<O> getOperations();
}

@ -1,120 +0,0 @@
/*
* 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.actuate.endpoint;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Utility class that can be used to filter source data using a name regular expression.
* Detects if the name is classic "single value" key or a regular expression. Subclasses
* must provide implementations of {@link #getValue(Object, String)} and
* {@link #getNames(Object, NameCallback)}.
*
* @param <T> the source data type
* @author Phillip Webb
* @author Sergei Egorov
* @author Andy Wilkinson
* @author Dylian Bego
*/
abstract class NamePatternFilter<T> {
private static final String[] REGEX_PARTS = { "*", "$", "^", "+", "[" };
private final T source;
NamePatternFilter(T source) {
this.source = source;
}
public Map<String, Object> getResults(String name) {
Pattern pattern = compilePatternIfNecessary(name);
if (pattern == null) {
Object value = getValue(this.source, name);
Map<String, Object> result = new HashMap<>();
result.put(name, value);
return result;
}
ResultCollectingNameCallback resultCollector = new ResultCollectingNameCallback(
pattern);
getNames(this.source, resultCollector);
return resultCollector.getResults();
}
private Pattern compilePatternIfNecessary(String name) {
for (String part : REGEX_PARTS) {
if (name.contains(part)) {
try {
return Pattern.compile(name);
}
catch (PatternSyntaxException ex) {
return null;
}
}
}
return null;
}
protected abstract void getNames(T source, NameCallback callback);
protected abstract Object getValue(T source, String name);
protected abstract Object getOptionalValue(T source, String name);
/**
* Callback used to add a name.
*/
interface NameCallback {
void addName(String name);
}
/**
* {@link NameCallback} implementation to collect results.
*/
private class ResultCollectingNameCallback implements NameCallback {
private final Pattern pattern;
private final Map<String, Object> results = new LinkedHashMap<>();
ResultCollectingNameCallback(Pattern pattern) {
this.pattern = pattern;
}
@Override
public void addName(String name) {
if (this.pattern.matcher(name).matches()) {
Object value = getOptionalValue(NamePatternFilter.this.source, name);
if (value != null) {
this.results.put(name, value);
}
}
}
public Map<String, Object> getResults() {
return this.results;
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,58 +16,28 @@
package org.springframework.boot.actuate.endpoint;
import java.util.Map;
/**
* An operation on an endpoint.
* An operation on an {@link ExposableEndpoint endpoint}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class Operation {
private final OperationType type;
private final OperationInvoker invoker;
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 operationType the type of the operation
* @param invoker used to perform the operation
* @param blocking whether or not this is a blocking operation
*/
public Operation(OperationType operationType, OperationInvoker invoker,
boolean blocking) {
this.type = operationType;
this.invoker = invoker;
this.blocking = blocking;
}
public interface Operation {
/**
* Returns the {@link OperationType type} of the operation.
* @return the type
*/
public OperationType getType() {
return this.type;
}
/**
* Returns the {@code OperationInvoker} that can be used to invoke this endpoint
* operation.
* @return the operation invoker
*/
public OperationInvoker getInvoker() {
return this.invoker;
}
OperationType getType();
/**
* Whether or not this is a blocking operation.
*
* @return {@code true} if it is a blocking operation, otherwise {@code false}.
* 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}
*/
public boolean isBlocking() {
return this.blocking;
}
Object invoke(Map<String, Object> arguments);
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -21,6 +21,7 @@ package org.springframework.boot.actuate.endpoint;
*
* @author Andy Wilkinson
* @since 2.0.0
* @see Operation
*/
public enum OperationType {

@ -0,0 +1,71 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.AbstractExposableEndpoint;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* Abstract base class for {@link ExposableEndpoint endpoints} discovered by a
* {@link EndpointDiscoverer}.
*
* @param <O> The operation type
* @author Phillip Webb
* @since 2.0.0
*/
public abstract class AbstractDiscoveredEndpoint<O extends Operation>
extends AbstractExposableEndpoint<O> implements DiscoveredEndpoint<O> {
private final EndpointDiscoverer<?, ?> discoverer;
/**
* Create a mew {@link AbstractDiscoveredEndpoint} instance.
* @param discoverer the discoverer that discovered the endpoint
* @param id the ID of the endpoint
* @param enabledByDefault if the endpoint is enabled by default
* @param operations the endpoint operations
*/
public AbstractDiscoveredEndpoint(EndpointDiscoverer<?, ?> discoverer, String id,
boolean enabledByDefault, Collection<? extends O> operations) {
super(id, enabledByDefault, operations);
Assert.notNull(discoverer, "Discoverer must not be null");
Assert.notNull(discoverer, "EndpointBean must not be null");
this.discoverer = discoverer;
}
@Override
public boolean wasDiscoveredBy(Class<? extends EndpointDiscoverer<?, ?>> discoverer) {
return discoverer.isInstance(this.discoverer);
}
@Override
public String toString() {
ToStringCreator creator = new ToStringCreator(this).append("discoverer",
this.discoverer.getClass().getName());
appendFields(creator);
return creator.toString();
}
protected void appendFields(ToStringCreator creator) {
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.core.style.ToStringCreator;
/**
* Abstract base class for {@link Operation endpoints operations} discovered by a
* {@link EndpointDiscoverer}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public abstract class AbstractDiscoveredOperation implements Operation {
private final OperationMethod operationMethod;
private final OperationInvoker invoker;
/**
* Create a new {@link AbstractDiscoveredOperation} instance.
* @param operationMethod the method backing the operation
* @param invoker the operation invoker to use
*/
public AbstractDiscoveredOperation(DiscoveredOperationMethod operationMethod,
OperationInvoker invoker) {
this.operationMethod = operationMethod;
this.invoker = invoker;
}
public OperationMethod getOperationMethod() {
return this.operationMethod;
}
@Override
public OperationType getType() {
return this.operationMethod.getOperationType();
}
@Override
public Object invoke(Map<String, Object> arguments) {
return this.invoker.invoke(arguments);
}
@Override
public String toString() {
ToStringCreator creator = new ToStringCreator(this)
.append("operationMethod", this.operationMethod)
.append("invoker", this.invoker);
appendFields(creator);
return creator.toString();
}
protected void appendFields(ToStringCreator creator) {
}
}

@ -1,517 +0,0 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* A base {@link EndpointDiscoverer} implementation that discovers
* {@link Endpoint @Endpoint} beans and {@link EndpointExtension @EndpointExtension} beans
* in an application context.
*
* @param <K> the type of the operation key
* @param <T> the type of the operation
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public abstract class AnnotationEndpointDiscoverer<K, T extends Operation>
implements EndpointDiscoverer<T> {
private static final Log logger = LogFactory
.getLog(AnnotationEndpointDiscoverer.class);
private final ApplicationContext applicationContext;
private final Function<T, K> operationKeyFactory;
private final OperationsFactory<T> operationsFactory;
private final List<EndpointFilter<T>> filters;
/**
* Create a new {@link AnnotationEndpointDiscoverer} instance.
* @param applicationContext the application context
* @param operationFactory a factory used to create operations
* @param operationKeyFactory a factory used to create a key for an operation
* @param parameterMapper the {@link ParameterMapper} used to convert arguments when
* an operation is invoked
* @param invokerAdvisors advisors used to add additional invoker advise
* @param filters filters that must match for an endpoint to be exposed
*/
protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationFactory<T> operationFactory, Function<T, K> operationKeyFactory,
ParameterMapper parameterMapper,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<T>> filters) {
Assert.notNull(applicationContext, "Application Context must not be null");
Assert.notNull(operationFactory, "Operation Factory must not be null");
Assert.notNull(operationKeyFactory, "Operation Key Factory must not be null");
Assert.notNull(parameterMapper, "Parameter Mapper must not be null");
this.applicationContext = applicationContext;
this.operationKeyFactory = operationKeyFactory;
this.operationsFactory = new OperationsFactory<>(operationFactory,
parameterMapper, invokerAdvisors);
this.filters = (filters == null ? Collections.emptyList()
: new ArrayList<>(filters));
}
@Override
public final Collection<EndpointInfo<T>> discoverEndpoints() {
Class<T> operationType = getOperationType();
Map<Class<?>, DiscoveredEndpoint> endpoints = getEndpoints(operationType);
Map<Class<?>, DiscoveredExtension> extensions = getExtensions(operationType,
endpoints);
Collection<DiscoveredEndpoint> exposed = mergeExposed(endpoints, extensions);
verify(exposed);
return exposed.stream().map(DiscoveredEndpoint::getInfo)
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* Return the operation type being discovered. By default this method will resolve the
* class generic "{@code <T>}".
* @return the operation type
*/
@SuppressWarnings("unchecked")
protected Class<T> getOperationType() {
return (Class<T>) ResolvableType
.forClass(AnnotationEndpointDiscoverer.class, getClass())
.resolveGeneric(1);
}
private Map<Class<?>, DiscoveredEndpoint> getEndpoints(Class<T> operationType) {
Map<Class<?>, DiscoveredEndpoint> endpoints = new LinkedHashMap<>();
Map<String, DiscoveredEndpoint> endpointsById = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, Endpoint.class);
for (String beanName : beanNames) {
addEndpoint(endpoints, endpointsById, beanName);
}
return endpoints;
}
private void addEndpoint(Map<Class<?>, DiscoveredEndpoint> endpoints,
Map<String, DiscoveredEndpoint> endpointsById, String beanName) {
Class<?> endpointType = this.applicationContext.getType(beanName);
Object target = this.applicationContext.getBean(beanName);
DiscoveredEndpoint endpoint = createEndpoint(target, endpointType);
String id = endpoint.getInfo().getId();
DiscoveredEndpoint previous = endpointsById.putIfAbsent(id, endpoint);
Assert.state(previous == null, () -> "Found two endpoints with the id '" + id
+ "': " + endpoint + " and " + previous);
endpoints.put(endpointType, endpoint);
}
private DiscoveredEndpoint createEndpoint(Object target, Class<?> endpointType) {
AnnotationAttributes annotationAttributes = AnnotatedElementUtils
.findMergedAnnotationAttributes(endpointType, Endpoint.class, true, true);
String id = annotationAttributes.getString("id");
Assert.state(StringUtils.hasText(id),
"No @Endpoint id attribute specified for " + endpointType.getName());
boolean enabledByDefault = (Boolean) annotationAttributes.get("enableByDefault");
Collection<T> operations = this.operationsFactory
.createOperations(id, target, endpointType).values();
EndpointInfo<T> endpointInfo = new EndpointInfo<>(id, enabledByDefault,
operations);
boolean exposed = isEndpointExposed(endpointType, endpointInfo);
return new DiscoveredEndpoint(endpointType, endpointInfo, exposed);
}
private Map<Class<?>, DiscoveredExtension> getExtensions(Class<T> operationType,
Map<Class<?>, DiscoveredEndpoint> endpoints) {
Map<Class<?>, DiscoveredExtension> extensions = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, EndpointExtension.class);
for (String beanName : beanNames) {
addExtension(endpoints, extensions, beanName);
}
return extensions;
}
private void addExtension(Map<Class<?>, DiscoveredEndpoint> endpoints,
Map<Class<?>, DiscoveredExtension> extensions, String beanName) {
Class<?> extensionType = this.applicationContext.getType(beanName);
Class<?> endpointType = getEndpointType(extensionType);
DiscoveredEndpoint endpoint = getExtendingEndpoint(endpoints, extensionType,
endpointType);
if (isExtensionExposed(endpointType, extensionType, endpoint.getInfo())) {
Assert.state(endpoint.isExposed() || isEndpointFiltered(endpoint.getInfo()),
() -> "Invalid extension " + extensionType.getName() + "': endpoint '"
+ endpointType.getName()
+ "' does not support such extension");
Object target = this.applicationContext.getBean(beanName);
Map<Method, T> operations = this.operationsFactory
.createOperations(endpoint.getInfo().getId(), target, extensionType);
DiscoveredExtension extension = new DiscoveredExtension(extensionType,
operations.values());
DiscoveredExtension previous = extensions.putIfAbsent(endpointType,
extension);
Assert.state(previous == null,
() -> "Found two extensions for the same endpoint '"
+ endpointType.getName() + "': "
+ extension.getExtensionType().getName() + " and "
+ previous.getExtensionType().getName());
}
}
private Class<?> getEndpointType(Class<?> extensionType) {
AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(extensionType, EndpointExtension.class);
Class<?> endpointType = attributes.getClass("endpoint");
Assert.state(!endpointType.equals(Void.class), () -> "Extension "
+ endpointType.getName() + " does not specify an endpoint");
return endpointType;
}
private DiscoveredEndpoint getExtendingEndpoint(
Map<Class<?>, DiscoveredEndpoint> endpoints, Class<?> extensionType,
Class<?> endpointType) {
DiscoveredEndpoint endpoint = endpoints.get(endpointType);
Assert.state(endpoint != null,
() -> "Invalid extension '" + extensionType.getName()
+ "': no endpoint found with type '" + endpointType.getName()
+ "'");
return endpoint;
}
private boolean isEndpointExposed(Class<?> endpointType,
EndpointInfo<T> endpointInfo) {
if (isEndpointFiltered(endpointInfo)) {
return false;
}
AnnotationAttributes annotationAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(endpointType, FilteredEndpoint.class);
if (annotationAttributes == null) {
return true;
}
Class<?> filterClass = annotationAttributes.getClass("value");
return isFilterMatch(filterClass, endpointInfo);
}
private boolean isEndpointFiltered(EndpointInfo<T> endpointInfo) {
for (EndpointFilter<T> filter : this.filters) {
if (!isFilterMatch(filter, endpointInfo)) {
return true;
}
}
return false;
}
/**
* Determines if an extension is exposed.
* @param endpointType the endpoint type
* @param extensionType the extension type
* @param endpointInfo the endpoint info
* @return if the extension is exposed
*/
protected boolean isExtensionExposed(Class<?> endpointType, Class<?> extensionType,
EndpointInfo<T> endpointInfo) {
AnnotationAttributes annotationAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(extensionType, EndpointExtension.class);
Class<?> filterClass = annotationAttributes.getClass("filter");
return isFilterMatch(filterClass, endpointInfo);
}
@SuppressWarnings("unchecked")
private boolean isFilterMatch(Class<?> filterClass, EndpointInfo<T> endpointInfo) {
Class<?> generic = ResolvableType.forClass(EndpointFilter.class, filterClass)
.resolveGeneric(0);
if (generic == null || generic.isAssignableFrom(getOperationType())) {
EndpointFilter<T> filter = (EndpointFilter<T>) BeanUtils
.instantiateClass(filterClass);
return isFilterMatch(filter, endpointInfo);
}
return false;
}
private boolean isFilterMatch(EndpointFilter<T> filter,
EndpointInfo<T> endpointInfo) {
try {
return filter.match(endpointInfo, this);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(endpointInfo.getClass().getName())) {
// Possibly a lambda-defined EndpointFilter which we could not resolve the
// generic EndpointInfo type for
if (logger.isDebugEnabled()) {
logger.debug(
"Non-matching EndpointInfo for EndpointFilter: " + filter,
ex);
}
return false;
}
throw ex;
}
}
private Collection<DiscoveredEndpoint> mergeExposed(
Map<Class<?>, DiscoveredEndpoint> endpoints,
Map<Class<?>, DiscoveredExtension> extensions) {
List<DiscoveredEndpoint> result = new ArrayList<>();
endpoints.forEach((endpointClass, endpoint) -> {
if (endpoint.isExposed()) {
DiscoveredExtension extension = extensions.remove(endpointClass);
result.add(endpoint.merge(extension));
}
});
return result;
}
/**
* Allows subclasses to verify that the descriptors are correctly configured.
* @param exposedEndpoints the discovered endpoints to verify before exposing
*/
protected void verify(Collection<DiscoveredEndpoint> exposedEndpoints) {
}
/**
* A discovered endpoint (which may not be valid and might not ultimately be exposed).
*/
protected final class DiscoveredEndpoint {
private final EndpointInfo<T> info;
private final boolean exposed;
private final Map<OperationKey, List<T>> operations;
private DiscoveredEndpoint(Class<?> type, EndpointInfo<T> info, boolean exposed) {
Assert.notNull(info, "Info must not be null");
this.info = info;
this.exposed = exposed;
this.operations = indexEndpointOperations(type, info);
}
private Map<OperationKey, List<T>> indexEndpointOperations(Class<?> endpointType,
EndpointInfo<T> info) {
return Collections.unmodifiableMap(
indexOperations(info.getId(), endpointType, info.getOperations()));
}
private DiscoveredEndpoint(EndpointInfo<T> info, boolean exposed,
Map<OperationKey, List<T>> operations) {
Assert.notNull(info, "Info must not be null");
this.info = info;
this.exposed = exposed;
this.operations = operations;
}
/**
* Return the {@link EndpointInfo} for the discovered endpoint.
* @return the endpoint info
*/
public EndpointInfo<T> getInfo() {
return this.info;
}
/**
* Return {@code true} if the endpoint is exposed.
* @return if the is exposed
*/
private boolean isExposed() {
return this.exposed;
}
/**
* Return all operation that were discovered. These might be different to the ones
* that are in {@link #getInfo()}.
* @return the endpoint operations
*/
public Map<OperationKey, List<T>> getOperations() {
return this.operations;
}
/**
* Find any duplicate operations.
* @return any duplicate operations
*/
public Map<OperationKey, List<T>> findDuplicateOperations() {
return this.operations.entrySet().stream()
.filter((entry) -> entry.getValue().size() > 1)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (u, v) -> v,
LinkedHashMap::new));
}
private DiscoveredEndpoint merge(DiscoveredExtension extension) {
if (extension == null) {
return this;
}
Map<OperationKey, List<T>> operations = mergeOperations(extension);
EndpointInfo<T> info = new EndpointInfo<>(this.info.getId(),
this.info.isEnableByDefault(), flatten(operations).values());
return new DiscoveredEndpoint(info, this.exposed, operations);
}
private Map<OperationKey, List<T>> mergeOperations(
DiscoveredExtension extension) {
MultiValueMap<OperationKey, T> operations = new LinkedMultiValueMap<>(
this.operations);
operations.addAll(indexOperations(getInfo().getId(),
extension.getExtensionType(), extension.getOperations()));
return Collections.unmodifiableMap(operations);
}
private Map<K, T> flatten(Map<OperationKey, List<T>> operations) {
Map<K, T> flattened = new LinkedHashMap<>();
operations.forEach((operationKey, value) -> flattened
.put(operationKey.getKey(), getLastValue(value)));
return Collections.unmodifiableMap(flattened);
}
private T getLastValue(List<T> value) {
return value.get(value.size() - 1);
}
private MultiValueMap<OperationKey, T> indexOperations(String endpointId,
Class<?> target, Collection<T> operations) {
LinkedMultiValueMap<OperationKey, T> result = new LinkedMultiValueMap<>();
operations.forEach((operation) -> {
K key = getOperationKey(operation);
result.add(new OperationKey(endpointId, target, key), operation);
});
return result;
}
private K getOperationKey(T operation) {
return AnnotationEndpointDiscoverer.this.operationKeyFactory.apply(operation);
}
@Override
public String toString() {
return getInfo().toString();
}
}
/**
* A discovered extension.
*/
protected final class DiscoveredExtension {
private final Class<?> extensionType;
private final Collection<T> operations;
private DiscoveredExtension(Class<?> extensionType, Collection<T> operations) {
this.extensionType = extensionType;
this.operations = operations;
}
public Class<?> getExtensionType() {
return this.extensionType;
}
public Collection<T> getOperations() {
return this.operations;
}
@Override
public String toString() {
return this.extensionType.getName();
}
}
/**
* Define the key of an operation in the context of an operation's implementation.
*/
protected final class OperationKey {
private final String endpointId;
private final Class<?> target;
private final K key;
public OperationKey(String endpointId, Class<?> target, K key) {
this.endpointId = endpointId;
this.target = target;
this.key = key;
}
public K getKey() {
return this.key;
}
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OperationKey other = (OperationKey) o;
Boolean result = true;
result = result && this.endpointId.equals(other.endpointId);
result = result && this.target.equals(other.target);
result = result && this.key.equals(other.key);
return result;
}
@Override
public int hashCode() {
int result = this.endpointId.hashCode();
result = 31 * result + this.target.hashCode();
result = 31 * result + this.key.hashCode();
return result;
}
@Override
public String toString() {
return new ToStringCreator(this).append("endpointId", this.endpointId)
.append("target", this.target).append("key", this.key).toString();
}
}
}

@ -0,0 +1,38 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
/**
* An {@link ExposableEndpoint endpoint} discovered by a {@link EndpointDiscoverer}.
*
* @param <O> The operation type
* @author Phillip Webb
* @since 2.0.0
*/
public interface DiscoveredEndpoint<O extends Operation> extends ExposableEndpoint<O> {
/**
* Return {@code true} if the endpoint was discovered by the specified discoverer.
* @param discoverer the discoverer type
* @return {@code true} if discovered using the specified discoverer
*/
boolean wasDiscoveredBy(Class<? extends EndpointDiscoverer<?, ?>> discoverer);
}

@ -0,0 +1,51 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.Assert;
/**
* A {@link OperationMethod} discovered by a {@link EndpointDiscoverer}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public class DiscoveredOperationMethod extends OperationMethod {
private final List<String> producesMediaTypes;
public DiscoveredOperationMethod(Method method, OperationType operationType,
AnnotationAttributes annotationAttributes) {
super(method, operationType);
Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null");
String[] produces = annotationAttributes.getStringArray("produces");
this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(produces));
}
public List<String> getProducesMediaTypes() {
return this.producesMediaTypes;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -18,7 +18,6 @@ package org.springframework.boot.actuate.endpoint.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
@ -26,12 +25,12 @@ import java.util.Map;
import java.util.Objects;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.reflect.ReflectiveOperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.boot.actuate.endpoint.invoke.reflect.ReflectiveOperationInvoker;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodIntrospector.MetadataLookup;
import org.springframework.core.annotation.AnnotatedElementUtils;
@ -39,14 +38,14 @@ import org.springframework.core.annotation.AnnotationAttributes;
/**
* Factory to creates an {@link Operation} for a annotated methods on an
* {@link Endpoint @Endpoint}.
* {@link Endpoint @Endpoint} or {@link EndpointExtension @EndpointExtension}.
*
* @param <T> The operation type
* @param <O> The operation type
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
*/
class OperationsFactory<T extends Operation> {
abstract class DiscoveredOperationsFactory<O extends Operation> {
private static final Map<OperationType, Class<? extends Annotation>> OPERATION_TYPES;
@ -58,56 +57,56 @@ class OperationsFactory<T extends Operation> {
OPERATION_TYPES = Collections.unmodifiableMap(operationTypes);
}
private final OperationFactory<T> operationFactory;
private final ParameterValueMapper parameterValueMapper;
private final ParameterMapper parameterMapper;
private final Collection<OperationInvokerAdvisor> invokerAdvisors;
private final Collection<OperationMethodInvokerAdvisor> invokerAdvisors;
OperationsFactory(OperationFactory<T> operationFactory,
ParameterMapper parameterMapper,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors) {
this.operationFactory = operationFactory;
this.parameterMapper = parameterMapper;
this.invokerAdvisors = (invokerAdvisors == null ? Collections.emptyList()
: new ArrayList<>(invokerAdvisors));
DiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper,
Collection<OperationInvokerAdvisor> invokerAdvisors) {
this.parameterValueMapper = parameterValueMapper;
this.invokerAdvisors = invokerAdvisors;
}
public Map<Method, T> createOperations(String id, Object target, Class<?> type) {
return MethodIntrospector.selectMethods(type,
(MetadataLookup<T>) (method) -> createOperation(id, target, method));
public Collection<O> createOperations(String id, Object target) {
return MethodIntrospector.selectMethods(target.getClass(),
(MetadataLookup<O>) (method) -> createOperation(id, target, method))
.values();
}
private T createOperation(String endpointId, Object target, Method method) {
private O createOperation(String endpointId, Object target, Method method) {
return OPERATION_TYPES.entrySet().stream()
.map((entry) -> createOperation(endpointId, target, method,
entry.getKey(), entry.getValue()))
.filter(Objects::nonNull).findFirst().orElse(null);
}
private T createOperation(String endpointId, Object target, Method method,
private O createOperation(String endpointId, Object target, Method method,
OperationType operationType, Class<? extends Annotation> annotationType) {
AnnotationAttributes annotationAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(method, annotationType);
if (annotationAttributes == null) {
return null;
}
OperationMethodInfo methodInfo = new OperationMethodInfo(method, operationType,
annotationAttributes);
OperationInvoker invoker = new ReflectiveOperationInvoker(target, methodInfo,
this.parameterMapper);
return this.operationFactory.createOperation(endpointId, methodInfo, target,
applyAdvisors(endpointId, methodInfo, invoker));
DiscoveredOperationMethod operationMethod = new DiscoveredOperationMethod(method,
operationType, annotationAttributes);
OperationInvoker invoker = new ReflectiveOperationInvoker(target, operationMethod,
this.parameterValueMapper);
invoker = applyAdvisors(endpointId, operationMethod, invoker);
return createOperation(endpointId, operationMethod, invoker);
}
private OperationInvoker applyAdvisors(String endpointId,
OperationMethodInfo methodInfo, OperationInvoker invoker) {
OperationMethod operationMethod, OperationInvoker invoker) {
if (this.invokerAdvisors != null) {
for (OperationMethodInvokerAdvisor advisor : this.invokerAdvisors) {
invoker = advisor.apply(endpointId, methodInfo, invoker);
for (OperationInvokerAdvisor advisor : this.invokerAdvisors) {
invoker = advisor.apply(endpointId, operationMethod.getOperationType(),
operationMethod.getParameters(), invoker);
}
}
return invoker;
}
protected abstract O createOperation(String endpointId,
DiscoveredOperationMethod operationMethod, OperationInvoker invoker);
}

@ -0,0 +1,48 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.util.Assert;
/**
* {@link EndpointFilter} the matches based on the {@link EndpointDiscoverer} the created
* the endpoint.
*
* @author Phillip Webb
*/
public abstract class DiscovererEndpointFilter
implements EndpointFilter<DiscoveredEndpoint<?>> {
private final Class<? extends EndpointDiscoverer<?, ?>> discoverer;
/**
* Create a new {@link DiscovererEndpointFilter} instance.
* @param discoverer the required discoverer
*/
protected DiscovererEndpointFilter(
Class<? extends EndpointDiscoverer<?, ?>> discoverer) {
Assert.notNull(discoverer, "Discoverer must not be null");
this.discoverer = discoverer;
}
@Override
public boolean match(DiscoveredEndpoint<?> endpoint) {
return endpoint.wasDiscoveredBy(this.discoverer);
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -44,7 +44,7 @@ import java.lang.annotation.Target;
* @since 2.0.0
* @see EndpointExtension
* @see FilteredEndpoint
* @see AnnotationEndpointDiscoverer
* @see EndpointDiscoverer
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

@ -0,0 +1,525 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* A Base for {@link EndpointsSupplier} implementations that discover
* {@link Endpoint @Endpoint} beans and {@link EndpointExtension @EndpointExtension} beans
* in an application context.
*
* @param <E> The endpoint type
* @param <O> The operation type
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O extends Operation>
implements EndpointsSupplier<E> {
private static final Log logger = LogFactory.getLog(EndpointDiscoverer.class);
private final ApplicationContext applicationContext;
private final Collection<EndpointFilter<E>> filters;
private final DiscoveredOperationsFactory<O> operationsFactory;
private final Map<EndpointBean, E> filterEndpoints = new ConcurrentHashMap<>();
private volatile Collection<E> endpoints;
/**
* Create a new {@link EndpointDiscoverer} instance.
* @param applicationContext the source application context
* @param parameterValueMapper the parameter value mapper
* @param invokerAdvisors invoker advisors to apply
* @param filters filters to apply
*/
public EndpointDiscoverer(ApplicationContext applicationContext,
ParameterValueMapper parameterValueMapper,
Collection<OperationInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<E>> filters) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(parameterValueMapper, "ParameterValueMapper must not be null");
Assert.notNull(invokerAdvisors, "InvokerAdvisors must not be null");
Assert.notNull(filters, "Filters must not be null");
this.applicationContext = applicationContext;
this.filters = Collections.unmodifiableCollection(filters);
this.operationsFactory = getOperationsFactory(parameterValueMapper,
invokerAdvisors);
}
private DiscoveredOperationsFactory<O> getOperationsFactory(
ParameterValueMapper parameterValueMapper,
Collection<OperationInvokerAdvisor> invokerAdvisors) {
return new DiscoveredOperationsFactory<O>(parameterValueMapper, invokerAdvisors) {
@Override
protected O createOperation(String endpointId,
DiscoveredOperationMethod operationMethod, OperationInvoker invoker) {
return EndpointDiscoverer.this.createOperation(endpointId,
operationMethod, invoker);
}
};
}
@Override
public final Collection<E> getEndpoints() {
if (this.endpoints == null) {
this.endpoints = discoverEndpoints();
}
return this.endpoints;
}
private Collection<E> discoverEndpoints() {
Collection<EndpointBean> endpointBeans = createEndpointBeans();
addExtensionBeans(endpointBeans);
return convertToEndpoints(endpointBeans);
}
private Collection<EndpointBean> createEndpointBeans() {
Map<String, EndpointBean> byId = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, Endpoint.class);
for (String beanName : beanNames) {
EndpointBean endpointBean = createEndpointBean(beanName);
EndpointBean previous = byId.putIfAbsent(endpointBean.getId(), endpointBean);
Assert.state(previous == null,
() -> "Found two endpoints with the id '" + endpointBean.getId()
+ "': '" + endpointBean.getBeanName() + "' and '"
+ previous.getBeanName() + "'");
}
return byId.values();
}
private EndpointBean createEndpointBean(String beanName) {
Object bean = this.applicationContext.getBean(beanName);
return new EndpointBean(beanName, bean);
}
private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
Map<?, EndpointBean> byType = endpointBeans.stream()
.collect(Collectors.toMap((bean) -> bean.getType(), (bean) -> bean));
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(
this.applicationContext, EndpointExtension.class);
for (String beanName : beanNames) {
ExtensionBean extensionBean = createExtensionBean(beanName);
EndpointBean endpointBean = byType.get(extensionBean.getEndpointType());
Assert.state(endpointBean != null,
() -> ("Invalid extension '" + extensionBean.getBeanName()
+ "': no endpoint found with type '"
+ extensionBean.getEndpointType().getName() + "'"));
addExtensionBean(endpointBean, extensionBean);
}
}
private ExtensionBean createExtensionBean(String beanName) {
Object bean = this.applicationContext.getBean(beanName);
return new ExtensionBean(beanName, bean);
}
private void addExtensionBean(EndpointBean endpointBean,
ExtensionBean extensionBean) {
if (isExtensionExposed(endpointBean, extensionBean)) {
Assert.state(
isEndpointExposed(endpointBean) || isEndpointFiltered(endpointBean),
() -> "Endpoint bean '" + endpointBean.getBeanName()
+ "' cannot support the extension bean '"
+ extensionBean.getBeanName() + "'");
endpointBean.addExtension(extensionBean);
}
}
private Collection<E> convertToEndpoints(Collection<EndpointBean> endpointBeans) {
Set<E> endpoints = new LinkedHashSet<>();
for (EndpointBean endpointBean : endpointBeans) {
if (isEndpointExposed(endpointBean)) {
endpoints.add(convertToEndpoint(endpointBean));
}
}
return Collections.unmodifiableSet(endpoints);
}
private E convertToEndpoint(EndpointBean endpointBean) {
MultiValueMap<OperationKey, O> indexed = new LinkedMultiValueMap<>();
String id = endpointBean.getId();
addOperations(indexed, id, endpointBean.getBean(), false);
if (endpointBean.getExtensions().size() > 1) {
String extensionBeans = endpointBean.getExtensions().stream()
.map(ExtensionBean::getBeanName).collect(Collectors.joining(", "));
throw new IllegalStateException(
"Found multiple extensions for the endpoint bean "
+ endpointBean.getBeanName() + " (" + extensionBeans + ")");
}
for (ExtensionBean extensionBean : endpointBean.getExtensions()) {
addOperations(indexed, id, extensionBean.getBean(), true);
}
assertNoDuplicateOperations(endpointBean, indexed);
List<O> operations = indexed.values().stream().map(this::getLast)
.filter(Objects::nonNull).collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList));
return createEndpoint(id, endpointBean.isEnabledByDefault(), operations);
}
private void addOperations(MultiValueMap<OperationKey, O> indexed, String id,
Object target, boolean replaceLast) {
Set<OperationKey> replacedLast = new HashSet<>();
Collection<O> operations = this.operationsFactory.createOperations(id, target);
for (O operation : operations) {
OperationKey key = createOperationKey(operation);
O last = getLast(indexed.get(key));
if (replaceLast && replacedLast.add(key) && last != null) {
indexed.get(key).remove(last);
}
indexed.add(key, operation);
}
}
private <T> T getLast(List<T> list) {
return CollectionUtils.isEmpty(list) ? null : list.get(list.size() - 1);
}
private void assertNoDuplicateOperations(EndpointBean endpointBean,
MultiValueMap<OperationKey, O> indexed) {
List<OperationKey> duplicates = indexed.entrySet().stream()
.filter((entry) -> entry.getValue().size() > 1).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!duplicates.isEmpty()) {
Set<ExtensionBean> extensions = endpointBean.getExtensions();
String extensionBeanNames = extensions.stream()
.map(ExtensionBean::getBeanName).collect(Collectors.joining(", "));
throw new IllegalStateException(
"Unable to map duplicate endpoint operations: "
+ duplicates.toString() + " to " + endpointBean.getBeanName()
+ (extensions.isEmpty() ? ""
: " (" + extensionBeanNames + ")"));
}
}
private boolean isExtensionExposed(EndpointBean endpointBean,
ExtensionBean extensionBean) {
return isFilterMatch(extensionBean.getFilter(), endpointBean)
&& isExtensionExposed(extensionBean.getBean());
}
/**
* Determine if an extension bean should be exposed. Subclasses can override this
* method to provide additional logic.
* @param extensionBean the extension bean
* @return {@code true} if the extension is exposed
*/
protected boolean isExtensionExposed(Object extensionBean) {
return true;
}
private boolean isEndpointExposed(EndpointBean endpointBean) {
return isFilterMatch(endpointBean.getFilter(), endpointBean)
&& !isEndpointFiltered(endpointBean);
}
private boolean isEndpointFiltered(EndpointBean endpointBean) {
for (EndpointFilter<E> filter : this.filters) {
if (!isFilterMatch(filter, endpointBean)) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private boolean isFilterMatch(Class<?> filter, EndpointBean endpointBean) {
if (filter == null) {
return true;
}
E endpoint = getFilterEndpoint(endpointBean);
Class<?> generic = ResolvableType.forClass(EndpointFilter.class, filter)
.resolveGeneric(0);
if (generic == null || generic.isInstance(endpoint)) {
EndpointFilter<E> instance = (EndpointFilter<E>) BeanUtils
.instantiateClass(filter);
return isFilterMatch(instance, endpoint);
}
return false;
}
private boolean isFilterMatch(EndpointFilter<E> filter, EndpointBean endpointBean) {
return isFilterMatch(filter, getFilterEndpoint(endpointBean));
}
private boolean isFilterMatch(EndpointFilter<E> filter, E endpoint) {
try {
return filter.match(endpoint);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(endpoint.getClass().getName())) {
// Possibly a lambda-defined EndpointFilter which we could not resolve the
// generic EndpointInfo type for
if (logger.isDebugEnabled()) {
logger.debug("Non-matching Endpoint for EndpointFilter: " + filter,
ex);
}
return false;
}
throw ex;
}
}
private E getFilterEndpoint(EndpointBean endpointBean) {
E endpoint = this.filterEndpoints.get(endpointBean);
if (endpoint == null) {
endpoint = createEndpoint(endpointBean.getId(),
endpointBean.isEnabledByDefault(), Collections.emptySet());
this.filterEndpoints.put(endpointBean, endpoint);
}
return endpoint;
}
@SuppressWarnings("unchecked")
protected Class<? extends E> getEndpointType() {
return (Class<? extends E>) ResolvableType
.forClass(EndpointDiscoverer.class, getClass()).resolveGeneric(0);
}
/**
* Factory method called to create the {@link ExposableEndpoint endpoint}.
* @param id the ID of the endpoint
* @param enabledByDefault if the endpoint is enabled by default
* @param operations the endpoint operations
* @return a created endpoint (a {@link DiscoveredEndpoint} is recommended)
*/
protected abstract E createEndpoint(String id, boolean enabledByDefault,
Collection<O> operations);
/**
* Factory method to create an {@link Operation endpoint operation}.
* @param endpointId the endpoint id
* @param operationMethod the operation method
* @param invoker the invoker to use
* @return a created operation
*/
protected abstract O createOperation(String endpointId,
DiscoveredOperationMethod operationMethod, OperationInvoker invoker);
/**
* Create a {@link OperationKey} for the given operation.
* @param operation the source operation
* @return the operation key
*/
protected abstract OperationKey createOperationKey(O operation);
/**
* A key generated for an {@link Operation} based on specific criteria from the actual
* operation implementation.
*/
protected static final class OperationKey {
private final Object key;
private final Supplier<String> description;
/**
* Create a new {@link OperationKey} instance.
* @param key the underlying key for the operation
* @param description a human readable description of the key
*/
public OperationKey(Object key, Supplier<String> description) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(description, "Description must not be null");
this.key = key;
this.description = description;
}
@Override
public int hashCode() {
return this.key.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.key.equals(((OperationKey) obj).key);
}
@Override
public String toString() {
return this.description.get();
}
}
/**
* Information about an {@link Endpoint @Endpoint} bean.
*/
private static class EndpointBean {
private final String beanName;
private final Object bean;
private final String id;
private boolean enabledByDefault;
private final Class<?> filter;
private Set<ExtensionBean> extensions = new LinkedHashSet<>();
EndpointBean(String beanName, Object bean) {
AnnotationAttributes attributes = AnnotatedElementUtils
.findMergedAnnotationAttributes(bean.getClass(), Endpoint.class, true,
true);
this.beanName = beanName;
this.bean = bean;
this.id = attributes.getString("id");
this.enabledByDefault = (Boolean) attributes.get("enableByDefault");
this.filter = getFilter(this.bean.getClass());
Assert.state(StringUtils.hasText(this.id),
"No @Endpoint id attribute specified for "
+ bean.getClass().getName());
}
public void addExtension(ExtensionBean extensionBean) {
this.extensions.add(extensionBean);
}
public Set<ExtensionBean> getExtensions() {
return this.extensions;
}
private Class<?> getFilter(Class<?> type) {
AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(type, FilteredEndpoint.class);
if (attributes == null) {
return null;
}
return attributes.getClass("value");
}
public String getBeanName() {
return this.beanName;
}
public Object getBean() {
return this.bean;
}
public String getId() {
return this.id;
}
public Class<?> getType() {
return this.bean.getClass();
}
public boolean isEnabledByDefault() {
return this.enabledByDefault;
}
public Class<?> getFilter() {
return this.filter;
}
}
/**
* Information about an {@link EndpointExtension EndpointExtension} bean.
*/
private static class ExtensionBean {
private final String beanName;
private final Object bean;
private final Class<?> endpointType;
private final Class<?> filter;
ExtensionBean(String beanName, Object bean) {
AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(bean.getClass(),
EndpointExtension.class);
this.beanName = beanName;
this.bean = bean;
this.endpointType = attributes.getClass("endpoint");
this.filter = attributes.getClass("filter");
Assert.state(!this.endpointType.equals(Void.class), () -> "Extension "
+ this.endpointType.getName() + " does not specify an endpoint");
}
public String getBeanName() {
return this.beanName;
}
public Object getBean() {
return this.bean;
}
public Class<?> getEndpointType() {
return this.endpointType;
}
public Class<?> getFilter() {
return this.filter;
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -41,6 +41,7 @@ import org.springframework.boot.actuate.endpoint.EndpointFilter;
* } </pre>
* @author Phillip Webb
* @since 2.0.0
* @see DiscovererEndpointFilter
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

@ -1,46 +0,0 @@
/*
* 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.actuate.endpoint.annotation;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo;
/**
* Factory to creates an {@link Operation} for an annotated method on an
* {@link Endpoint @Endpoint}.
*
* @param <T> the {@link Operation} type
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
*/
@FunctionalInterface
public interface OperationFactory<T extends Operation> {
/**
* Creates an {@link Operation} for an operation on an endpoint.
* @param endpointId the id of the endpoint
* @param target the target that implements the operation
* @param methodInfo the method on the bean that implements the operation
* @param invoker the invoker that should be used for the operation
* @return the operation info that describes the operation
*/
T createOperation(String endpointId, OperationMethodInfo methodInfo, Object target,
OperationInvoker invoker);
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke;
import java.util.Set;
@ -25,24 +25,25 @@ import org.springframework.util.StringUtils;
* parameters.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.0.0
*/
public class ParametersMissingException extends RuntimeException {
public class MissingParametersException extends RuntimeException {
private final Set<String> parameters;
private final Set<OperationParameter> missingParameters;
public ParametersMissingException(Set<String> parameters) {
public MissingParametersException(Set<OperationParameter> missingParameters) {
super("Failed to invoke operation because the following required "
+ "parameters were missing: "
+ StringUtils.collectionToCommaDelimitedString(parameters));
this.parameters = parameters;
+ StringUtils.collectionToCommaDelimitedString(missingParameters));
this.missingParameters = missingParameters;
}
/**
* Returns the parameters that were missing.
* @return the parameters
*/
public Set<String> getParameters() {
return this.parameters;
public Set<OperationParameter> getMissingParameters() {
return this.missingParameters;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,14 +14,15 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint;
package org.springframework.boot.actuate.endpoint.invoke;
import java.util.Map;
/**
* An {@code OperationInvoker} is used to invoke an operation on an endpoint.
* Interface to perform an operation invocation.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
@FunctionalInterface
@ -31,7 +32,8 @@ 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}
* @throws MissingParametersException if parameters are missing
*/
Object invoke(Map<String, Object> arguments);
Object invoke(Map<String, Object> arguments) throws MissingParametersException;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,28 +14,28 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* Allows additional functionality to be applied to an {@link OperationInvoker} being used
* to invoke a {@link OperationMethodInfo operation method}.
* Allows additional functionality to be applied to an {@link OperationInvoker}.
*
* @author Phillip Webb
* @since 2.0.0
*/
@FunctionalInterface
public interface OperationMethodInvokerAdvisor {
public interface OperationInvokerAdvisor {
/**
* Apply additional functionality to the given invoker.
* @param endpointId the endpoint ID
* @param methodInfo the method information
* @param operationType the operation type
* @param parameters the operation parameters
* @param invoker the invoker to advise
* @return an potentially new operation invoker with support for additional features.
* @return an potentially new operation invoker with support for additional features
*/
OperationInvoker apply(String endpointId, OperationMethodInfo methodInfo,
OperationInvoker invoker);
OperationInvoker apply(String endpointId, OperationType operationType,
OperationParameters parameters, OperationInvoker invoker);
}

@ -0,0 +1,45 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.invoke;
/**
* A single operation parameter.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface OperationParameter {
/**
* Returns the parameter name.
* @return the name
*/
String getName();
/**
* Returns the parameter type.
* @return the type
*/
Class<?> getType();
/**
* Return if the parameter accepts null values.
* @return if the parameter is nullable
*/
boolean isNullable();
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.invoke;
import java.util.stream.Stream;
/**
* A collection of {@link OperationParameter operation parameters}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface OperationParameters extends Iterable<OperationParameter> {
/**
* Return {@code true} if there is at least one parameter.
* @return if there are parameters
*/
default boolean hasParameters() {
return getParameterCount() > 0;
}
/**
* Return the total number of parameters.
* @return the total number of parameters
*/
int getParameterCount();
/**
* Return the parameter at the specified index.
* @param index the parameter index
* @return the paramter
*/
OperationParameter get(int index);
/**
* Return a stream of the contained paramteres.
* @return a stream of the parameters
*/
Stream<OperationParameter> stream();
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,50 +14,50 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke;
/**
* A {@code ParameterMappingException} is thrown when a failure occurs during
* {@link ParameterMapper#mapParameter(Object, Class) operation parameter mapping}.
* {@link ParameterValueMapper#mapParameterValue operation parameter mapping}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class ParameterMappingException extends RuntimeException {
private final Object input;
private final OperationParameter parameter;
private final Class<?> type;
private final Object value;
/**
* 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 parameter the parameter being mapping
* @param value the value being mapped
* @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;
public ParameterMappingException(OperationParameter parameter, Object value,
Throwable cause) {
super("Failed to map " + value + " of type " + value.getClass() + " to "
+ parameter, cause);
this.parameter = parameter;
this.value = value;
}
/**
* Returns the input that was to be mapped.
* @return the input
* Return the parameter being mapped.
* @return the parameter
*/
public Object getInput() {
return this.input;
public OperationParameter getParameter() {
return this.parameter;
}
/**
* Returns the type to be mapped to.
* @return the type
* Return the value being mapped.
* @return the value
*/
public Class<?> getType() {
return this.type;
public Object getValue() {
return this.value;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,25 +14,25 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke;
/**
* Maps parameters to the required type when invoking an endpoint.
* Maps parameter values to the required type when invoking an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface ParameterMapper {
public interface ParameterValueMapper {
/**
* Map the specified {@code input} parameter to the given {@code parameterType}.
* @param parameter the parameter to map
* @param value a parameter value
* @param type 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 value, Class<T> type);
Object mapParameterValue(OperationParameter parameter, Object value)
throws ParameterMappingException;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,49 +14,56 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.convert;
package org.springframework.boot.actuate.endpoint.invoke.convert;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
/**
* {@link ParameterMapper} that uses a {@link ConversionService} to map parameter values
* if necessary.
* {@link ParameterValueMapper} backed by a {@link ConversionService}.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public class ConversionServiceParameterMapper implements ParameterMapper {
public class ConversionServiceParameterValueMapper implements ParameterValueMapper {
private final ConversionService conversionService;
public ConversionServiceParameterMapper() {
this(createDefaultConversionService());
/**
* Create a new {@link ConversionServiceParameterValueMapper} instance.
*/
public ConversionServiceParameterValueMapper() {
this(createConversionService());
}
/**
* Create a new instance with the {@link ConversionService} to use.
* Create a new {@link ConversionServiceParameterValueMapper} instance backed by a
* specific conversion service.
* @param conversionService the conversion service
*/
public ConversionServiceParameterMapper(ConversionService conversionService) {
public ConversionServiceParameterValueMapper(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService must not be null");
this.conversionService = new BinderConversionService(conversionService);
}
@Override
public <T> T mapParameter(Object input, Class<T> parameterType) {
public Object mapParameterValue(OperationParameter parameter, Object value)
throws ParameterMappingException {
try {
return this.conversionService.convert(input, parameterType);
return this.conversionService.convert(value, parameter.getType());
}
catch (Exception ex) {
throw new ParameterMappingException(input, parameterType, ex);
throw new ParameterMappingException(parameter, value, ex);
}
}
private static ConversionService createDefaultConversionService() {
private static ConversionService createConversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
IsoOffsetDateTimeConverter.registerConverter(conversionService);
return conversionService;

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.convert;
package org.springframework.boot.actuate.endpoint.invoke.convert;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,4 +17,4 @@
/**
* Converter support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.convert;
package org.springframework.boot.actuate.endpoint.invoke.convert;

@ -0,0 +1,20 @@
/*
* Copyright 2012-2018 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.
*/
/**
* Interfaces and classes relating to invoking operation methods.
*/
package org.springframework.boot.actuate.endpoint.invoke;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,17 +14,14 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke.reflect;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.Assert;
/**
@ -34,7 +31,7 @@ import org.springframework.util.Assert;
* @since 2.0.0
* @see ReflectiveOperationInvoker
*/
public final class OperationMethodInfo {
public class OperationMethod {
private static final ParameterNameDiscoverer DEFAULT_PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@ -42,18 +39,20 @@ public final class OperationMethodInfo {
private final OperationType operationType;
private final AnnotationAttributes annotationAttributes;
private final OperationParameters operationParameters;
private final ParameterNameDiscoverer parameterNameDiscoverer = DEFAULT_PARAMETER_NAME_DISCOVERER;
public OperationMethodInfo(Method method, OperationType operationType,
AnnotationAttributes annotationAttributes) {
/**
* Create a new {@link OperationMethod} instance.
* @param method the source method
* @param operationType the operation type
*/
public OperationMethod(Method method, OperationType operationType) {
Assert.notNull(method, "Method must not be null");
Assert.notNull(operationType, "Operation Type must not be null");
Assert.notNull(annotationAttributes, "Annotation Attributes must not be null");
Assert.notNull(operationType, "OperationType must not be null");
this.method = method;
this.operationType = operationType;
this.annotationAttributes = annotationAttributes;
this.operationParameters = new OperationMethodParameters(method,
DEFAULT_PARAMETER_NAME_DISCOVERER);
}
/**
@ -73,27 +72,17 @@ public final class OperationMethodInfo {
}
/**
* Return the mime type that the operation produces.
* @return the produced mime type
* Return the operation parameters.
* @return the operation parameters
*/
public String[] getProduces() {
return this.annotationAttributes.getStringArray("produces");
public OperationParameters getParameters() {
return this.operationParameters;
}
/**
* Return a map of method parameters with the key being the discovered parameter name.
* @return the method parameters
*/
public Map<String, Parameter> getParameters() {
Parameter[] parameters = this.method.getParameters();
String[] names = this.parameterNameDiscoverer.getParameterNames(this.method);
Assert.state(names != null,
"Failed to extract parameter names for " + this.method);
Map<String, Parameter> result = new LinkedHashMap<>();
for (int i = 0; i < names.length; i++) {
result.put(names[i], parameters[i]);
}
return result;
@Override
public String toString() {
return "Operation " + this.operationType.name().toLowerCase() + " method "
+ this.method;
}
}

@ -0,0 +1,66 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.invoke.reflect;
import java.lang.reflect.Parameter;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* {@link OperationParameter} created from an {@link OperationMethod}.
*
* @author Phillip Webb
*/
class OperationMethodParameter implements OperationParameter {
private final String name;
private final Parameter parameter;
/**
* Create a new {@link OperationMethodParameter} instance.
* @param name the parameter name
* @param parameter the parameter
*/
OperationMethodParameter(String name, Parameter parameter) {
this.name = name;
this.parameter = parameter;
}
@Override
public String getName() {
return this.name;
}
@Override
public Class<?> getType() {
return this.parameter.getType();
}
@Override
public boolean isNullable() {
return !ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class));
}
@Override
public String toString() {
return this.name + " of type " + this.parameter.getType().getName();
}
}

@ -0,0 +1,88 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.invoke.reflect;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.util.Assert;
/**
* {@link OperationParameters} created from an {@link OperationMethod}.
*
* @author Phillip Webb
*/
class OperationMethodParameters implements OperationParameters {
private final List<OperationParameter> operationParameters;
/**
* Create a new {@link OperationMethodParameters} instance.
* @param method the source method
* @param parameterNameDiscoverer the parameter name discoverer
*/
OperationMethodParameters(Method method,
ParameterNameDiscoverer parameterNameDiscoverer) {
Assert.notNull(method, "Method must not be null");
Assert.notNull(parameterNameDiscoverer,
"ParameterNameDiscoverer must not be null");
String[] paramterNames = parameterNameDiscoverer.getParameterNames(method);
Parameter[] parameters = method.getParameters();
Assert.state(paramterNames != null,
"Failed to extract parameter names for " + method);
this.operationParameters = getOperationParameters(parameters, paramterNames);
}
private List<OperationParameter> getOperationParameters(Parameter[] parameters,
String[] names) {
List<OperationParameter> operationParameters = new ArrayList<>(parameters.length);
for (int i = 0; i < names.length; i++) {
operationParameters
.add(new OperationMethodParameter(names[i], parameters[i]));
}
return Collections.unmodifiableList(operationParameters);
}
@Override
public int getParameterCount() {
return this.operationParameters.size();
}
@Override
public OperationParameter get(int index) {
return this.operationParameters.get(index);
}
@Override
public Iterator<OperationParameter> iterator() {
return this.operationParameters.iterator();
}
@Override
public Stream<OperationParameter> stream() {
return this.operationParameters.stream();
}
}

@ -0,0 +1,111 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.invoke.reflect;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* An {@code OperationInvoker} that invokes an operation using reflection.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public class ReflectiveOperationInvoker implements OperationInvoker {
private final Object target;
private final OperationMethod operationMethod;
private final ParameterValueMapper parameterValueMapper;
/**
* 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 and the given
* {@code parameterNameMapper} will be used map parameters by name.
* @param target the target of the reflective call
* @param operationMethod the method info
* @param parameterValueMapper the parameter mapper
*/
public ReflectiveOperationInvoker(Object target, OperationMethod operationMethod,
ParameterValueMapper parameterValueMapper) {
Assert.notNull(target, "Target must not be null");
Assert.notNull(operationMethod, "OperationMethod must not be null");
Assert.notNull(parameterValueMapper, "ParameterValueMapper must not be null");
ReflectionUtils.makeAccessible(operationMethod.getMethod());
this.target = target;
this.operationMethod = operationMethod;
this.parameterValueMapper = parameterValueMapper;
}
@Override
public Object invoke(Map<String, Object> arguments) {
validateRequiredParameters(arguments);
Method method = this.operationMethod.getMethod();
Object[] resolvedArguments = resolveArguments(arguments);
ReflectionUtils.makeAccessible(method);
return ReflectionUtils.invokeMethod(method, this.target, resolvedArguments);
}
private void validateRequiredParameters(Map<String, Object> arguments) {
Set<OperationParameter> missing = this.operationMethod.getParameters().stream()
.filter((parameter) -> isMissing(arguments, parameter))
.collect(Collectors.toSet());
if (!missing.isEmpty()) {
throw new MissingParametersException(missing);
}
}
private boolean isMissing(Map<String, Object> arguments,
OperationParameter parameter) {
if (parameter.isNullable()) {
return false;
}
return arguments.get(parameter.getName()) == null;
}
private Object[] resolveArguments(Map<String, Object> arguments) {
return this.operationMethod.getParameters().stream()
.map((parameter) -> resolveArgument(parameter, arguments)).toArray();
}
private Object resolveArgument(OperationParameter parameter,
Map<String, Object> arguments) {
Object value = arguments.get(parameter.getName());
return this.parameterValueMapper.mapParameterValue(parameter, value);
}
@Override
public String toString() {
return new ToStringCreator(this).append("target", this.target)
.append("method", this.operationMethod).toString();
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,4 +17,4 @@
/**
* Endpoint reflection support.
*/
package org.springframework.boot.actuate.endpoint.reflect;
package org.springframework.boot.actuate.endpoint.invoke.reflect;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.cache;
package org.springframework.boot.actuate.endpoint.invoker.cache;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.util.Assert;
/**
@ -30,7 +30,7 @@ import org.springframework.util.Assert;
*/
public class CachingOperationInvoker implements OperationInvoker {
private final OperationInvoker target;
private final OperationInvoker invoker;
private final long timeToLive;
@ -39,12 +39,12 @@ public class CachingOperationInvoker implements OperationInvoker {
/**
* 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 invoker 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;
CachingOperationInvoker(OperationInvoker invoker, long timeToLive) {
Assert.isTrue(timeToLive > 0, "TimeToLive must be strictly positive");
this.invoker = invoker;
this.timeToLive = timeToLive;
}
@ -61,7 +61,7 @@ public class CachingOperationInvoker implements OperationInvoker {
long accessTime = System.currentTimeMillis();
CachedResponse cached = this.cachedResponse;
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
Object response = this.target.invoke(arguments);
Object response = this.invoker.invoke(arguments);
this.cachedResponse = new CachedResponse(response, accessTime);
return response;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -14,23 +14,22 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.cache;
package org.springframework.boot.actuate.endpoint.invoker.cache;
import java.util.function.Function;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
/**
* {@link OperationMethodInvokerAdvisor} to optionally wrap an {@link OperationInvoker}
* with a {@link CachingOperationInvoker}.
* {@link OperationInvokerAdvisor} to optionally provide result caching support.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CachingOperationInvokerAdvisor implements OperationMethodInvokerAdvisor {
public class CachingOperationInvokerAdvisor implements OperationInvokerAdvisor {
private final Function<String, Long> endpointIdTimeToLive;
@ -39,10 +38,9 @@ public class CachingOperationInvokerAdvisor implements OperationMethodInvokerAdv
}
@Override
public OperationInvoker apply(String endpointId, OperationMethodInfo methodInfo,
OperationInvoker invoker) {
if (methodInfo.getOperationType() == OperationType.READ
&& methodInfo.getParameters().isEmpty()) {
public OperationInvoker apply(String endpointId, OperationType operationType,
OperationParameters parameters, OperationInvoker invoker) {
if (operationType == OperationType.READ && !parameters.hasParameters()) {
Long timeToLive = this.endpointIdTimeToLive.apply(endpointId);
if (timeToLive != null && timeToLive > 0) {
return new CachingOperationInvoker(invoker, timeToLive);

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,4 +17,4 @@
/**
* Caching support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.cache;
package org.springframework.boot.actuate.endpoint.invoker.cache;

@ -16,10 +16,9 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.management.Attribute;
import javax.management.AttributeList;
@ -32,76 +31,87 @@ import javax.management.ReflectionException;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.reflect.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A {@link DynamicMBean} that invokes operations on an {@link EndpointInfo endpoint}.
* Adapter to expose a {@link ExposableJmxEndpoint JMX endpoint} as a
* {@link DynamicMBean}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
* @see EndpointMBeanInfoAssembler
*/
public class EndpointMBean implements DynamicMBean {
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
private final Function<Object, Object> operationResponseConverter;
private final JmxOperationResponseMapper responseMapper;
private final EndpointMBeanInfo endpointInfo;
private final ExposableJmxEndpoint endpoint;
EndpointMBean(Function<Object, Object> operationResponseConverter,
EndpointMBeanInfo endpointInfo) {
this.operationResponseConverter = operationResponseConverter;
this.endpointInfo = endpointInfo;
private final MBeanInfo info;
private final Map<String, JmxOperation> operations;
EndpointMBean(JmxOperationResponseMapper responseMapper,
ExposableJmxEndpoint endpoint) {
Assert.notNull(responseMapper, "ResponseMapper must not be null");
Assert.notNull(endpoint, "Endpoint must not be null");
this.responseMapper = responseMapper;
this.endpoint = endpoint;
this.info = new MBeanInfoFactory(responseMapper).getMBeanInfo(endpoint);
this.operations = getOperations(endpoint);
}
/**
* Return the id of the related endpoint.
* @return the endpoint id
*/
public String getEndpointId() {
return this.endpointInfo.getEndpointId();
private Map<String, JmxOperation> getOperations(ExposableJmxEndpoint endpoint) {
Map<String, JmxOperation> operations = new HashMap<>();
endpoint.getOperations()
.forEach((operation) -> operations.put(operation.getName(), operation));
return Collections.unmodifiableMap(operations);
}
@Override
public MBeanInfo getMBeanInfo() {
return this.endpointInfo.getMbeanInfo();
return this.info;
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
JmxOperation operation = this.endpointInfo.getOperations().get(actionName);
if (operation != null) {
Map<String, Object> arguments = getArguments(params,
operation.getParameters());
try {
Object result = operation.getInvoker().invoke(arguments);
if (REACTOR_PRESENT) {
result = ReactiveHandler.handle(result);
}
return this.operationResponseConverter.apply(result);
}
catch (ParametersMissingException | ParameterMappingException ex) {
throw new IllegalArgumentException(ex.getMessage());
}
JmxOperation operation = this.operations.get(actionName);
if (operation == null) {
String message = "Endpoint with id '" + this.endpoint.getId()
+ "' has no operation named " + actionName;
throw new ReflectionException(new IllegalArgumentException(message), message);
}
return invoke(operation, params);
}
private Object invoke(JmxOperation operation, Object[] params) {
try {
String[] parameterNames = operation.getParameters().stream()
.map(JmxOperationParameter::getName).toArray(String[]::new);
Map<String, Object> arguments = getArguments(parameterNames, params);
Object result = operation.invoke(arguments);
if (REACTOR_PRESENT) {
result = ReactiveHandler.handle(result);
}
return this.responseMapper.mapResponse(result);
}
catch (MissingParametersException | ParameterMappingException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
throw new ReflectionException(new IllegalArgumentException(
String.format("Endpoint with id '%s' has no operation named %s",
this.endpointInfo.getEndpointId(), actionName)));
}
private Map<String, Object> getArguments(Object[] params,
List<JmxEndpointOperationParameterInfo> parameters) {
private Map<String, Object> getArguments(String[] parameterNames, Object[] params) {
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < params.length; i++) {
arguments.put(parameters.get(i).getName(), params[i]);
arguments.put(parameterNames[i], params[i]);
}
return arguments;
}
@ -109,13 +119,13 @@ public class EndpointMBean implements DynamicMBean {
@Override
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
throw new AttributeNotFoundException("EndpointMBeans do not support attributes");
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
InvalidAttributeValueException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
throw new AttributeNotFoundException("EndpointMBeans do not support attributes");
}
@Override

@ -1,60 +0,0 @@
/*
* 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.actuate.endpoint.jmx;
import java.util.Map;
import javax.management.MBeanInfo;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
/**
* The {@link MBeanInfo} for a particular {@link EndpointInfo endpoint}. Maps operation
* names to an {@link Operation}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public final class EndpointMBeanInfo {
private final String endpointId;
private final MBeanInfo mBeanInfo;
private final Map<String, JmxOperation> operations;
public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
Map<String, JmxOperation> operations) {
this.endpointId = endpointId;
this.mBeanInfo = mBeanInfo;
this.operations = operations;
}
public String getEndpointId() {
return this.endpointId;
}
public MBeanInfo getMbeanInfo() {
return this.mBeanInfo;
}
public Map<String, JmxOperation> getOperations() {
return this.operations;
}
}

@ -1,125 +0,0 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* Gathers the management operations of a particular {@link EndpointInfo endpoint}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
class EndpointMBeanInfoAssembler {
private final JmxOperationResponseMapper responseMapper;
EndpointMBeanInfoAssembler(JmxOperationResponseMapper responseMapper) {
this.responseMapper = responseMapper;
}
/**
* Creates the {@link EndpointMBeanInfo} for the specified {@link EndpointInfo
* endpoint}.
* @param endpointInfo the endpoint to handle
* @return the mbean info for the endpoint
*/
EndpointMBeanInfo createEndpointMBeanInfo(EndpointInfo<JmxOperation> endpointInfo) {
Map<String, OperationInfos> operationsMapping = getOperationInfo(endpointInfo);
ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
.stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
.toArray(new ModelMBeanOperationInfo[] {});
Map<String, JmxOperation> operationsInfo = new LinkedHashMap<>();
operationsMapping.forEach((name, t) -> operationsInfo.put(name, t.operation));
MBeanInfo info = new ModelMBeanInfoSupport(EndpointMBean.class.getName(),
getDescription(endpointInfo), new ModelMBeanAttributeInfo[0],
new ModelMBeanConstructorInfo[0], operationsMBeanInfo,
new ModelMBeanNotificationInfo[0]);
return new EndpointMBeanInfo(endpointInfo.getId(), info, operationsInfo);
}
private String getDescription(EndpointInfo<?> endpointInfo) {
return "MBean operations for endpoint " + endpointInfo.getId();
}
private Map<String, OperationInfos> getOperationInfo(
EndpointInfo<JmxOperation> endpointInfo) {
Map<String, OperationInfos> operationInfos = new HashMap<>();
endpointInfo.getOperations().forEach((operationInfo) -> {
String name = operationInfo.getOperationName();
ModelMBeanOperationInfo mBeanOperationInfo = new ModelMBeanOperationInfo(
operationInfo.getOperationName(), operationInfo.getDescription(),
getMBeanParameterInfos(operationInfo), this.responseMapper
.mapResponseType(operationInfo.getOutputType()).getName(),
mapOperationType(operationInfo.getType()));
operationInfos.put(name,
new OperationInfos(mBeanOperationInfo, operationInfo));
});
return operationInfos;
}
private MBeanParameterInfo[] getMBeanParameterInfos(JmxOperation operation) {
return operation.getParameters().stream()
.map((operationParameter) -> new MBeanParameterInfo(
operationParameter.getName(),
operationParameter.getType().getName(),
operationParameter.getDescription()))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(parameterInfos) -> parameterInfos
.toArray(new MBeanParameterInfo[parameterInfos.size()])));
}
private int mapOperationType(OperationType type) {
if (type == OperationType.READ) {
return MBeanOperationInfo.INFO;
}
if (type == OperationType.WRITE || type == OperationType.DELETE) {
return MBeanOperationInfo.ACTION;
}
return MBeanOperationInfo.UNKNOWN;
}
private static class OperationInfos {
private final ModelMBeanOperationInfo mBeanOperationInfo;
private final JmxOperation operation;
OperationInfos(ModelMBeanOperationInfo mBeanOperationInfo,
JmxOperation operation) {
this.mBeanOperationInfo = mBeanOperationInfo;
this.operation = operation;
}
}
}

@ -1,115 +0,0 @@
/*
* 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.actuate.endpoint.jmx;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jmx.JmxException;
import org.springframework.jmx.export.MBeanExportException;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.util.Assert;
/**
* JMX Registrar for {@link EndpointMBean}.
*
* @author Stephane Nicoll
* @since 2.0.0
* @see EndpointObjectNameFactory
*/
public class EndpointMBeanRegistrar {
private static final Log logger = LogFactory.getLog(EndpointMBeanRegistrar.class);
private final MBeanServer mBeanServer;
private final EndpointObjectNameFactory objectNameFactory;
/**
* Create a new instance with the {@link MBeanExporter} and
* {@link EndpointObjectNameFactory} to use.
* @param mBeanServer the mbean exporter
* @param objectNameFactory the {@link ObjectName} factory
*/
public EndpointMBeanRegistrar(MBeanServer mBeanServer,
EndpointObjectNameFactory objectNameFactory) {
Assert.notNull(mBeanServer, "MBeanServer must not be null");
Assert.notNull(objectNameFactory, "ObjectNameFactory must not be null");
this.mBeanServer = mBeanServer;
this.objectNameFactory = objectNameFactory;
}
/**
* Register the specified {@link EndpointMBean} and return its {@link ObjectName}.
* @param endpoint the endpoint to register
* @return the {@link ObjectName} used to register the {@code endpoint}
*/
public ObjectName registerEndpointMBean(EndpointMBean endpoint) {
Assert.notNull(endpoint, "Endpoint must not be null");
try {
if (logger.isDebugEnabled()) {
logger.debug("Registering endpoint with id '" + endpoint.getEndpointId()
+ "' to the JMX domain");
}
ObjectName objectName = this.objectNameFactory.generate(endpoint);
this.mBeanServer.registerMBean(endpoint, objectName);
return objectName;
}
catch (MalformedObjectNameException ex) {
throw new IllegalStateException("Invalid ObjectName for endpoint with id '"
+ endpoint.getEndpointId() + "'", ex);
}
catch (Exception ex) {
throw new MBeanExportException(
"Failed to register MBean for endpoint with id '"
+ endpoint.getEndpointId() + "'",
ex);
}
}
/**
* Unregister the specified {@link ObjectName} if necessary.
* @param objectName the {@link ObjectName} of the endpoint to unregister
* @return {@code true} if the endpoint was unregistered, {@code false} if no such
* endpoint was found
*/
public boolean unregisterEndpointMbean(ObjectName objectName) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Unregister endpoint with ObjectName '" + objectName + "' "
+ "from the JMX domain");
}
this.mBeanServer.unregisterMBean(objectName);
return true;
}
catch (InstanceNotFoundException ex) {
return false;
}
catch (MBeanRegistrationException ex) {
throw new JmxException(
"Failed to unregister MBean with ObjectName '" + objectName + "'",
ex);
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -29,11 +29,13 @@ import javax.management.ObjectName;
public interface EndpointObjectNameFactory {
/**
* Generate an {@link ObjectName} for the specified {@link EndpointMBean endpoint}.
* @param mBean the endpoint to handle
* Generate an {@link ObjectName} for the specified {@link ExposableJmxEndpoint
* endpoint}.
* @param endpoint the endpoint MBean to handle
* @return the {@link ObjectName} to use for the endpoint
* @throws MalformedObjectNameException if the object name is invalid
*/
ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException;
ObjectName getObjectName(ExposableJmxEndpoint endpoint)
throws MalformedObjectNameException;
}

@ -0,0 +1,29 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
/**
* Information describing an endpoint that can be exposed over JMX.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface ExposableJmxEndpoint extends ExposableEndpoint<JmxOperation> {
}

@ -0,0 +1,74 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link JmxOperationResponseMapper} that delegates to a Jackson {@link ObjectMapper} to
* return a JSON response.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class JacksonJmxOperationResponseMapper implements JmxOperationResponseMapper {
private final ObjectMapper objectMapper;
private final JavaType listType;
private final JavaType mapType;
public JacksonJmxOperationResponseMapper(ObjectMapper objectMapper) {
this.objectMapper = (objectMapper == null ? new ObjectMapper() : objectMapper);
this.listType = this.objectMapper.getTypeFactory()
.constructParametricType(List.class, Object.class);
this.mapType = this.objectMapper.getTypeFactory()
.constructParametricType(Map.class, String.class, Object.class);
}
@Override
public Class<?> mapResponseType(Class<?> responseType) {
if (CharSequence.class.isAssignableFrom(responseType)) {
return String.class;
}
if (responseType.isArray() || Collection.class.isAssignableFrom(responseType)) {
return List.class;
}
return Map.class;
}
@Override
public Object mapResponse(Object response) {
if (response == null) {
return null;
}
if (response instanceof CharSequence) {
return response.toString();
}
if (response.getClass().isArray() || response instanceof Collection) {
return this.objectMapper.convertValue(response, this.listType);
}
return this.objectMapper.convertValue(response, this.mapType);
}
}

@ -0,0 +1,132 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jmx.JmxException;
import org.springframework.jmx.export.MBeanExportException;
import org.springframework.util.Assert;
/**
* Exports {@link ExposableJmxEndpoint JMX endpoints} to a {@link MBeanServer}.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public class JmxEndpointExporter implements InitializingBean, DisposableBean {
private static final Log logger = LogFactory.getLog(JmxEndpointExporter.class);
private final MBeanServer mBeanServer;
private final EndpointObjectNameFactory objectNameFactory;
private final JmxOperationResponseMapper responseMapper;
private final Collection<ExposableJmxEndpoint> endpoints;
private Collection<ObjectName> registered;
public JmxEndpointExporter(MBeanServer mBeanServer,
EndpointObjectNameFactory objectNameFactory,
JmxOperationResponseMapper responseMapper,
Collection<? extends ExposableJmxEndpoint> endpoints) {
Assert.notNull(mBeanServer, "MBeanServer must not be null");
Assert.notNull(objectNameFactory, "ObjectNameFactory must not be null");
Assert.notNull(responseMapper, "ResponseMapper must not be null");
Assert.notNull(endpoints, "Endpoints must not be null");
this.mBeanServer = mBeanServer;
this.objectNameFactory = objectNameFactory;
this.responseMapper = responseMapper;
this.endpoints = Collections.unmodifiableCollection(endpoints);
}
@Override
public void afterPropertiesSet() {
this.registered = register();
}
@Override
public void destroy() throws Exception {
unregister(this.registered);
}
private Collection<ObjectName> register() {
return this.endpoints.stream().map(this::register).collect(Collectors.toList());
}
private ObjectName register(ExposableJmxEndpoint endpoint) {
Assert.notNull(endpoint, "Endpoint must not be null");
try {
ObjectName name = this.objectNameFactory.getObjectName(endpoint);
EndpointMBean mbean = new EndpointMBean(this.responseMapper, endpoint);
this.mBeanServer.registerMBean(mbean, name);
return name;
}
catch (MalformedObjectNameException ex) {
throw new IllegalStateException(
"Invalid ObjectName for " + getEndpointDescription(endpoint), ex);
}
catch (Exception ex) {
throw new MBeanExportException(
"Failed to register MBean for " + getEndpointDescription(endpoint),
ex);
}
}
private void unregister(Collection<ObjectName> objectNames) {
objectNames.forEach(this::unregister);
}
private void unregister(ObjectName objectName) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Unregister endpoint with ObjectName '" + objectName + "' "
+ "from the JMX domain");
}
this.mBeanServer.unregisterMBean(objectName);
}
catch (InstanceNotFoundException ex) {
// Ignore and continue
}
catch (MBeanRegistrationException ex) {
throw new JmxException(
"Failed to unregister MBean with ObjectName '" + objectName + "'",
ex);
}
}
private String getEndpointDescription(ExposableJmxEndpoint endpoint) {
return "endpoint '" + endpoint.getId() + "'";
}
}

@ -1,63 +0,0 @@
/*
* 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.actuate.endpoint.jmx;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
/**
* A factory for creating JMX MBeans for endpoint operations.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxEndpointMBeanFactory {
private final EndpointMBeanInfoAssembler assembler;
private final JmxOperationResponseMapper resultMapper;
/**
* Create a new {@link JmxEndpointMBeanFactory} instance that will use the given
* {@code responseMapper} to convert an operation's response to a JMX-friendly form.
* @param responseMapper the response mapper
*/
public JmxEndpointMBeanFactory(JmxOperationResponseMapper responseMapper) {
this.assembler = new EndpointMBeanInfoAssembler(responseMapper);
this.resultMapper = responseMapper;
}
/**
* Creates MBeans for the given {@code endpoints}.
* @param endpoints the endpoints
* @return the MBeans
*/
public Collection<EndpointMBean> createMBeans(
Collection<EndpointInfo<JmxOperation>> endpoints) {
return endpoints.stream().map(this::createMBean).collect(Collectors.toList());
}
private EndpointMBean createMBean(EndpointInfo<JmxOperation> endpointInfo) {
EndpointMBeanInfo endpointMBeanInfo = this.assembler
.createEndpointMBeanInfo(endpointInfo);
return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
/**
* {@link EndpointsSupplier} for {@link ExposableJmxEndpoint JMX endpoints}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface JmxEndpointsSupplier extends EndpointsSupplier<ExposableJmxEndpoint> {
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,90 +16,43 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.core.style.ToStringCreator;
/**
* An operation on a JMX endpoint.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class JmxOperation extends Operation {
private final String operationName;
private final Class<?> outputType;
private final String description;
private final List<JmxEndpointOperationParameterInfo> parameters;
/**
* Creates a new {@code JmxEndpointOperation} for an operation of the given
* {@code type}. The operation can be performed using the given {@code invoker}.
* @param type the type of the operation
* @param invoker used to perform the operation
* @param operationName the name of the operation
* @param outputType the type of the output of the operation
* @param description the description of the operation
* @param parameters the parameters of the operation
*/
public JmxOperation(OperationType type, OperationInvoker invoker,
String operationName, Class<?> outputType, String description,
List<JmxEndpointOperationParameterInfo> parameters) {
super(type, invoker, true);
this.operationName = operationName;
this.outputType = outputType;
this.description = description;
this.parameters = parameters;
}
public interface JmxOperation extends Operation {
/**
* Returns the name of the operation.
* @return the operation name
*/
public String getOperationName() {
return this.operationName;
}
String getName();
/**
* Returns the type of the output of the operation.
* @return the output type
*/
public Class<?> getOutputType() {
return this.outputType;
}
Class<?> getOutputType();
/**
* Returns the description of the operation.
* @return the operation description
*/
public String getDescription() {
return this.description;
}
String getDescription();
/**
* Returns the parameters of the operation.
* @return the operation parameters
* Returns the parameters the operation expects in the order that they should be
* provided.
* @return the operation parameter names
*/
public List<JmxEndpointOperationParameterInfo> getParameters() {
return Collections.unmodifiableList(this.parameters);
}
@Override
public String toString() {
return new ToStringCreator(this).append("type", getType())
.append("invoker", getInvoker()).append("blocking", isBlocking())
.append("operationName", this.operationName)
.append("outputType", this.outputType)
.append("description", this.description).toString();
}
List<JmxOperationParameter> getParameters();
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -20,37 +20,27 @@ package org.springframework.boot.actuate.endpoint.jmx;
* Describes the parameters of an operation on a JMX endpoint.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
*/
public class JmxEndpointOperationParameterInfo {
public interface JmxOperationParameter {
private final String name;
private final Class<?> type;
private final String description;
public JmxEndpointOperationParameterInfo(String name, Class<?> type,
String description) {
this.name = name;
this.type = type;
this.description = description;
}
public String getName() {
return this.name;
}
/**
* Return the name of the operation parameter.
* @return the name of the parameter
*/
String getName();
public Class<?> getType() {
return this.type;
}
/**
* Return the type of the operation parameter.
* @return the type
*/
Class<?> getType();
/**
* Return the description of the parameter or {@code null} if none is available.
* @return the description or {@code null}
*/
public String getDescription() {
return this.description;
}
String getDescription();
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,21 +17,13 @@
package org.springframework.boot.actuate.endpoint.jmx;
/**
* A {@code JmxOperationResponseMapper} maps an operation's response to a JMX-friendly
* form.
* Maps an operation's response to a JMX-friendly form.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public interface JmxOperationResponseMapper {
/**
* Map the operation's response so that it can be consumed by a JMX compliant client.
* @param response the operation's response
* @return the {@code response}, in a JMX compliant format
*/
Object mapResponse(Object response);
/**
* Map the response type to its JMX compliant counterpart.
* @param responseType the operation's response type
@ -39,4 +31,11 @@ public interface JmxOperationResponseMapper {
*/
Class<?> mapResponseType(Class<?> responseType);
/**
* Map the operation's response so that it can be consumed by a JMX compliant client.
* @param response the operation's response
* @return the {@code response}, in a JMX compliant format
*/
Object mapResponse(Object response);
}

@ -0,0 +1,103 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx;
import java.util.List;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* Factory to create {@link MBeanInfo} from a {@link ExposableJmxEndpoint}.
*
* @author Stephane Nicoll
* @author Phillip Webb
*/
class MBeanInfoFactory {
private static final ModelMBeanAttributeInfo[] NO_ATTRIBUTES = new ModelMBeanAttributeInfo[0];
private static final ModelMBeanConstructorInfo[] NO_CONSTRUCTORS = new ModelMBeanConstructorInfo[0];
private static final ModelMBeanNotificationInfo[] NO_NOTIFICATIONS = new ModelMBeanNotificationInfo[0];
private final JmxOperationResponseMapper responseMapper;
MBeanInfoFactory(JmxOperationResponseMapper responseMapper) {
this.responseMapper = responseMapper;
}
public MBeanInfo getMBeanInfo(ExposableJmxEndpoint endpoint) {
String className = EndpointMBean.class.getName();
String description = getDescription(endpoint);
ModelMBeanOperationInfo[] operations = getMBeanOperations(endpoint);
return new ModelMBeanInfoSupport(className, description, NO_ATTRIBUTES,
NO_CONSTRUCTORS, operations, NO_NOTIFICATIONS);
}
private String getDescription(ExposableJmxEndpoint endpoint) {
return "MBean operations for endpoint " + endpoint.getId();
}
private ModelMBeanOperationInfo[] getMBeanOperations(ExposableJmxEndpoint endpoint) {
return endpoint.getOperations().stream().map(this::getMBeanOperation)
.toArray(ModelMBeanOperationInfo[]::new);
}
private ModelMBeanOperationInfo getMBeanOperation(JmxOperation operation) {
String name = operation.getName();
String description = operation.getDescription();
MBeanParameterInfo[] signature = getSignature(operation.getParameters());
String type = getType(operation.getOutputType());
int impact = getImact(operation.getType());
return new ModelMBeanOperationInfo(name, description, signature, type, impact);
}
private MBeanParameterInfo[] getSignature(List<JmxOperationParameter> parameters) {
return parameters.stream().map(this::getMBeanParameter)
.toArray(MBeanParameterInfo[]::new);
}
private MBeanParameterInfo getMBeanParameter(JmxOperationParameter parameter) {
return new MBeanParameterInfo(parameter.getName(), parameter.getType().getName(),
parameter.getDescription());
}
private int getImact(OperationType operationType) {
if (operationType == OperationType.READ) {
return MBeanOperationInfo.INFO;
}
if (operationType == OperationType.WRITE
|| operationType == OperationType.DELETE) {
return MBeanOperationInfo.ACTION;
}
return MBeanOperationInfo.UNKNOWN;
}
private String getType(Class<?> outputType) {
return this.responseMapper.mapResponseType(outputType).getName();
}
}

@ -0,0 +1,39 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx.annotation;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
/**
* A discovered {@link ExposableJmxEndpoint JMX endpoint}.
*
* @author Phillip Webb
*/
class DiscoveredJmxEndpoint extends AbstractDiscoveredEndpoint<JmxOperation>
implements ExposableJmxEndpoint {
DiscoveredJmxEndpoint(EndpointDiscoverer<?, ?> discoverer, String id,
boolean enabledByDefault, Collection<JmxOperation> operations) {
super(discoverer, id, enabledByDefault, operations);
}
}

@ -0,0 +1,213 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx.annotation;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredOperation;
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationParameter;
import org.springframework.core.style.ToStringCreator;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.metadata.JmxAttributeSource;
import org.springframework.jmx.export.metadata.ManagedOperation;
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
import org.springframework.util.StringUtils;
/**
* A discovered {@link JmxOperation JMX operation}.
*
* @author Stephane Nicoll
* @author Philip Webb
*/
class DiscoveredJmxOperation extends AbstractDiscoveredOperation implements JmxOperation {
private static final JmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
private final String name;
private final Class<?> outputType;
private final String description;
private final List<JmxOperationParameter> parameters;
DiscoveredJmxOperation(String endpointId, DiscoveredOperationMethod operationMethod,
OperationInvoker invoker) {
super(operationMethod, invoker);
Method method = operationMethod.getMethod();
this.name = method.getName();
this.outputType = JmxType.get(method.getReturnType());
this.description = getDescription(method,
() -> "Invoke " + this.name + " for endpoint " + endpointId);
this.parameters = getParameters(operationMethod);
}
private String getDescription(Method method, Supplier<String> fallback) {
ManagedOperation managed = jmxAttributeSource.getManagedOperation(method);
if (managed != null && StringUtils.hasText(managed.getDescription())) {
return managed.getDescription();
}
return fallback.get();
}
private List<JmxOperationParameter> getParameters(OperationMethod operationMethod) {
if (!operationMethod.getParameters().hasParameters()) {
return Collections.emptyList();
}
Method method = operationMethod.getMethod();
ManagedOperationParameter[] managed = jmxAttributeSource
.getManagedOperationParameters(method);
if (managed.length == 0) {
return asList(operationMethod.getParameters().stream()
.map(DiscoveredJmxOperationParameter::new));
}
return mergeParameters(operationMethod.getParameters(), managed);
}
private List<JmxOperationParameter> mergeParameters(
OperationParameters operationParameters,
ManagedOperationParameter[] managedParameters) {
List<JmxOperationParameter> merged = new ArrayList<>(managedParameters.length);
for (int i = 0; i < managedParameters.length; i++) {
merged.add(new DiscoveredJmxOperationParameter(managedParameters[i],
operationParameters.get(i)));
}
return Collections.unmodifiableList(merged);
}
private <T> List<T> asList(Stream<T> stream) {
return stream.collect(Collectors.collectingAndThen(Collectors.toList(),
Collections::unmodifiableList));
}
@Override
public String getName() {
return this.name;
}
@Override
public Class<?> getOutputType() {
return this.outputType;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public List<JmxOperationParameter> getParameters() {
return this.parameters;
}
@Override
protected void appendFields(ToStringCreator creator) {
creator.append("name", this.name).append("outputType", this.outputType)
.append("description", this.description)
.append("parameters", this.parameters);
}
/**
* A discovered {@link JmxOperationParameter}.
*/
private static class DiscoveredJmxOperationParameter
implements JmxOperationParameter {
private final String name;
private final Class<?> type;
private final String description;
DiscoveredJmxOperationParameter(OperationParameter operationParameter) {
this.name = operationParameter.getName();
this.type = JmxType.get(operationParameter.getType());
this.description = null;
}
DiscoveredJmxOperationParameter(ManagedOperationParameter managedParameter,
OperationParameter operationParameter) {
this.name = managedParameter.getName();
this.type = JmxType.get(operationParameter.getType());
this.description = managedParameter.getDescription();
}
@Override
public String getName() {
return this.name;
}
@Override
public Class<?> getType() {
return this.type;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder(this.name);
if (this.description != null) {
result.append(" (" + this.description + ")");
}
result.append(":" + this.type);
return result.toString();
}
}
/**
* Utility to convert to JMX supported types.
*/
private static class JmxType {
public static Class<?> get(Class<?> source) {
if (source.isEnum()) {
return String.class;
}
if (Date.class.isAssignableFrom(source)
|| Instant.class.isAssignableFrom(source)) {
return String.class;
}
if (source.getName().startsWith("java.")) {
return source;
}
if (source.equals(Void.TYPE)) {
return source;
}
return Object.class;
}
}
}

@ -1,83 +0,0 @@
/*
* 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.actuate.endpoint.jmx.annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
/**
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
* {@link EndpointJmxExtension JMX extensions} applied to them.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxAnnotationEndpointDiscoverer
extends AnnotationEndpointDiscoverer<String, JmxOperation> {
static final AnnotationJmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
/**
* Creates a new {@link JmxAnnotationEndpointDiscoverer} that will discover
* {@link Endpoint endpoints} and {@link EndpointJmxExtension jmx extensions} using
* the given {@link ApplicationContext}.
* @param applicationContext the application context
* @param parameterMapper the {@link ParameterMapper} used to convert arguments when
* an operation is invoked
* @param invokerAdvisors advisors used to add additional invoker advise
* @param filters filters that must match for an endpoint to be exposed
*/
public JmxAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
ParameterMapper parameterMapper,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<JmxOperation>> filters) {
super(applicationContext, new JmxEndpointOperationFactory(),
JmxOperation::getOperationName, parameterMapper, invokerAdvisors,
filters);
}
@Override
protected void verify(Collection<DiscoveredEndpoint> exposedEndpoints) {
List<List<JmxOperation>> clashes = new ArrayList<>();
exposedEndpoints.forEach((exposedEndpoint) -> clashes
.addAll(exposedEndpoint.findDuplicateOperations().values()));
if (!clashes.isEmpty()) {
StringBuilder message = new StringBuilder();
message.append(
String.format("Found multiple JMX operations with the same name:%n"));
clashes.forEach((clash) -> {
message.append(" ").append(clash.get(0).getOperationName())
.append(String.format(":%n"));
clash.forEach((operation) -> message.append(" ")
.append(String.format("%s%n", operation)));
});
throw new IllegalStateException(message.toString());
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -22,7 +22,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
import org.springframework.core.annotation.AliasFor;
@ -33,7 +32,6 @@ import org.springframework.core.annotation.AliasFor;
* @author Stephane Nicoll
* @author Phillip Webb
* @since 2.0.0
* @see AnnotationEndpointDiscoverer
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

@ -0,0 +1,74 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx.annotation;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.context.ApplicationContext;
/**
* {@link EndpointDiscoverer} for {@link ExposableJmxEndpoint JMX endpoints}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public class JmxEndpointDiscoverer
extends EndpointDiscoverer<ExposableJmxEndpoint, JmxOperation>
implements JmxEndpointsSupplier {
/**
* Create a new {@link JmxEndpointDiscoverer} instance.
* @param applicationContext the source application context
* @param parameterValueMapper the parameter value mapper
* @param invokerAdvisors invoker advisors to apply
* @param filters filters to apply
*/
public JmxEndpointDiscoverer(ApplicationContext applicationContext,
ParameterValueMapper parameterValueMapper,
Collection<OperationInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<ExposableJmxEndpoint>> filters) {
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
}
@Override
protected ExposableJmxEndpoint createEndpoint(String id, boolean enabledByDefault,
Collection<JmxOperation> operations) {
return new DiscoveredJmxEndpoint(this, id, enabledByDefault, operations);
}
@Override
protected JmxOperation createOperation(String endpointId,
DiscoveredOperationMethod operationMethod, OperationInvoker invoker) {
return new DiscoveredJmxOperation(endpointId, operationMethod, invoker);
}
@Override
protected OperationKey createOperationKey(JmxOperation operation) {
return new OperationKey(operation.getName(),
() -> "MBean call '" + operation.getName() + "'");
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,23 +16,18 @@
package org.springframework.boot.actuate.endpoint.jmx.annotation;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.annotation.DiscovererEndpointFilter;
/**
* {@link EndpointFilter} for endpoints discovered by
* {@link JmxAnnotationEndpointDiscoverer}.
* {@link EndpointFilter} for endpoints discovered by {@link JmxEndpointDiscoverer}.
*
* @author Phillip Webb
*/
class JmxEndpointFilter implements EndpointFilter<JmxOperation> {
class JmxEndpointFilter extends DiscovererEndpointFilter {
@Override
public boolean match(EndpointInfo<JmxOperation> info,
EndpointDiscoverer<JmxOperation> discoverer) {
return (discoverer instanceof JmxAnnotationEndpointDiscoverer);
JmxEndpointFilter() {
super(JmxEndpointDiscoverer.class);
}
}

@ -1,128 +0,0 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.jmx.annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.annotation.OperationFactory;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointOperationParameterInfo;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo;
import org.springframework.jmx.export.metadata.ManagedOperation;
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
import org.springframework.util.StringUtils;
/**
* {@link OperationFactory} for {@link JmxOperation JMX operations}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
*/
class JmxEndpointOperationFactory implements OperationFactory<JmxOperation> {
@Override
public JmxOperation createOperation(String endpointId, OperationMethodInfo methodInfo,
Object target, OperationInvoker invoker) {
Method method = methodInfo.getMethod();
String name = method.getName();
OperationType operationType = methodInfo.getOperationType();
Class<?> outputType = getJmxType(method.getReturnType());
String description = getDescription(method,
() -> "Invoke " + name + " for endpoint " + endpointId);
return new JmxOperation(operationType, invoker, name, outputType, description,
getParameters(methodInfo));
}
private String getDescription(Method method, Supplier<String> fallback) {
ManagedOperation managedOperation = JmxAnnotationEndpointDiscoverer.jmxAttributeSource
.getManagedOperation(method);
if (managedOperation != null
&& StringUtils.hasText(managedOperation.getDescription())) {
return managedOperation.getDescription();
}
return fallback.get();
}
private List<JmxEndpointOperationParameterInfo> getParameters(
OperationMethodInfo methodInfo) {
if (methodInfo.getParameters().isEmpty()) {
return Collections.emptyList();
}
Method method = methodInfo.getMethod();
ManagedOperationParameter[] operationParameters = JmxAnnotationEndpointDiscoverer.jmxAttributeSource
.getManagedOperationParameters(method);
if (operationParameters.length == 0) {
return methodInfo.getParameters().entrySet().stream().map(this::getParameter)
.collect(Collectors.toCollection(ArrayList::new));
}
return mergeParameters(method.getParameters(), operationParameters);
}
private List<JmxEndpointOperationParameterInfo> mergeParameters(
Parameter[] methodParameters,
ManagedOperationParameter[] operationParameters) {
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
for (int i = 0; i < operationParameters.length; i++) {
ManagedOperationParameter operationParameter = operationParameters[i];
Parameter methodParameter = methodParameters[i];
JmxEndpointOperationParameterInfo parameter = getParameter(
operationParameter.getName(), methodParameter,
operationParameter.getDescription());
parameters.add(parameter);
}
return parameters;
}
private JmxEndpointOperationParameterInfo getParameter(
Map.Entry<String, Parameter> entry) {
return getParameter(entry.getKey(), entry.getValue(), null);
}
private JmxEndpointOperationParameterInfo getParameter(String name,
Parameter methodParameter, String description) {
return new JmxEndpointOperationParameterInfo(name,
getJmxType(methodParameter.getType()), description);
}
private Class<?> getJmxType(Class<?> type) {
if (type.isEnum()) {
return String.class;
}
if (Instant.class.isAssignableFrom(type)) {
return String.class;
}
if (type.getName().startsWith("java.")) {
return type;
}
if (type.equals(Void.TYPE)) {
return type;
}
return Object.class;
}
}

@ -1,113 +0,0 @@
/*
* 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.actuate.endpoint.reflect;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* An {@code OperationInvoker} that invokes an operation using reflection.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
public class ReflectiveOperationInvoker implements OperationInvoker {
private final Object target;
private final OperationMethodInfo methodInfo;
private final ParameterMapper parameterMapper;
/**
* 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 and the given
* {@code parameterNameMapper} will be used map parameters by name.
* @param target the target of the reflective call
* @param methodInfo the method info
* @param parameterMapper the parameter mapper
*/
public ReflectiveOperationInvoker(Object target, OperationMethodInfo methodInfo,
ParameterMapper parameterMapper) {
Assert.notNull(target, "Target must not be null");
Assert.notNull(methodInfo, "MethodInfo must not be null");
Assert.notNull(parameterMapper, "ParameterMapper must not be null");
ReflectionUtils.makeAccessible(methodInfo.getMethod());
this.target = target;
this.methodInfo = methodInfo;
this.parameterMapper = parameterMapper;
}
@Override
public Object invoke(Map<String, Object> arguments) {
Map<String, Parameter> parameters = this.methodInfo.getParameters();
validateRequiredParameters(parameters, arguments);
return ReflectionUtils.invokeMethod(this.methodInfo.getMethod(), this.target,
resolveArguments(parameters, arguments));
}
private void validateRequiredParameters(Map<String, Parameter> parameters,
Map<String, Object> arguments) {
Set<String> missingParameters = parameters.keySet().stream()
.filter((n) -> isMissing(n, parameters.get(n), arguments))
.collect(Collectors.toSet());
if (!missingParameters.isEmpty()) {
throw new ParametersMissingException(missingParameters);
}
}
private boolean isMissing(String name, Parameter parameter,
Map<String, Object> arguments) {
Object resolved = arguments.get(name);
return (resolved == null && !isExplicitNullable(parameter));
}
private boolean isExplicitNullable(Parameter parameter) {
return (parameter.getAnnotationsByType(Nullable.class).length != 0);
}
private Object[] resolveArguments(Map<String, Parameter> parameters,
Map<String, Object> arguments) {
return parameters.keySet().stream()
.map((name) -> resolveArgument(name, parameters.get(name), arguments))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(list) -> list.toArray(new Object[list.size()])));
}
private Object resolveArgument(String name, Parameter parameter,
Map<String, Object> arguments) {
Object resolved = arguments.get(name);
return this.parameterMapper.mapParameter(resolved, parameter.getType());
}
@Override
public String toString() {
return new ToStringCreator(this).append("target", this.target)
.append("method", this.methodInfo.getMethod().toString()).toString();
}
}

@ -20,8 +20,6 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
/**
* A resolver for {@link Link links} to web endpoints.
*
@ -33,18 +31,18 @@ public class EndpointLinksResolver {
/**
* Resolves links to the operations of the given {code webEndpoints} based on a
* request with the given {@code requestUrl}.
* @param webEndpoints the web endpoints
* @param endpoints the source endpoints
* @param requestUrl the url of the request for the endpoint links
* @return the links
*/
public Map<String, Link> resolveLinks(
Collection<EndpointInfo<WebOperation>> webEndpoints, String requestUrl) {
public Map<String, Link> resolveLinks(Collection<ExposableWebEndpoint> endpoints,
String requestUrl) {
String normalizedUrl = normalizeRequestUrl(requestUrl);
Map<String, Link> links = new LinkedHashMap<>();
links.put("self", new Link(normalizedUrl));
for (EndpointInfo<WebOperation> endpoint : webEndpoints) {
for (ExposableWebEndpoint endpoint : endpoints) {
for (WebOperation operation : endpoint.getOperations()) {
webEndpoints.stream().map(EndpointInfo::getId).forEach((id) -> links
endpoints.stream().map(ExposableWebEndpoint::getId).forEach((id) -> links
.put(operation.getId(), createLink(normalizedUrl, operation)));
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -49,7 +49,6 @@ public class EndpointMediaTypes {
/**
* Returns the media types produced by an endpoint.
*
* @return the produced media types
*/
public List<String> getProduced() {
@ -58,7 +57,6 @@ public class EndpointMediaTypes {
/**
* Returns the media types consumed by an endpoint.
*
* @return the consumed media types
*/
public List<String> getConsumed() {

@ -0,0 +1,29 @@
/*
* Copyright 2012-2018 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.actuate.endpoint.web;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
/**
* Information describing an endpoint that can be exposed over the web.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface ExposableWebEndpoint extends ExposableEndpoint<WebOperation> {
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -17,6 +17,7 @@
package org.springframework.boot.actuate.endpoint.web;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* Details for a link in a
@ -37,6 +38,7 @@ public class Link {
* @param href the href
*/
public Link(String href) {
Assert.notNull(href, "HREF must not be null");
this.href = href;
this.templated = href.contains("{");
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save