diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java index 3607ce26ef..38f7cc5c6f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java @@ -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 endpointPaths; + private final List 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilter.java index 812515f57f..dbcccece75 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilter.java @@ -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 { +class CloudFoundryEndpointFilter extends DiscovererEndpointFilter { - @Override - public boolean match(EndpointInfo info, EndpointDiscoverer discoverer) { - return (discoverer instanceof CloudFoundryWebAnnotationEndpointDiscoverer); + protected CloudFoundryEndpointFilter() { + super(CloudFoundryWebEndpointDiscoverer.class); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscoverer.java deleted file mode 100644 index 4d508c8f47..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscoverer.java +++ /dev/null @@ -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 invokerAdvisors, - Collection> filters, - Class requiredExtensionType) { - super(applicationContext, parameterMapper, endpointMediaTypes, - endpointPathResolver, invokerAdvisors, filters); - this.requiredExtensionType = requiredExtensionType; - } - - @Override - protected boolean isExtensionExposed(Class endpointType, Class extensionType, - EndpointInfo endpointInfo) { - if (HealthEndpoint.class.equals(endpointType) - && !this.requiredExtensionType.equals(extensionType)) { - return false; - } - return super.isExtensionExposed(endpointType, extensionType, endpointInfo); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java new file mode 100644 index 0000000000..79229d8e5c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java @@ -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 invokerAdvisors, + Collection> 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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/HealthEndpointCloudFoundryExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/HealthEndpointCloudFoundryExtension.java new file mode 100644 index 0000000000..d7485662a5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/HealthEndpointCloudFoundryExtension.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java index 4aaa4e55d7..4ad914121b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundrySecurityInterceptor.java similarity index 94% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundrySecurityInterceptor.java index 4565e5c976..ea379d6dbd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundrySecurityInterceptor.java @@ -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 SUCCESS = Mono.just(SecurityResponse.success()); - ReactiveCloudFoundrySecurityInterceptor(ReactiveTokenValidator tokenValidator, + CloudFoundrySecurityInterceptor(ReactiveTokenValidator tokenValidator, ReactiveCloudFoundrySecurityService cloudFoundrySecurityService, String applicationId) { this.tokenValidator = tokenValidator; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index d7bcf4a941..2868252f8c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java @@ -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 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> links(ServerWebExchange exchange) { + protected Publisher> 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 links = this.endpointLinksResolver + Map 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> 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> doHandle(ServerWebExchange exchange, + @Override + public Mono> handle(ServerWebExchange exchange, Map body) { return this.securityInterceptor.preHandle(exchange, this.endpointId) .flatMap((securityResponse) -> flatMapResponse(exchange, body, securityResponse)); } - private Mono> flatMapResponse( - ServerWebExchange exchange, Map body, - SecurityResponse securityResponse) { + private Mono> flatMapResponse(ServerWebExchange exchange, + Map body, SecurityResponse securityResponse) { if (!securityResponse.getStatus().equals(HttpStatus.OK)) { return Mono.just(new ResponseEntity<>(securityResponse.getStatus())); } - Map 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> 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 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> handle(ServerWebExchange exchange, - @RequestBody(required = false) Map 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> handle(ServerWebExchange exchange) { - return doHandle(exchange, null); + return this.delegate.handle(exchange, body); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java index 5b754994ad..5edfe28a33 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java @@ -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")); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index 53aff3a958..1b204f1a99 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -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( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java index 87e211ae37..02b5434b79 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java index f1f4d264b4..9e3a9f6f26 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java @@ -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"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index b50e1759f4..2cf7f1738a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java @@ -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> webEndpoints, + Collection 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> links(HttpServletRequest request, + protected Map> 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 links = this.endpointLinksResolver.resolveLinks(getEndpoints(), + Map links = this.linksResolver.resolveLinks(getEndpoints(), request.getRequestURL().toString()); Map 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 body) { + @Override + public Object handle(HttpServletRequest request, Map body) { SecurityResponse securityResponse = this.securityInterceptor .preHandle(request, this.endpointId); if (!securityResponse.getStatus().equals(HttpStatus.OK)) { - return failureResponse(securityResponse); + return new ResponseEntity(securityResponse.getMessage(), + securityResponse.getStatus()); } - Map arguments = new HashMap<>((Map) 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(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(response.getBody(), - HttpStatus.valueOf(response.getStatus())); + return this.delegate.handle(request, body); } - } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index 41e554d40a..30b7b2591c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointIdTimeToLivePropertyFunction.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointIdTimeToLivePropertyFunction.java index a1437dac0c..494bea5b50 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointIdTimeToLivePropertyFunction.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointIdTimeToLivePropertyFunction.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java index a5139e0cec..ddb5a5cd42 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java @@ -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 The operation type + * @param The endpoint type * @author Phillip Webb * @since 2.0.0 */ -public class ExposeExcludePropertyEndpointFilter - implements EndpointFilter { +public class ExposeExcludePropertyEndpointFilter> + implements EndpointFilter { - private final Class> discovererType; + private final Class endpointType; private final Set expose; @@ -52,25 +50,23 @@ public class ExposeExcludePropertyEndpointFilter private final Set exposeDefaults; - public ExposeExcludePropertyEndpointFilter( - Class> discovererType, + public ExposeExcludePropertyEndpointFilter(Class 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> discovererType, + public ExposeExcludePropertyEndpointFilter(Class endpointType, Collection expose, Collection 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 } @Override - public boolean match(EndpointInfo info, EndpointDiscoverer 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 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 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 items, EndpointInfo info) { - return items.contains(info.getId().toLowerCase()); + private boolean contains(Set items, ExposableEndpoint endpoint) { + return items.contains(endpoint.getId().toLowerCase()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactory.java index daa794a2d9..c513b4933e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactory.java @@ -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 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(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index 0bbea34ef1..660bc88efd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -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 invokerAdvisors, - Collection> filters) { - return new JmxAnnotationEndpointDiscoverer(this.applicationContext, - parameterMapper, invokerAdvisors, filters); + @ConditionalOnMissingBean(JmxEndpointsSupplier.class) + public JmxEndpointDiscoverer jmxAnnotationEndpointDiscoverer( + ParameterValueMapper parameterValueMapper, + ObjectProvider> invokerAdvisors, + ObjectProvider>> 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) { + public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, + ObjectProvider 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 jmxIncludeExcludePropertyEndpointFilter() { - return new ExposeExcludePropertyEndpointFilter<>( - JmxAnnotationEndpointDiscoverer.class, this.properties.getExpose(), - this.properties.getExclude(), "*"); + public ExposeExcludePropertyEndpointFilter jmxIncludeExcludePropertyEndpointFilter() { + Set expose = this.properties.getExpose(); + Set exclude = this.properties.getExclude(); + return new ExposeExcludePropertyEndpointFilter<>(ExposableJmxEndpoint.class, + expose, exclude, "*"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointExporter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointExporter.java deleted file mode 100644 index 4f479451aa..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointExporter.java +++ /dev/null @@ -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 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 registerEndpointMBeans() { - Collection> 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 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; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java index 1965297f2c..7e6dad0cd6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProvider.java @@ -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 endpointDiscoverer; + private final Collection endpoints; - public DefaultEndpointPathProvider( - EndpointDiscoverer endpointDiscoverer, - WebEndpointProperties webEndpointProperties) { - this.endpointDiscoverer = endpointDiscoverer; + public DefaultEndpointPathProvider(WebEndpointProperties webEndpointProperties, + Collection endpoints) { this.basePath = webEndpointProperties.getBasePath(); + this.endpoints = Collections.unmodifiableCollection(endpoints); } @Override public List 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> getEndpoints() { - return this.endpointDiscoverer.discoverEndpoints().stream(); - } - - private String getPath(EndpointInfo endpointInfo) { - return this.basePath + "/" + endpointInfo.getId(); + private String getPath(ExposableWebEndpoint endpoint) { + return this.basePath + "/" + endpoint.getId(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java index 206a428a61..f89d5067e4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java @@ -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 invokerAdvisors, - Collection> 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> invokerAdvisors, + ObjectProvider>> filters) { + return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper, + endpointMediaTypes, endpointPathResolver, + invokerAdvisors.getIfAvailable(Collections::emptyList), + filters.getIfAvailable(Collections::emptyList)); } @Bean @ConditionalOnMissingBean public EndpointPathProvider endpointPathProvider( - EndpointDiscoverer endpointDiscoverer, + WebEndpointsSupplier webEndpointsSupplier, WebEndpointProperties webEndpointProperties) { - return new DefaultEndpointPathProvider(endpointDiscoverer, webEndpointProperties); + return new DefaultEndpointPathProvider(webEndpointProperties, + webEndpointsSupplier.getEndpoints()); } @Bean - public ExposeExcludePropertyEndpointFilter webIncludeExcludePropertyEndpointFilter() { - return new ExposeExcludePropertyEndpointFilter<>( - WebAnnotationEndpointDiscoverer.class, this.properties.getExpose(), - this.properties.getExclude(), "info", "health"); + public ExposeExcludePropertyEndpointFilter webIncludeExcludePropertyEndpointFilter() { + Set expose = this.properties.getExpose(); + Set exclude = this.properties.getExclude(); + return new ExposeExcludePropertyEndpointFilter<>(ExposableWebEndpoint.class, + expose, exclude, "info", "health"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index c38b0d69fa..9691f2b7e6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -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 endpoints = Collections + .unmodifiableCollection(webEndpointsSupplier.getEndpoints()); + resourceConfig.registerResources( + new HashSet<>(resourceFactory.createEndpointResources(endpointMapping, + endpoints, endpointMediaTypes))); + }; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 2d463119e4..917d7eb9a6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -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()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 4e1f35f4f2..f356e26ffe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilterTests.java index 35a9e32f5f..5159550307 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointFilterTests.java @@ -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 invokerAdvisors, - Collection> 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(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java similarity index 61% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscovererTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java index b89c082b6a..3c35275245 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebAnnotationEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java @@ -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> endpoints = endpointDiscoverer - .discoverEndpoints(); + public void getEndpointsShouldAddCloudFoundryHealthExtension() { + load(TestConfiguration.class, (discoverer) -> { + Collection 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 consumer) { + Consumer consumer) { this.load((id) -> null, (id) -> id, configuration, consumer); } private void load(Function timeToLive, EndpointPathResolver endpointPathResolver, Class configuration, - Consumer consumer) { + Consumer 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"; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index 6722ecb25f..8c117bfbdb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -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 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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index 95208b820b..1a8e1b48f7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java @@ -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> endpoints = (List>) handlerMapping - .getEndpoints(); - List endpointIds = endpoints.stream().map(EndpointInfo::getId) + Collection endpoints = handlerMapping.getEndpoints(); + List 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> endpoints = (List>) handlerMapping - .getEndpoints(); - EndpointInfo endpoint = endpoints.stream() + Collection 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> endpoints = getHandlerMapping() - .getEndpoints(); - EndpointInfo endpointInfo = endpoints.iterator().next(); - WebOperation webOperation = endpointInfo.getOperations().iterator().next(); - ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation - .getInvoker(); + Collection 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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java index 851ec6b5f3..6c89c56e7c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java @@ -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()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index bbaa645918..e7288da5e6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -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> endpoints = (List>) handlerMapping - .getEndpoints(); + Collection 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> endpoints = (List>) handlerMapping - .getEndpoints(); - EndpointInfo endpoint = endpoints.stream() + Collection endpoints = handlerMapping.getEndpoints(); + ExposableWebEndpoint endpoint = endpoints.stream() .filter((candidate) -> "test".equals(candidate.getId())).findFirst() .get(); Collection 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> endpoints = this.context + Collection endpoints = this.context .getBean("cloudFoundryWebEndpointServletHandlerMapping", CloudFoundryWebEndpointServletHandlerMapping.class) .getEndpoints(); - EndpointInfo 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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 174d44b193..d407d59375 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -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 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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java index 2e6bc26ad5..624eb59b8a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java @@ -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 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 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 { + private abstract static class TestExposableWebEndpoint + implements ExposableWebEndpoint { } - private abstract static class DifferentTestEndpointDiscoverer - implements EndpointDiscoverer { + private abstract static class DifferentTestExposableWebEndpoint + implements ExposableWebEndpoint { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java index 9efeb38f9f..def3920af7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java @@ -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 { + static class TestFilter implements EndpointFilter> { @Override - public boolean match(EndpointInfo info, - EndpointDiscoverer discoverer) { + public boolean match(ExposableEndpoint endpoint) { return true; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactoryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactoryTests.java index e401c1db47..08ebe3e580 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactoryTests.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java index 4358b10ecc..048cf6c805 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/DefaultEndpointPathProviderTests.java @@ -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 discoverer; - @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -78,13 +72,18 @@ public class DefaultEndpointPathProviderTests { } private DefaultEndpointPathProvider createProvider(String contextPath) { - Collection> 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 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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java index e0b839828f..858dc44bf7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java @@ -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. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractExposableEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractExposableEndpoint.java new file mode 100644 index 0000000000..b8e6e41e8b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractExposableEndpoint.java @@ -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 The operation type. + * @author Phillip Webb + * @since 2.0.0 + */ +public abstract class AbstractExposableEndpoint + implements ExposableEndpoint { + + private final String id; + + private boolean enabledByDefault; + + private List 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 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 getOperations() { + return this.operations; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java index a7fb1d8f19..a08d05c110 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java @@ -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 the type of the endpoint's operations + * @param the endpoint type * @since 2.0.0 */ @FunctionalInterface -public interface EndpointFilter { +public interface EndpointFilter> { /** * 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 info, EndpointDiscoverer discoverer); + boolean match(E endpoint); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointInfo.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointInfo.java deleted file mode 100644 index 90336068bf..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointInfo.java +++ /dev/null @@ -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 the type of the endpoint's operations - * @author Andy Wilkinson - * @since 2.0.0 - */ -public class EndpointInfo { - - private final String id; - - private final boolean enableByDefault; - - private final Collection 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 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 getOperations() { - return this.operations; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointsSupplier.java similarity index 66% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointDiscoverer.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointsSupplier.java index 218655133f..fbe3e11e99 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointsSupplier.java @@ -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 the type of the operation + * @param the endpoint type * @author Andy Wilkinson * @author Stephane Nicoll + * @author Phillip Webb * @since 2.0.0 */ -@FunctionalInterface -public interface EndpointDiscoverer { +public interface EndpointsSupplier> { /** - * Perform endpoint discovery. - * @return the discovered endpoints + * Return the provided endpoints. + * @return the endpoints */ - Collection> discoverEndpoints(); + Collection getEndpoints(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ExposableEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ExposableEndpoint.java new file mode 100644 index 0000000000..64dce2f9d1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ExposableEndpoint.java @@ -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 the type of the endpoint's operations + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.0.0 + */ +public interface ExposableEndpoint { + + /** + * 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 getOperations(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/NamePatternFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/NamePatternFilter.java deleted file mode 100644 index 3c118ed111..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/NamePatternFilter.java +++ /dev/null @@ -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 the source data type - * @author Phillip Webb - * @author Sergei Egorov - * @author Andy Wilkinson - * @author Dylian Bego - */ -abstract class NamePatternFilter { - - private static final String[] REGEX_PARTS = { "*", "$", "^", "+", "[" }; - - private final T source; - - NamePatternFilter(T source) { - this.source = source; - } - - public Map getResults(String name) { - Pattern pattern = compilePatternIfNecessary(name); - if (pattern == null) { - Object value = getValue(this.source, name); - Map 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 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 getResults() { - return this.results; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java index aac6a05b41..f3462f9c74 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java @@ -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 arguments); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationType.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationType.java index 3ccb945689..3a04d023b3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationType.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationType.java @@ -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 { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredEndpoint.java new file mode 100644 index 0000000000..b1e3036ec0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredEndpoint.java @@ -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 The operation type + * @author Phillip Webb + * @since 2.0.0 + */ +public abstract class AbstractDiscoveredEndpoint + extends AbstractExposableEndpoint implements DiscoveredEndpoint { + + 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 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> 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) { + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredOperation.java new file mode 100644 index 0000000000..82b6843911 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AbstractDiscoveredOperation.java @@ -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 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) { + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscoverer.java deleted file mode 100644 index 6a825dcc7d..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscoverer.java +++ /dev/null @@ -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 the type of the operation key - * @param the type of the operation - * @author Andy Wilkinson - * @author Stephane Nicoll - * @author Phillip Webb - * @since 2.0.0 - */ -public abstract class AnnotationEndpointDiscoverer - implements EndpointDiscoverer { - - private static final Log logger = LogFactory - .getLog(AnnotationEndpointDiscoverer.class); - - private final ApplicationContext applicationContext; - - private final Function operationKeyFactory; - - private final OperationsFactory operationsFactory; - - private final List> 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 operationFactory, Function operationKeyFactory, - ParameterMapper parameterMapper, - Collection invokerAdvisors, - Collection> 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> discoverEndpoints() { - Class operationType = getOperationType(); - Map, DiscoveredEndpoint> endpoints = getEndpoints(operationType); - Map, DiscoveredExtension> extensions = getExtensions(operationType, - endpoints); - Collection 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 }". - * @return the operation type - */ - @SuppressWarnings("unchecked") - protected Class getOperationType() { - return (Class) ResolvableType - .forClass(AnnotationEndpointDiscoverer.class, getClass()) - .resolveGeneric(1); - } - - private Map, DiscoveredEndpoint> getEndpoints(Class operationType) { - Map, DiscoveredEndpoint> endpoints = new LinkedHashMap<>(); - Map 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, DiscoveredEndpoint> endpoints, - Map 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 operations = this.operationsFactory - .createOperations(id, target, endpointType).values(); - EndpointInfo endpointInfo = new EndpointInfo<>(id, enabledByDefault, - operations); - boolean exposed = isEndpointExposed(endpointType, endpointInfo); - return new DiscoveredEndpoint(endpointType, endpointInfo, exposed); - } - - private Map, DiscoveredExtension> getExtensions(Class operationType, - Map, DiscoveredEndpoint> endpoints) { - Map, 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, DiscoveredEndpoint> endpoints, - Map, 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 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, 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 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 endpointInfo) { - for (EndpointFilter 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 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 endpointInfo) { - Class generic = ResolvableType.forClass(EndpointFilter.class, filterClass) - .resolveGeneric(0); - if (generic == null || generic.isAssignableFrom(getOperationType())) { - EndpointFilter filter = (EndpointFilter) BeanUtils - .instantiateClass(filterClass); - return isFilterMatch(filter, endpointInfo); - } - return false; - } - - private boolean isFilterMatch(EndpointFilter filter, - EndpointInfo 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 mergeExposed( - Map, DiscoveredEndpoint> endpoints, - Map, DiscoveredExtension> extensions) { - List 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 exposedEndpoints) { - } - - /** - * A discovered endpoint (which may not be valid and might not ultimately be exposed). - */ - protected final class DiscoveredEndpoint { - - private final EndpointInfo info; - - private final boolean exposed; - - private final Map> operations; - - private DiscoveredEndpoint(Class type, EndpointInfo info, boolean exposed) { - Assert.notNull(info, "Info must not be null"); - this.info = info; - this.exposed = exposed; - this.operations = indexEndpointOperations(type, info); - } - - private Map> indexEndpointOperations(Class endpointType, - EndpointInfo info) { - return Collections.unmodifiableMap( - indexOperations(info.getId(), endpointType, info.getOperations())); - } - - private DiscoveredEndpoint(EndpointInfo info, boolean exposed, - Map> 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 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> getOperations() { - return this.operations; - } - - /** - * Find any duplicate operations. - * @return any duplicate operations - */ - public Map> 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> operations = mergeOperations(extension); - EndpointInfo info = new EndpointInfo<>(this.info.getId(), - this.info.isEnableByDefault(), flatten(operations).values()); - return new DiscoveredEndpoint(info, this.exposed, operations); - } - - private Map> mergeOperations( - DiscoveredExtension extension) { - MultiValueMap operations = new LinkedMultiValueMap<>( - this.operations); - operations.addAll(indexOperations(getInfo().getId(), - extension.getExtensionType(), extension.getOperations())); - return Collections.unmodifiableMap(operations); - } - - private Map flatten(Map> operations) { - Map flattened = new LinkedHashMap<>(); - operations.forEach((operationKey, value) -> flattened - .put(operationKey.getKey(), getLastValue(value))); - return Collections.unmodifiableMap(flattened); - } - - private T getLastValue(List value) { - return value.get(value.size() - 1); - } - - private MultiValueMap indexOperations(String endpointId, - Class target, Collection operations) { - LinkedMultiValueMap 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 operations; - - private DiscoveredExtension(Class extensionType, Collection operations) { - this.extensionType = extensionType; - this.operations = operations; - } - - public Class getExtensionType() { - return this.extensionType; - } - - public Collection 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(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredEndpoint.java new file mode 100644 index 0000000000..2f7b09e2d7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredEndpoint.java @@ -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 The operation type + * @author Phillip Webb + * @since 2.0.0 + */ +public interface DiscoveredEndpoint extends ExposableEndpoint { + + /** + * 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> discoverer); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java new file mode 100644 index 0000000000..3ee20e10fd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java @@ -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 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 getProducesMediaTypes() { + return this.producesMediaTypes; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationsFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactory.java similarity index 55% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationsFactory.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactory.java index ab317a3ce5..f03407cfcc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationsFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactory.java @@ -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 The operation type + * @param The operation type * @author Andy Wilkinson * @author Stephane Nicoll * @author Phillip Webb */ -class OperationsFactory { +abstract class DiscoveredOperationsFactory { private static final Map> OPERATION_TYPES; @@ -58,56 +57,56 @@ class OperationsFactory { OPERATION_TYPES = Collections.unmodifiableMap(operationTypes); } - private final OperationFactory operationFactory; + private final ParameterValueMapper parameterValueMapper; - private final ParameterMapper parameterMapper; + private final Collection invokerAdvisors; - private final Collection invokerAdvisors; - - OperationsFactory(OperationFactory operationFactory, - ParameterMapper parameterMapper, - Collection invokerAdvisors) { - this.operationFactory = operationFactory; - this.parameterMapper = parameterMapper; - this.invokerAdvisors = (invokerAdvisors == null ? Collections.emptyList() - : new ArrayList<>(invokerAdvisors)); + DiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors) { + this.parameterValueMapper = parameterValueMapper; + this.invokerAdvisors = invokerAdvisors; } - public Map createOperations(String id, Object target, Class type) { - return MethodIntrospector.selectMethods(type, - (MetadataLookup) (method) -> createOperation(id, target, method)); + public Collection createOperations(String id, Object target) { + return MethodIntrospector.selectMethods(target.getClass(), + (MetadataLookup) (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 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); + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilter.java new file mode 100644 index 0000000000..4c1b0f5bcb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilter.java @@ -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> { + + private final Class> discoverer; + + /** + * Create a new {@link DiscovererEndpointFilter} instance. + * @param discoverer the required discoverer + */ + protected DiscovererEndpointFilter( + Class> discoverer) { + Assert.notNull(discoverer, "Discoverer must not be null"); + this.discoverer = discoverer; + } + + @Override + public boolean match(DiscoveredEndpoint endpoint) { + return endpoint.wasDiscoveredBy(this.discoverer); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Endpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Endpoint.java index 66fcdffcc1..94fff19801 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Endpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Endpoint.java @@ -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) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java new file mode 100644 index 0000000000..9897963cfa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java @@ -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 The endpoint type + * @param The operation type + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Phillip Webb + * @since 2.0.0 + */ +public abstract class EndpointDiscoverer, O extends Operation> + implements EndpointsSupplier { + + private static final Log logger = LogFactory.getLog(EndpointDiscoverer.class); + + private final ApplicationContext applicationContext; + + private final Collection> filters; + + private final DiscoveredOperationsFactory operationsFactory; + + private final Map filterEndpoints = new ConcurrentHashMap<>(); + + private volatile Collection 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 invokerAdvisors, + Collection> 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 getOperationsFactory( + ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors) { + return new DiscoveredOperationsFactory(parameterValueMapper, invokerAdvisors) { + + @Override + protected O createOperation(String endpointId, + DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { + return EndpointDiscoverer.this.createOperation(endpointId, + operationMethod, invoker); + } + + }; + } + + @Override + public final Collection getEndpoints() { + if (this.endpoints == null) { + this.endpoints = discoverEndpoints(); + } + return this.endpoints; + } + + private Collection discoverEndpoints() { + Collection endpointBeans = createEndpointBeans(); + addExtensionBeans(endpointBeans); + return convertToEndpoints(endpointBeans); + } + + private Collection createEndpointBeans() { + Map 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 endpointBeans) { + Map 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 convertToEndpoints(Collection endpointBeans) { + Set endpoints = new LinkedHashSet<>(); + for (EndpointBean endpointBean : endpointBeans) { + if (isEndpointExposed(endpointBean)) { + endpoints.add(convertToEndpoint(endpointBean)); + } + } + return Collections.unmodifiableSet(endpoints); + } + + private E convertToEndpoint(EndpointBean endpointBean) { + MultiValueMap 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 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 indexed, String id, + Object target, boolean replaceLast) { + Set replacedLast = new HashSet<>(); + Collection 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 getLast(List list) { + return CollectionUtils.isEmpty(list) ? null : list.get(list.size() - 1); + } + + private void assertNoDuplicateOperations(EndpointBean endpointBean, + MultiValueMap indexed) { + List duplicates = indexed.entrySet().stream() + .filter((entry) -> entry.getValue().size() > 1).map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!duplicates.isEmpty()) { + Set 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 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 instance = (EndpointFilter) BeanUtils + .instantiateClass(filter); + return isFilterMatch(instance, endpoint); + } + return false; + + } + + private boolean isFilterMatch(EndpointFilter filter, EndpointBean endpointBean) { + return isFilterMatch(filter, getFilterEndpoint(endpointBean)); + } + + private boolean isFilterMatch(EndpointFilter 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 getEndpointType() { + return (Class) 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 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 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 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 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 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/FilteredEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/FilteredEndpoint.java index 4f816148be..c5f3841966 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/FilteredEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/FilteredEndpoint.java @@ -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; * } * @author Phillip Webb * @since 2.0.0 + * @see DiscovererEndpointFilter */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationFactory.java deleted file mode 100644 index 46db9d0922..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OperationFactory.java +++ /dev/null @@ -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 the {@link Operation} type - * @author Andy Wilkinson - * @author Stephane Nicoll - * @author Phillip Webb - */ -@FunctionalInterface -public interface OperationFactory { - - /** - * 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); - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParametersMissingException.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/MissingParametersException.java similarity index 64% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParametersMissingException.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/MissingParametersException.java index 3f8d14a9bf..8a00070058 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParametersMissingException.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/MissingParametersException.java @@ -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 parameters; + private final Set missingParameters; - public ParametersMissingException(Set parameters) { + public MissingParametersException(Set 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 getParameters() { - return this.parameters; + public Set getMissingParameters() { + return this.missingParameters; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvoker.java similarity index 73% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationInvoker.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvoker.java index 49368aeb99..a2031df145 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvoker.java @@ -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 arguments); + Object invoke(Map arguments) throws MissingParametersException; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInvokerAdvisor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvokerAdvisor.java similarity index 65% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInvokerAdvisor.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvokerAdvisor.java index 3a92637d87..841cf4d0b7 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInvokerAdvisor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationInvokerAdvisor.java @@ -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); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameter.java new file mode 100644 index 0000000000..4bef3a74a8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameter.java @@ -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(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameters.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameters.java new file mode 100644 index 0000000000..100b335dac --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/OperationParameters.java @@ -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 { + + /** + * 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 stream(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMappingException.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterMappingException.java similarity index 54% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMappingException.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterMappingException.java index 0cc45e12f0..c928cc5da4 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMappingException.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterMappingException.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterValueMapper.java similarity index 70% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMapper.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterValueMapper.java index be0fea7808..16414c6636 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ParameterMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/ParameterValueMapper.java @@ -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 the actual type of the parameter * @throws ParameterMappingException when a mapping failure occurs */ - T mapParameter(Object value, Class type); + Object mapParameterValue(OperationParameter parameter, Object value) + throws ParameterMappingException; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java similarity index 50% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapper.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java index 3b44790b83..f4ba3fc0b1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java @@ -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 mapParameter(Object input, Class 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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverter.java similarity index 95% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverter.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverter.java index 8babd7cd2c..d95af382aa 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverter.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/package-info.java similarity index 83% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/package-info.java index dc0bfc39d4..a3222cbc8f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/convert/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/package-info.java new file mode 100644 index 0000000000..4227e8a2b5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInfo.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java similarity index 51% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInfo.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java index 1a17c08968..f73a60444d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/OperationMethodInfo.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java @@ -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 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 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; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java new file mode 100644 index 0000000000..85b81908c7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java new file mode 100644 index 0000000000..aa9fbc2f10 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java @@ -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 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 getOperationParameters(Parameter[] parameters, + String[] names) { + List 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 iterator() { + return this.operationParameters.iterator(); + } + + @Override + public Stream stream() { + return this.operationParameters.stream(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java new file mode 100644 index 0000000000..f1dd3be18a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java @@ -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 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 arguments) { + Set missing = this.operationMethod.getParameters().stream() + .filter((parameter) -> isMissing(arguments, parameter)) + .collect(Collectors.toSet()); + if (!missing.isEmpty()) { + throw new MissingParametersException(missing); + } + } + + private boolean isMissing(Map arguments, + OperationParameter parameter) { + if (parameter.isNullable()) { + return false; + } + return arguments.get(parameter.getName()) == null; + } + + private Object[] resolveArguments(Map arguments) { + return this.operationMethod.getParameters().stream() + .map((parameter) -> resolveArgument(parameter, arguments)).toArray(); + } + + private Object resolveArgument(OperationParameter parameter, + Map 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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/package-info.java similarity index 83% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/package-info.java index f4536f7a99..d102f9cf3a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java similarity index 84% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvoker.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java index e1d299e58b..96960b75a4 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java @@ -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; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java similarity index 62% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisor.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java index 91d72c0950..b0f02f6095 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java @@ -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 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); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/package-info.java similarity index 83% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/package-info.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/package-info.java index e2568b6501..0587c18d42 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/cache/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index 9579f21a0a..372210c73c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -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 operationResponseConverter; + private final JmxOperationResponseMapper responseMapper; - private final EndpointMBeanInfo endpointInfo; + private final ExposableJmxEndpoint endpoint; - EndpointMBean(Function operationResponseConverter, - EndpointMBeanInfo endpointInfo) { - this.operationResponseConverter = operationResponseConverter; - this.endpointInfo = endpointInfo; + private final MBeanInfo info; + + private final Map 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 getOperations(ExposableJmxEndpoint endpoint) { + Map 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 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 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 getArguments(Object[] params, - List parameters) { + private Map getArguments(String[] parameterNames, Object[] params) { Map 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 diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfo.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfo.java deleted file mode 100644 index b189c8fca5..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfo.java +++ /dev/null @@ -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 operations; - - public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo, - Map operations) { - this.endpointId = endpointId; - this.mBeanInfo = mBeanInfo; - this.operations = operations; - } - - public String getEndpointId() { - return this.endpointId; - } - - public MBeanInfo getMbeanInfo() { - return this.mBeanInfo; - } - - public Map getOperations() { - return this.operations; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssembler.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssembler.java deleted file mode 100644 index c5d56c7e20..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssembler.java +++ /dev/null @@ -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 endpointInfo) { - Map operationsMapping = getOperationInfo(endpointInfo); - ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values() - .stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList()) - .toArray(new ModelMBeanOperationInfo[] {}); - Map 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 getOperationInfo( - EndpointInfo endpointInfo) { - Map 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; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrar.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrar.java deleted file mode 100644 index ca48373bde..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrar.java +++ /dev/null @@ -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); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointObjectNameFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointObjectNameFactory.java index 63eb529b87..c3c858b289 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointObjectNameFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointObjectNameFactory.java @@ -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; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ExposableJmxEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ExposableJmxEndpoint.java new file mode 100644 index 0000000000..1b7cf69cdc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/ExposableJmxEndpoint.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapper.java new file mode 100644 index 0000000000..a75a9eb2a0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapper.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java new file mode 100644 index 0000000000..97f71333db --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporter.java @@ -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 endpoints; + + private Collection registered; + + public JmxEndpointExporter(MBeanServer mBeanServer, + EndpointObjectNameFactory objectNameFactory, + JmxOperationResponseMapper responseMapper, + Collection 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 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 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() + "'"; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointMBeanFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointMBeanFactory.java deleted file mode 100644 index 250e12cd48..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointMBeanFactory.java +++ /dev/null @@ -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 createMBeans( - Collection> endpoints) { - return endpoints.stream().map(this::createMBean).collect(Collectors.toList()); - } - - private EndpointMBean createMBean(EndpointInfo endpointInfo) { - EndpointMBeanInfo endpointMBeanInfo = this.assembler - .createEndpointMBeanInfo(endpointInfo); - return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointsSupplier.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointsSupplier.java new file mode 100644 index 0000000000..2885a80a6d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointsSupplier.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperation.java index e9b53774f6..e95870110e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperation.java @@ -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 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 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 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 getParameters(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointOperationParameterInfo.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationParameter.java similarity index 62% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointOperationParameterInfo.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationParameter.java index 0f622e3780..d55d7f659f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointOperationParameterInfo.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationParameter.java @@ -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(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationResponseMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationResponseMapper.java index 1730a572fc..f2b1c93309 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationResponseMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxOperationResponseMapper.java @@ -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); + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactory.java new file mode 100644 index 0000000000..2d874c52c6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactory.java @@ -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 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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxEndpoint.java new file mode 100644 index 0000000000..086985c8f3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxEndpoint.java @@ -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 + implements ExposableJmxEndpoint { + + DiscoveredJmxEndpoint(EndpointDiscoverer discoverer, String id, + boolean enabledByDefault, Collection operations) { + super(discoverer, id, enabledByDefault, operations); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperation.java new file mode 100644 index 0000000000..08cb765214 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperation.java @@ -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 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 fallback) { + ManagedOperation managed = jmxAttributeSource.getManagedOperation(method); + if (managed != null && StringUtils.hasText(managed.getDescription())) { + return managed.getDescription(); + } + return fallback.get(); + } + + private List 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 mergeParameters( + OperationParameters operationParameters, + ManagedOperationParameter[] managedParameters) { + List 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 List asList(Stream 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 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; + } + + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscoverer.java deleted file mode 100644 index 466c0c27b9..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscoverer.java +++ /dev/null @@ -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 { - - 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 invokerAdvisors, - Collection> filters) { - super(applicationContext, new JmxEndpointOperationFactory(), - JmxOperation::getOperationName, parameterMapper, invokerAdvisors, - filters); - } - - @Override - protected void verify(Collection exposedEndpoints) { - List> 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()); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpoint.java index def482bdcd..48f17206bd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpoint.java @@ -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) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscoverer.java new file mode 100644 index 0000000000..4bc61741a0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscoverer.java @@ -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 + 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 invokerAdvisors, + Collection> filters) { + super(applicationContext, parameterValueMapper, invokerAdvisors, filters); + } + + @Override + protected ExposableJmxEndpoint createEndpoint(String id, boolean enabledByDefault, + Collection 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() + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointFilter.java index d5519aa793..a6a789803f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointFilter.java @@ -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 { +class JmxEndpointFilter extends DiscovererEndpointFilter { - @Override - public boolean match(EndpointInfo info, - EndpointDiscoverer discoverer) { - return (discoverer instanceof JmxAnnotationEndpointDiscoverer); + JmxEndpointFilter() { + super(JmxEndpointDiscoverer.class); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointOperationFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointOperationFactory.java deleted file mode 100644 index 8070f999f8..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointOperationFactory.java +++ /dev/null @@ -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 { - - @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 fallback) { - ManagedOperation managedOperation = JmxAnnotationEndpointDiscoverer.jmxAttributeSource - .getManagedOperation(method); - if (managedOperation != null - && StringUtils.hasText(managedOperation.getDescription())) { - return managedOperation.getDescription(); - } - return fallback.get(); - } - - private List 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 mergeParameters( - Parameter[] methodParameters, - ManagedOperationParameter[] operationParameters) { - List 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 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; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ReflectiveOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ReflectiveOperationInvoker.java deleted file mode 100644 index ae4037c4eb..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/reflect/ReflectiveOperationInvoker.java +++ /dev/null @@ -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 arguments) { - Map parameters = this.methodInfo.getParameters(); - validateRequiredParameters(parameters, arguments); - return ReflectionUtils.invokeMethod(this.methodInfo.getMethod(), this.target, - resolveArguments(parameters, arguments)); - } - - private void validateRequiredParameters(Map parameters, - Map arguments) { - Set 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 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 parameters, - Map 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 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(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java index 6c045ed825..e95ff06cc2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java @@ -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 resolveLinks( - Collection> webEndpoints, String requestUrl) { + public Map resolveLinks(Collection endpoints, + String requestUrl) { String normalizedUrl = normalizeRequestUrl(requestUrl); Map links = new LinkedHashMap<>(); links.put("self", new Link(normalizedUrl)); - for (EndpointInfo 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))); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java index 76c442e049..4ef96a832d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java @@ -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 getProduced() { @@ -58,7 +57,6 @@ public class EndpointMediaTypes { /** * Returns the media types consumed by an endpoint. - * * @return the consumed media types */ public List getConsumed() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ExposableWebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ExposableWebEndpoint.java new file mode 100644 index 0000000000..a7a78f95e7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ExposableWebEndpoint.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/Link.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/Link.java index 5365895e05..137ab70323 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/Link.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/Link.java @@ -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("{"); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointsSupplier.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointsSupplier.java new file mode 100644 index 0000000000..80ce5ab7ef --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointsSupplier.java @@ -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.EndpointsSupplier; + +/** + * {@link EndpointsSupplier} for {@link ExposableWebEndpoint web endpoints}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public interface WebEndpointsSupplier extends EndpointsSupplier { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperation.java index cb9b66ffa4..e6320f3a25 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperation.java @@ -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,61 +17,32 @@ package org.springframework.boot.actuate.endpoint.web; 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 web endpoint. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ -public class WebOperation extends Operation { - - private final OperationRequestPredicate requestPredicate; - - private final String id; +public interface WebOperation extends Operation { /** - * Creates a new {@code WebEndpointOperation} with the given {@code type}. The - * operation can be performed using the given {@code operationInvoker}. The operation - * can handle requests that match the given {@code requestPredicate}. - * @param type the type of the operation - * @param operationInvoker used to perform the operation - * @param blocking whether or not this is a blocking operation - * @param requestPredicate the predicate for requests that can be handled by the - * @param id the id of the operation, unique within its endpoint operation + * Returns the ID of the operation that uniquely identifies it within its endpoint. + * @return the ID */ - public WebOperation(OperationType type, OperationInvoker operationInvoker, - boolean blocking, OperationRequestPredicate requestPredicate, String id) { - super(type, operationInvoker, blocking); - this.requestPredicate = requestPredicate; - this.id = id; - } + String getId(); /** - * Returns the predicate for requests that can be handled by this operation. - * @return the predicate + * Returns if the underlying operation is blocking. + * @return {@code true} if the operation is blocking */ - public OperationRequestPredicate getRequestPredicate() { - return this.requestPredicate; - } + boolean isBlocking(); /** - * Returns the ID of the operation that uniquely identifies it within its endpoint. - * @return the ID + * Returns the predicate for requests that can be handled by this operation. + * @return the predicate */ - public String getId() { - return this.id; - } - - @Override - public String toString() { - return new ToStringCreator(this).append("type", getType()) - .append("invoker", getInvoker()).append("blocking", isBlocking()) - .append("requestPredicate", getRequestPredicate()).append("id", getId()) - .toString(); - } + WebOperationRequestPredicate getRequestPredicate(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicate.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicate.java similarity index 79% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicate.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicate.java index 4946e83d40..cf8494b2f5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicate.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicate.java @@ -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,8 @@ package org.springframework.boot.actuate.endpoint.web; import java.util.Collection; import java.util.Collections; -import org.springframework.core.style.ToStringCreator; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * A predicate for a request to an operation on a web endpoint. @@ -27,7 +28,7 @@ import org.springframework.core.style.ToStringCreator; * @author Andy Wilkinson * @since 2.0.0 */ -public class OperationRequestPredicate { +public final class WebOperationRequestPredicate { private final String path; @@ -41,13 +42,12 @@ public class OperationRequestPredicate { /** * Creates a new {@code OperationRequestPredicate}. - * * @param path the path for the operation * @param httpMethod the HTTP method that the operation supports * @param produces the media types that the operation produces * @param consumes the media types that the operation consumes */ - public OperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod, + public WebOperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod, Collection consumes, Collection produces) { this.path = path; this.canonicalPath = path.replaceAll("\\{.*?}", "{*}"); @@ -90,9 +90,17 @@ public class OperationRequestPredicate { @Override public String toString() { - return new ToStringCreator(this).append("httpMethod", this.httpMethod) - .append("path", this.path).append("consumes", this.consumes) - .append("produces", this.produces).toString(); + StringBuilder result = new StringBuilder( + this.httpMethod + " to path '" + this.path + "'"); + if (!CollectionUtils.isEmpty(this.consumes)) { + result.append(" consumes: " + + StringUtils.collectionToCommaDelimitedString(this.consumes)); + } + if (!CollectionUtils.isEmpty(this.produces)) { + result.append(" produces: " + + StringUtils.collectionToCommaDelimitedString(this.produces)); + } + return result.toString(); } @Override @@ -114,7 +122,7 @@ public class OperationRequestPredicate { if (obj == null || getClass() != obj.getClass()) { return false; } - OperationRequestPredicate other = (OperationRequestPredicate) obj; + WebOperationRequestPredicate other = (WebOperationRequestPredicate) obj; boolean result = true; result = result && this.consumes.equals(other.consumes); result = result && this.httpMethod == other.httpMethod; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java new file mode 100644 index 0000000000..45cbbab417 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java @@ -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.web.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.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebOperation; + +/** + * A discovered {@link ExposableWebEndpoint web endpoint}. + * + * @author Phillip Webb + */ +class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint + implements ExposableWebEndpoint { + + DiscoveredWebEndpoint(EndpointDiscoverer discoverer, String id, + boolean enabledByDefault, Collection operations) { + super(discoverer, id, enabledByDefault, operations); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebOperation.java new file mode 100644 index 0000000000..a2bc7071c7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebOperation.java @@ -0,0 +1,102 @@ +/* + * 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.annotation; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.reactivestreams.Publisher; + +import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredOperation; +import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.ClassUtils; + +/** + * A discovered {@link WebOperation web operation}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Phillip Webb + */ +class DiscoveredWebOperation extends AbstractDiscoveredOperation implements WebOperation { + + private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( + "org.reactivestreams.Publisher", + DiscoveredWebOperation.class.getClassLoader()); + + private final String id; + + private final boolean blocking; + + private final WebOperationRequestPredicate requestPredicate; + + DiscoveredWebOperation(String endpointId, DiscoveredOperationMethod operationMethod, + OperationInvoker invoker, WebOperationRequestPredicate requestPredicate) { + super(operationMethod, invoker); + Method method = operationMethod.getMethod(); + this.id = getId(endpointId, method); + this.blocking = getBlocking(method); + this.requestPredicate = requestPredicate; + } + + private String getId(String endpointId, Method method) { + return endpointId + Stream.of(method.getParameters()).filter(this::hasSelector) + .map(this::dashName).collect(Collectors.joining()); + } + + private boolean hasSelector(Parameter parameter) { + return parameter.getAnnotation(Selector.class) != null; + } + + private String dashName(Parameter parameter) { + return "-" + parameter.getName(); + } + + private boolean getBlocking(Method method) { + return !REACTIVE_STREAMS_PRESENT + || !Publisher.class.isAssignableFrom(method.getReturnType()); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public boolean isBlocking() { + return this.blocking; + } + + @Override + public WebOperationRequestPredicate getRequestPredicate() { + return this.requestPredicate; + } + + @Override + protected void appendFields(ToStringCreator creator) { + creator.append("id", this.id).append("blocking", this.blocking) + .append("requestPredicate", this.requestPredicate); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointOperationFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java similarity index 51% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointOperationFactory.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java index 3d4d77b253..39c8251512 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointOperationFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java @@ -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,107 +17,91 @@ package org.springframework.boot.actuate.endpoint.web.annotation; import java.lang.reflect.Method; -import java.util.Arrays; +import java.lang.reflect.Parameter; import java.util.Collection; import java.util.Collections; +import java.util.stream.Collectors; import java.util.stream.Stream; -import org.reactivestreams.Publisher; - -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.annotation.DiscoveredOperationMethod; import org.springframework.boot.actuate.endpoint.annotation.Selector; -import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver; -import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; -import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; -import org.springframework.util.ClassUtils; /** - * {@link OperationFactory} for {@link WebOperation web operations}. + * Factory to create a {@link WebOperationRequestPredicate}. * * @author Andy Wilkinson * @author Stephane Nicoll * @author Phillip Webb */ -final class WebEndpointOperationFactory implements OperationFactory { - - private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( - "org.reactivestreams.Publisher", - WebEndpointOperationFactory.class.getClassLoader()); +class RequestPredicateFactory { private final EndpointMediaTypes endpointMediaTypes; private final EndpointPathResolver endpointPathResolver; - WebEndpointOperationFactory(EndpointMediaTypes endpointMediaTypes, + RequestPredicateFactory(EndpointMediaTypes endpointMediaTypes, EndpointPathResolver endpointPathResolver) { this.endpointMediaTypes = endpointMediaTypes; this.endpointPathResolver = endpointPathResolver; } - @Override - public WebOperation createOperation(String endpointId, OperationMethodInfo methodInfo, - Object target, OperationInvoker invoker) { - Method method = methodInfo.getMethod(); - OperationType operationType = methodInfo.getOperationType(); - WebEndpointHttpMethod httpMethod = determineHttpMethod(operationType); - OperationRequestPredicate requestPredicate = new OperationRequestPredicate( - determinePath(endpointId, method), httpMethod, - determineConsumedMediaTypes(httpMethod, method), - determineProducedMediaTypes(methodInfo.getProduces(), method)); - return new WebOperation(operationType, invoker, determineBlocking(method), - requestPredicate, determineId(endpointId, method)); + public WebOperationRequestPredicate getRequestPredicate(String endpointId, + DiscoveredOperationMethod operationMethod) { + Method method = operationMethod.getMethod(); + String path = getPath(endpointId, method); + WebEndpointHttpMethod httpMethod = determineHttpMethod( + operationMethod.getOperationType()); + Collection consumes = getConsumes(httpMethod, method); + Collection produces = getProduces(operationMethod, method); + return new WebOperationRequestPredicate(path, httpMethod, consumes, produces); + } + + private String getPath(String endpointId, Method method) { + return this.endpointPathResolver.resolvePath(endpointId) + + Stream.of(method.getParameters()).filter(this::hasSelector) + .map(this::slashName).collect(Collectors.joining()); } - private String determinePath(String endpointId, Method operationMethod) { - StringBuilder path = new StringBuilder( - this.endpointPathResolver.resolvePath(endpointId)); - Stream.of(operationMethod.getParameters()) - .filter((parameter) -> parameter.getAnnotation(Selector.class) != null) - .map((parameter) -> "/{" + parameter.getName() + "}") - .forEach(path::append); - return path.toString(); + private boolean hasSelector(Parameter parameter) { + return parameter.getAnnotation(Selector.class) != null; } - private String determineId(String endpointId, Method operationMethod) { - StringBuilder path = new StringBuilder(endpointId); - Stream.of(operationMethod.getParameters()) - .filter((parameter) -> parameter.getAnnotation(Selector.class) != null) - .map((parameter) -> "-" + parameter.getName()).forEach(path::append); - return path.toString(); + private String slashName(Parameter parameter) { + return "/{" + parameter.getName() + "}"; } - private Collection determineConsumedMediaTypes( - WebEndpointHttpMethod httpMethod, Method method) { + private Collection getConsumes(WebEndpointHttpMethod httpMethod, + Method method) { if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) { return this.endpointMediaTypes.getConsumed(); } return Collections.emptyList(); } - private Collection determineProducedMediaTypes(String[] produces, + private Collection getProduces(DiscoveredOperationMethod operationMethod, Method method) { - if (produces.length > 0) { - return Arrays.asList(produces); + if (!operationMethod.getProducesMediaTypes().isEmpty()) { + return operationMethod.getProducesMediaTypes(); } if (Void.class.equals(method.getReturnType()) || void.class.equals(method.getReturnType())) { return Collections.emptyList(); } - if (producesResourceResponseBody(method)) { + if (producesResource(method)) { return Collections.singletonList("application/octet-stream"); } return this.endpointMediaTypes.getProduced(); } - private boolean producesResourceResponseBody(Method method) { + private boolean producesResource(Method method) { if (Resource.class.equals(method.getReturnType())) { return true; } @@ -146,9 +130,4 @@ final class WebEndpointOperationFactory implements OperationFactory { - - /** - * Creates a new {@link WebAnnotationEndpointDiscoverer} that will discover - * {@link Endpoint endpoints} and {@link EndpointWebExtension web 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 endpointMediaTypes the media types produced and consumed by web endpoint - * operations - * @param endpointPathResolver the {@link EndpointPathResolver} used to resolve - * endpoint paths - * @param invokerAdvisors advisors used to add additional invoker advise - * @param filters filters that must match for an endpoint to be exposed - */ - public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext, - ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, - EndpointPathResolver endpointPathResolver, - Collection invokerAdvisors, - Collection> filters) { - super(applicationContext, - new WebEndpointOperationFactory(endpointMediaTypes, endpointPathResolver), - WebOperation::getRequestPredicate, parameterMapper, invokerAdvisors, - filters); - } - - @Override - protected void verify(Collection exposedEndpoints) { - List> clashes = new ArrayList<>(); - exposedEndpoints.forEach((descriptor) -> clashes - .addAll(descriptor.findDuplicateOperations().values())); - if (!clashes.isEmpty()) { - StringBuilder message = new StringBuilder(); - message.append(String.format( - "Found multiple web operations with matching request predicates:%n")); - clashes.forEach((clash) -> { - message.append(" ").append(clash.get(0).getRequestPredicate()) - .append(String.format(":%n")); - clash.forEach((operation) -> message.append(" ") - .append(String.format("%s%n", operation))); - }); - throw new IllegalStateException(message.toString()); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpoint.java index 2c19a331e7..f07d386496 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpoint.java @@ -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 Andy Wilkinson * @author Phillip Webb * @since 2.0.0 - * @see AnnotationEndpointDiscoverer */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java new file mode 100644 index 0000000000..e668eace02 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java @@ -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.web.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.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.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.context.ApplicationContext; + +/** + * {@link EndpointDiscoverer} for {@link ExposableWebEndpoint web endpoints}. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class WebEndpointDiscoverer + extends EndpointDiscoverer + implements WebEndpointsSupplier { + + private final RequestPredicateFactory requestPredicateFactory; + + /** + * 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 WebEndpointDiscoverer(ApplicationContext applicationContext, + ParameterValueMapper parameterValueMapper, + EndpointMediaTypes endpointMediaTypes, + EndpointPathResolver endpointPathResolver, + Collection invokerAdvisors, + Collection> filters) { + super(applicationContext, parameterValueMapper, invokerAdvisors, filters); + this.requestPredicateFactory = new RequestPredicateFactory(endpointMediaTypes, + endpointPathResolver); + } + + @Override + protected ExposableWebEndpoint createEndpoint(String id, boolean enabledByDefault, + Collection operations) { + return new DiscoveredWebEndpoint(this, id, enabledByDefault, operations); + } + + @Override + protected WebOperation createOperation(String endpointId, + DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { + WebOperationRequestPredicate requestPredicate = this.requestPredicateFactory + .getRequestPredicate(endpointId, operationMethod); + return new DiscoveredWebOperation(endpointId, operationMethod, invoker, + requestPredicate); + } + + @Override + protected OperationKey createOperationKey(WebOperation operation) { + return new OperationKey(operation.getRequestPredicate(), + () -> "web request predicate " + operation.getRequestPredicate()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointFilter.java index 071027c98f..70559fe3dc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointFilter.java @@ -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.web.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.web.WebOperation; +import org.springframework.boot.actuate.endpoint.annotation.DiscovererEndpointFilter; /** - * {@link EndpointFilter} for endpoints discovered by - * {@link WebAnnotationEndpointDiscoverer}. + * {@link EndpointFilter} for endpoints discovered by {@link WebEndpointDiscoverer}. * * @author Phillip Webb */ -class WebEndpointFilter implements EndpointFilter { +class WebEndpointFilter extends DiscovererEndpointFilter { - @Override - public boolean match(EndpointInfo info, - EndpointDiscoverer discoverer) { - return (discoverer instanceof WebAnnotationEndpointDiscoverer); + WebEndpointFilter() { + super(WebEndpointDiscoverer.class); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index 20b46656a6..d44967d40f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -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. @@ -38,16 +38,15 @@ import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.server.model.Resource.Builder; import reactor.core.publisher.Mono; -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.invoke.MissingParametersException; +import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException; 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.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -58,6 +57,7 @@ import org.springframework.util.StringUtils; * endpoint operations}. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ public class JerseyEndpointResourceFactory { @@ -68,34 +68,34 @@ public class JerseyEndpointResourceFactory { * Creates {@link Resource Resources} for the operations of the given * {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints - * @param webEndpoints the web endpoints + * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @return the resources for the operations */ public Collection createEndpointResources(EndpointMapping endpointMapping, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes) { List resources = new ArrayList<>(); - webEndpoints.stream() - .flatMap((endpointInfo) -> endpointInfo.getOperations().stream()) + endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream()) .map((operation) -> createResource(endpointMapping, operation)) .forEach(resources::add); if (StringUtils.hasText(endpointMapping.getPath())) { - resources.add(createEndpointLinksResource(endpointMapping.getPath(), - webEndpoints, endpointMediaTypes)); + Resource resource = createEndpointLinksResource(endpointMapping.getPath(), + endpoints, endpointMediaTypes); + resources.add(resource); } return resources; } private Resource createResource(EndpointMapping endpointMapping, WebOperation operation) { - OperationRequestPredicate requestPredicate = operation.getRequestPredicate(); + WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); Builder resourceBuilder = Resource.builder() .path(endpointMapping.createSubPath(requestPredicate.getPath())); resourceBuilder.addMethod(requestPredicate.getHttpMethod().name()) .consumes(toStringArray(requestPredicate.getConsumes())) .produces(toStringArray(requestPredicate.getProduces())) - .handledBy(new EndpointInvokingInflector(operation.getInvoker(), + .handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty())); return resourceBuilder.build(); } @@ -105,21 +105,21 @@ public class JerseyEndpointResourceFactory { } private Resource createEndpointLinksResource(String endpointPath, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes) { Builder resourceBuilder = Resource.builder().path(endpointPath); resourceBuilder.addMethod("GET") .produces(endpointMediaTypes.getProduced() .toArray(new String[endpointMediaTypes.getProduced().size()])) - .handledBy(new EndpointLinksInflector(webEndpoints, + .handledBy(new EndpointLinksInflector(endpoints, this.endpointLinksResolver)); return resourceBuilder.build(); } /** - * {@link Inflector} to invoke the endpoint. + * {@link Inflector} to invoke the {@link WebOperation}. */ - private static final class EndpointInvokingInflector + private static final class OperationInflector implements Inflector { private static final List> BODY_CONVERTERS; @@ -128,19 +128,18 @@ public class JerseyEndpointResourceFactory { List> converters = new ArrayList<>(); converters.add(new ResourceBodyConverter()); if (ClassUtils.isPresent("reactor.core.publisher.Mono", - EndpointInvokingInflector.class.getClassLoader())) { + OperationInflector.class.getClassLoader())) { converters.add(new MonoBodyConverter()); } BODY_CONVERTERS = Collections.unmodifiableList(converters); } - private final OperationInvoker operationInvoker; + private final WebOperation operation; private final boolean readBody; - private EndpointInvokingInflector(OperationInvoker operationInvoker, - boolean readBody) { - this.operationInvoker = operationInvoker; + private OperationInflector(WebOperation operation, boolean readBody) { + this.operation = operation; this.readBody = readBody; } @@ -153,10 +152,10 @@ public class JerseyEndpointResourceFactory { arguments.putAll(extractPathParameters(data)); arguments.putAll(extractQueryParameters(data)); try { - Object response = this.operationInvoker.invoke(arguments); + Object response = this.operation.invoke(arguments); return convertToJaxRsResponse(response, data.getRequest().getMethod()); } - catch (ParametersMissingException | ParameterMappingException ex) { + catch (MissingParametersException | ParameterMappingException ex) { return Response.status(Status.BAD_REQUEST).build(); } } @@ -263,11 +262,11 @@ public class JerseyEndpointResourceFactory { private static final class EndpointLinksInflector implements Inflector { - private final Collection> endpoints; + private final Collection endpoints; private final EndpointLinksResolver linksResolver; - private EndpointLinksInflector(Collection> endpoints, + private EndpointLinksInflector(Collection endpoints, EndpointLinksResolver linksResolver) { this.endpoints = endpoints; this.linksResolver = linksResolver; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index b657553cf8..d494736428 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -18,20 +18,32 @@ package org.springframework.boot.actuate.endpoint.web.reactive; import java.lang.reflect.Method; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.Map; +import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; import reactor.core.scheduler.Schedulers; -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.invoke.MissingParametersException; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; -import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; 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.util.StringUtils; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; +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.condition.ConsumesRequestCondition; @@ -40,6 +52,7 @@ import org.springframework.web.reactive.result.condition.ProducesRequestConditio import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.pattern.PathPatternParser; /** @@ -48,6 +61,7 @@ import org.springframework.web.util.pattern.PathPatternParser; * * @author Andy Wilkinson * @author Madhura Bhave + * @author Phillip Webb * @since 2.0.0 */ public abstract class AbstractWebFluxEndpointHandlerMapping @@ -57,38 +71,34 @@ public abstract class AbstractWebFluxEndpointHandlerMapping private final EndpointMapping endpointMapping; - private final Collection> webEndpoints; + private final Collection endpoints; private final EndpointMediaTypes endpointMediaTypes; private final CorsConfiguration corsConfiguration; - /** - * 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 collection the web endpoints - * @param endpointMediaTypes media types consumed and produced by the endpoints - */ - public AbstractWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection, - EndpointMediaTypes endpointMediaTypes) { - this(endpointMapping, collection, endpointMediaTypes, null); - } + private final Method linksMethod = ReflectionUtils.findMethod(getClass(), "links", + ServerWebExchange.class); + + private final Method handleWriteMethod = ReflectionUtils.findMethod( + WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class); + + private final Method handleReadMethod = ReflectionUtils + .findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class); /** - * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the - * operations of the given {@code webEndpoints}. + * Creates a new {@code AbstractWebFluxEndpointHandlerMapping} 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 endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints */ public AbstractWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { this.endpointMapping = endpointMapping; - this.webEndpoints = webEndpoints; + this.endpoints = endpoints; this.endpointMediaTypes = endpointMediaTypes; this.corsConfiguration = corsConfiguration; setOrder(-100); @@ -96,37 +106,59 @@ public abstract class AbstractWebFluxEndpointHandlerMapping @Override protected void initHandlerMethods() { - this.webEndpoints.stream() - .flatMap((webEndpoint) -> webEndpoint.getOperations().stream()) - .forEach(this::registerMappingForOperation); + for (ExposableWebEndpoint endpoint : this.endpoints) { + for (WebOperation operation : endpoint.getOperations()) { + registerMappingForOperation(endpoint, operation); + } + } if (StringUtils.hasText(this.endpointMapping.getPath())) { registerLinksMapping(); } } - private void registerLinksMapping() { - PatternsRequestCondition patterns = new PatternsRequestCondition( - pathPatternParser.parse(this.endpointMapping.getPath())); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.GET); - ProducesRequestCondition produces = new ProducesRequestCondition( - this.endpointMediaTypes.getProduced().toArray( - new String[this.endpointMediaTypes.getProduced().size()])); - RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, - null, produces, null); - registerMapping(mapping, this, getLinks()); + private void registerMappingForOperation(ExposableWebEndpoint endpoint, + WebOperation operation) { + OperationInvoker invoker = operation::invoke; + if (operation.isBlocking()) { + invoker = new ElasticSchedulerInvoker(invoker); + } + ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, + operation, new ReactiveWebOperationAdapter(invoker)); + if (operation.getType() == OperationType.WRITE) { + registerMapping(createRequestMappingInfo(operation), + new WriteOperationHandler((reactiveWebOperation)), + this.handleWriteMethod); + } + else { + registerMapping(createRequestMappingInfo(operation), + new ReadOperationHandler((reactiveWebOperation)), + this.handleReadMethod); + } } - protected RequestMappingInfo createRequestMappingInfo(WebOperation operationInfo) { - OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate(); + /** + * Hook point that allows subclasses to wrap the {@link ReactiveWebOperation} before + * it's called. Allows additional features, such as security, to be added. + * @param endpoint the source endpoint + * @param operation the source operation + * @param reactiveWebOperation the reactive web operation to wrap + * @return a wrapped reactive web operation + */ + protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint, + WebOperation operation, ReactiveWebOperation reactiveWebOperation) { + return reactiveWebOperation; + } + + private RequestMappingInfo createRequestMappingInfo(WebOperation operation) { + WebOperationRequestPredicate predicate = operation.getRequestPredicate(); PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser - .parse(this.endpointMapping.createSubPath(requestPredicate.getPath()))); + .parse(this.endpointMapping.createSubPath(predicate.getPath()))); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.valueOf(requestPredicate.getHttpMethod().name())); + RequestMethod.valueOf(predicate.getHttpMethod().name())); ConsumesRequestCondition consumes = new ConsumesRequestCondition( - toStringArray(requestPredicate.getConsumes())); + toStringArray(predicate.getConsumes())); ProducesRequestCondition produces = new ProducesRequestCondition( - toStringArray(requestPredicate.getProduces())); + toStringArray(predicate.getProduces())); return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null); } @@ -135,20 +167,25 @@ public abstract class AbstractWebFluxEndpointHandlerMapping return collection.toArray(new String[collection.size()]); } + private void registerLinksMapping() { + PatternsRequestCondition patterns = new PatternsRequestCondition( + pathPatternParser.parse(this.endpointMapping.getPath())); + RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( + RequestMethod.GET); + ProducesRequestCondition produces = new ProducesRequestCondition( + this.endpointMediaTypes.getProduced().toArray( + new String[this.endpointMediaTypes.getProduced().size()])); + RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, + null, produces, null); + registerMapping(mapping, this, this.linksMethod); + } + @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) { return this.corsConfiguration; } - public Collection> getEndpoints() { - return this.webEndpoints; - } - - protected abstract Method getLinks(); - - protected abstract void registerMappingForOperation(WebOperation operation); - @Override protected boolean isHandler(Class beanType) { return false; @@ -160,17 +197,26 @@ public abstract class AbstractWebFluxEndpointHandlerMapping return null; } + protected abstract Object links(ServerWebExchange exchange); + + /** + * Return the web endpoints being mapped. + * @return the endpoints + */ + public Collection getEndpoints() { + return this.endpoints; + } + /** * An {@link OperationInvoker} that performs the invocation of a blocking operation on * a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}. */ - protected static final class ElasticSchedulerOperationInvoker - implements OperationInvoker { + protected static final class ElasticSchedulerInvoker implements OperationInvoker { - private final OperationInvoker delegate; + private final OperationInvoker invoker; - public ElasticSchedulerOperationInvoker(OperationInvoker delegate) { - this.delegate = delegate; + public ElasticSchedulerInvoker(OperationInvoker invoker) { + this.invoker = invoker; } @Override @@ -181,7 +227,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping private void invoke(Map arguments, MonoSink sink) { try { - Object result = this.delegate.invoke(arguments); + Object result = this.invoker.invoke(arguments); sink.success(result); } catch (Exception ex) { @@ -191,4 +237,109 @@ public abstract class AbstractWebFluxEndpointHandlerMapping } + /** + * An reactive web operation that can be handled by WebFlux. + */ + protected interface ReactiveWebOperation { + + Mono> handle(ServerWebExchange exchange, + Map body); + } + + /** + * Adapter class to convert an {@link OperationInvoker} into a + * {@link ReactiveWebOperation}. + */ + private class ReactiveWebOperationAdapter implements ReactiveWebOperation { + + private final OperationInvoker invoker; + + ReactiveWebOperationAdapter(OperationInvoker invoker) { + this.invoker = invoker; + } + + @Override + public Mono> handle(ServerWebExchange exchange, + Map body) { + Map arguments = getArguments(exchange, body); + return handleResult((Publisher) this.invoker.invoke(arguments), + exchange.getRequest().getMethod()); + } + + private Map getArguments(ServerWebExchange exchange, + Map body) { + Map arguments = new LinkedHashMap<>(); + arguments.putAll(getTemplateVariables(exchange)); + if (body != null) { + arguments.putAll(body); + } + exchange.getRequest().getQueryParams().forEach((name, values) -> arguments + .put(name, values.size() == 1 ? values.get(0) : values)); + return arguments; + } + + @SuppressWarnings("unchecked") + private Map getTemplateVariables(ServerWebExchange exchange) { + return (Map) exchange + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + } + + private Mono> handleResult(Publisher result, + HttpMethod httpMethod) { + return Mono.from(result).map(this::toResponseEntity) + .onErrorReturn(MissingParametersException.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 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())); + } + + } + + /** + * Handler for a {@link ReactiveWebOperation}. + */ + private final class WriteOperationHandler { + + private final ReactiveWebOperation operation; + + WriteOperationHandler(ReactiveWebOperation operation) { + this.operation = operation; + } + + @ResponseBody + public Publisher> handle(ServerWebExchange exchange, + @RequestBody(required = false) Map body) { + return this.operation.handle(exchange, body); + } + + } + + /** + * Handler for a {@link ReactiveWebOperation}. + */ + private final class ReadOperationHandler { + + private final ReactiveWebOperation operation; + + ReadOperationHandler(ReactiveWebOperation operation) { + this.operation = operation; + } + + @ResponseBody + public Publisher> handle(ServerWebExchange exchange) { + return this.operation.handle(exchange, null); + } + + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java index e33c25a7f4..8e33534749 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java @@ -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,33 +16,16 @@ package org.springframework.boot.actuate.endpoint.web.reactive; -import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - import org.springframework.beans.factory.InitializingBean; -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.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; @@ -54,158 +37,36 @@ import org.springframework.web.util.UriComponentsBuilder; * Spring WebFlux. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean { - private final Method handleRead = ReflectionUtils - .findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class); - - private final Method handleWrite = ReflectionUtils.findMethod( - WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class); - - private final Method links = ReflectionUtils.findMethod(getClass(), "links", - ServerHttpRequest.class); - - private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); - - /** - * 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 collection the web endpoints - * @param endpointMediaTypes media types consumed and produced by the endpoints - */ - public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection, - EndpointMediaTypes endpointMediaTypes) { - this(endpointMapping, collection, endpointMediaTypes, null); - } + private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); /** - * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the - * operations of the given {@code webEndpoints}. + * Creates a new {@code WebFluxEndpointHandlerMapping} instance that provides mappings + * for the given endpoints. * @param endpointMapping the base mapping for all endpoints - * @param webEndpoints the web endpoints + * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints - * @param corsConfiguration the CORS configuration for the endpoints + * @param corsConfiguration the CORS configuration for the endpoints or {@code null} */ public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { - super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration); + super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); setOrder(-100); } @Override - protected Method getLinks() { - return this.links; - } - - @Override - protected void registerMappingForOperation(WebOperation operation) { - OperationType operationType = operation.getType(); - OperationInvoker operationInvoker = operation.getInvoker(); - if (operation.isBlocking()) { - operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker); - } - registerMapping(createRequestMappingInfo(operation), - operationType == OperationType.WRITE - ? new WebFluxEndpointHandlerMapping.WriteOperationHandler( - operationInvoker) - : new WebFluxEndpointHandlerMapping.ReadOperationHandler( - operationInvoker), - operationType == OperationType.WRITE ? this.handleWrite - : this.handleRead); - } - @ResponseBody - private Map> links(ServerHttpRequest request) { + protected Map> links(ServerWebExchange exchange) { + String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) + .replaceQuery(null).toUriString(); return Collections.singletonMap("_links", - this.endpointLinksResolver.resolveLinks(getEndpoints(), - UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null) - .toUriString())); - } - - /** - * Base class for handlers for endpoint operations. - */ - abstract class AbstractOperationHandler { - - private final OperationInvoker operationInvoker; - - AbstractOperationHandler(OperationInvoker operationInvoker) { - this.operationInvoker = operationInvoker; - } - - @SuppressWarnings({ "unchecked" }) - Publisher> doHandle(ServerWebExchange exchange, - Map body) { - Map arguments = new HashMap<>((Map) 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 Publisher> 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 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) { - super(operationInvoker); - } - - @ResponseBody - public Publisher> handle(ServerWebExchange exchange, - @RequestBody(required = false) Map body) { - return doHandle(exchange, body); - } - - } - - /** - * A handler for an endpoint write operation. - */ - final class ReadOperationHandler extends AbstractOperationHandler { - - ReadOperationHandler(OperationInvoker operationInvoker) { - super(operationInvoker); - } - - @ResponseBody - public Publisher> handle(ServerWebExchange exchange) { - return doHandle(exchange, null); - } - + this.linksResolver.resolveLinks(getEndpoints(), requestUri)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index a608055697..e7dae1a292 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -17,21 +17,35 @@ package org.springframework.boot.actuate.endpoint.web.servlet; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.xml.ws.WebEndpoint; import org.springframework.beans.factory.InitializingBean; -import org.springframework.boot.actuate.endpoint.EndpointInfo; +import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; -import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; 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.util.StringUtils; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; @@ -43,11 +57,12 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; /** - * A custom {@link HandlerMapping} that makes web endpoints available over HTTP using - * Spring MVC. + * A custom {@link HandlerMapping} that makes {@link WebEndpoint web endpoints} available + * over HTTP using Spring MVC. * * @author Andy Wilkinson * @author Madhura Bhave + * @author Phillip Webb * @since 2.0.0 */ public abstract class AbstractWebMvcEndpointHandlerMapping @@ -55,104 +70,123 @@ public abstract class AbstractWebMvcEndpointHandlerMapping private final EndpointMapping endpointMapping; - private final Collection> webEndpoints; + private final Collection endpoints; private final EndpointMediaTypes endpointMediaTypes; private final CorsConfiguration corsConfiguration; + private final Method linksMethod = ReflectionUtils.findMethod(getClass(), "links", + HttpServletRequest.class, HttpServletResponse.class); + + private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, + "handle", HttpServletRequest.class, Map.class); + /** * 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 collection the web endpoints operations + * @param endpoints the web endpoints operations * @param endpointMediaTypes media types consumed and produced by the endpoints */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection, + Collection endpoints, EndpointMediaTypes endpointMediaTypes) { - this(endpointMapping, collection, endpointMediaTypes, null); + this(endpointMapping, endpoints, endpointMediaTypes, null); } /** - * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the - * operations of the given {@code webEndpoints}. + * Creates a new {@code AbstractWebMvcEndpointHandlerMapping} that provides mappings + * for the operations of the given endpoints. * @param endpointMapping the base mapping for all endpoints - * @param webEndpoints the web endpoints + * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints - * @param corsConfiguration the CORS configuration for the endpoints + * @param corsConfiguration the CORS configuration for the endpoints or {@code null} */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { this.endpointMapping = endpointMapping; - this.webEndpoints = webEndpoints; + this.endpoints = endpoints; this.endpointMediaTypes = endpointMediaTypes; this.corsConfiguration = corsConfiguration; setOrder(-100); } - public Collection> getEndpoints() { - return this.webEndpoints; - } - - public EndpointMapping getEndpointMapping() { - return this.endpointMapping; - } - @Override protected void initHandlerMethods() { - this.webEndpoints.stream() - .flatMap((webEndpoint) -> webEndpoint.getOperations().stream()) - .forEach(this::registerMappingForOperation); + for (ExposableWebEndpoint endpoint : this.endpoints) { + for (WebOperation operation : endpoint.getOperations()) { + registerMappingForOperation(endpoint, operation); + } + } if (StringUtils.hasText(this.endpointMapping.getPath())) { - registerLinksRequestMapping(); + registerLinksMapping(); } } - private void registerLinksRequestMapping() { - PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.GET); - ProducesRequestCondition produces = new ProducesRequestCondition( - this.endpointMediaTypes.getProduced().toArray( - new String[this.endpointMediaTypes.getProduced().size()])); - RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, - null, produces, null); - registerMapping(mapping, this, getLinks()); - } - - @Override - protected CorsConfiguration initCorsConfiguration(Object handler, Method method, - RequestMappingInfo mapping) { - return this.corsConfiguration; + private void registerMappingForOperation(ExposableWebEndpoint endpoint, + WebOperation operation) { + OperationInvoker invoker = operation::invoke; + ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, + operation, new ServletWebOperationAdapter(invoker)); + registerMapping(createRequestMappingInfo(operation), + new OperationHandler(servletWebOperation), this.handleMethod); } - protected abstract Method getLinks(); + /** + * Hook point that allows subclasses to wrap the {@link ServletWebOperation} before + * it's called. Allows additional features, such as security, to be added. + * @param endpoint the source endpoint + * @param operation the source operation + * @param servletWebOperation the servlet web operation to wrap + * @return a wrapped servlet web operation + */ - protected abstract void registerMappingForOperation(WebOperation operation); + protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint, + WebOperation operation, ServletWebOperation servletWebOperation) { + return servletWebOperation; + } - protected RequestMappingInfo createRequestMappingInfo(WebOperation operationInfo) { - OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate(); + private RequestMappingInfo createRequestMappingInfo(WebOperation operation) { + WebOperationRequestPredicate predicate = operation.getRequestPredicate(); PatternsRequestCondition patterns = patternsRequestConditionForPattern( - requestPredicate.getPath()); + predicate.getPath()); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.valueOf(requestPredicate.getHttpMethod().name())); + RequestMethod.valueOf(predicate.getHttpMethod().name())); ConsumesRequestCondition consumes = new ConsumesRequestCondition( - toStringArray(requestPredicate.getConsumes())); + toStringArray(predicate.getConsumes())); ProducesRequestCondition produces = new ProducesRequestCondition( - toStringArray(requestPredicate.getProduces())); + toStringArray(predicate.getProduces())); return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null); } + private String[] toStringArray(Collection collection) { + return collection.toArray(new String[collection.size()]); + } + + private void registerLinksMapping() { + PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); + RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( + RequestMethod.GET); + ProducesRequestCondition produces = new ProducesRequestCondition( + this.endpointMediaTypes.getProduced().toArray( + new String[this.endpointMediaTypes.getProduced().size()])); + RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, + null, produces, null); + registerMapping(mapping, this, this.linksMethod); + } + private PatternsRequestCondition patternsRequestConditionForPattern(String path) { String[] patterns = new String[] { this.endpointMapping.createSubPath(path) }; return new PatternsRequestCondition(patterns, null, null, false, true); } - private String[] toStringArray(Collection collection) { - return collection.toArray(new String[collection.size()]); + @Override + protected CorsConfiguration initCorsConfiguration(Object handler, Method method, + RequestMappingInfo mapping) { + return this.corsConfiguration; } @Override @@ -171,6 +205,103 @@ public abstract class AbstractWebMvcEndpointHandlerMapping interceptors.add(new SkipPathExtensionContentNegotiation()); } + protected abstract Object links(HttpServletRequest request, + HttpServletResponse response); + + /** + * Return the web endpoints being mapped. + * @return the endpoints + */ + public Collection getEndpoints() { + return this.endpoints; + } + + /** + * An reactive web operation that can be handled by WebFlux. + */ + protected interface ServletWebOperation { + + Object handle(HttpServletRequest request, Map body); + + } + + /** + * Adapter class to convert an {@link OperationInvoker} into a + * {@link ServletWebOperation}. + */ + private class ServletWebOperationAdapter implements ServletWebOperation { + + private final OperationInvoker invoker; + + ServletWebOperationAdapter(OperationInvoker invoker) { + this.invoker = invoker; + } + + @Override + public Object handle(HttpServletRequest request, + @RequestBody(required = false) Map body) { + Map arguments = getArguments(request, body); + try { + return handleResult(this.invoker.invoke(arguments), + HttpMethod.valueOf(request.getMethod())); + } + catch (MissingParametersException | ParameterMappingException ex) { + return new ResponseEntity(HttpStatus.BAD_REQUEST); + } + } + + private Map getArguments(HttpServletRequest request, + Map body) { + Map arguments = new LinkedHashMap<>(); + arguments.putAll(getTemplateVariables(request)); + if (body != null && HttpMethod.POST.name().equals(request.getMethod())) { + arguments.putAll(body); + } + request.getParameterMap().forEach((name, values) -> arguments.put(name, + values.length == 1 ? values[0] : Arrays.asList(values))); + return arguments; + } + + @SuppressWarnings("unchecked") + private Map getTemplateVariables(HttpServletRequest request) { + return (Map) request + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + } + + 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(response.getBody(), + HttpStatus.valueOf(response.getStatus())); + } + + } + + /** + * Handler for a {@link ServletWebOperation}. + */ + private final class OperationHandler { + + private final ServletWebOperation operation; + + OperationHandler(ServletWebOperation operation) { + this.operation = operation; + } + + @ResponseBody + public Object handle(HttpServletRequest request, + @RequestBody(required = false) Map body) { + return this.operation.handle(request, body); + } + + } + /** * {@link HandlerInterceptorAdapter} to ensure that * {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java index 46b6c61055..4b5e0fc46b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java @@ -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,30 +16,18 @@ package org.springframework.boot.actuate.endpoint.web.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.Map; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; -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.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; @@ -49,107 +37,35 @@ import org.springframework.web.servlet.HandlerMapping; * Spring MVC. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { - private final Method handle = ReflectionUtils.findMethod(OperationHandler.class, - "handle", HttpServletRequest.class, Map.class); - - private final Method links = ReflectionUtils.findMethod( - WebMvcEndpointHandlerMapping.class, "links", HttpServletRequest.class); - - private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); - - /** - * 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 collection the web endpoints operations - * @param endpointMediaTypes media types consumed and produced by the endpoints - */ - public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection, - EndpointMediaTypes endpointMediaTypes) { - this(endpointMapping, collection, endpointMediaTypes, null); - } + private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); /** - * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the - * operations of the given {@code webEndpoints}. + * Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings + * for the given endpoints. * @param endpointMapping the base mapping for all endpoints - * @param webEndpoints the web endpoints + * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints - * @param corsConfiguration the CORS configuration for the endpoints + * @param corsConfiguration the CORS configuration for the endpoints or {@code null} */ public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> webEndpoints, + Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { - super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration); + super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); setOrder(-100); } @Override - protected void registerMappingForOperation(WebOperation operation) { - registerMapping(createRequestMappingInfo(operation), - new OperationHandler(operation.getInvoker()), this.handle); - } - - @Override - protected Method getLinks() { - return this.links; - } - @ResponseBody - private Map> links(HttpServletRequest request) { - return Collections.singletonMap("_links", this.endpointLinksResolver - .resolveLinks(getEndpoints(), request.getRequestURL().toString())); - } - - /** - * A handler for an endpoint operation. - */ - final class OperationHandler { - - private final OperationInvoker operationInvoker; - - OperationHandler(OperationInvoker operationInvoker) { - this.operationInvoker = operationInvoker; - } - - @SuppressWarnings("unchecked") - @ResponseBody - public Object handle(HttpServletRequest request, - @RequestBody(required = false) Map body) { - Map arguments = new HashMap<>((Map) 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(HttpStatus.BAD_REQUEST); - } - } - - 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(response.getBody(), - HttpStatus.valueOf(response.getStatus())); - } - + protected Map> links(HttpServletRequest request, + HttpServletResponse response) { + String requestUri = request.getRequestURL().toString(); + return Collections.singletonMap("_links", + this.linksResolver.resolveLinks(getEndpoints(), requestUri)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index 5d7f7b4291..ce157a3404 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -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,6 +18,7 @@ package org.springframework.boot.actuate.health; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.util.Assert; /** * {@link Endpoint} to expose application health information. @@ -37,6 +38,7 @@ public class HealthEndpoint { * @param healthIndicator the health indicator */ public HealthEndpoint(HealthIndicator healthIndicator) { + Assert.notNull(healthIndicator, "HealthIndicator must not be null"); this.healthIndicator = healthIndicator; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/NamePatternFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/NamePatternFilterTests.java deleted file mode 100644 index 3e7a32660b..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/NamePatternFilterTests.java +++ /dev/null @@ -1,126 +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.Map; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link NamePatternFilter}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Dylian Bego - */ -public class NamePatternFilterTests { - - @Test - public void nonRegex() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - assertThat(filter.getResults("not.a.regex")).containsEntry("not.a.regex", - "not.a.regex"); - assertThat(filter.isGetNamesCalled()).isFalse(); - } - - @Test - public void nonRegexThatContainsRegexPart() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - assertThat(filter.getResults("*")).containsEntry("*", "*"); - assertThat(filter.isGetNamesCalled()).isFalse(); - } - - @Test - public void regexRepetitionZeroOrMore() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - Map results = filter.getResults("fo.*"); - assertThat(results.get("foo")).isEqualTo("foo"); - assertThat(results.get("fool")).isEqualTo("fool"); - assertThat(filter.isGetNamesCalled()).isTrue(); - } - - @Test - public void regexRepetitionOneOrMore() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - Map results = filter.getResults("fo.+"); - assertThat(results.get("foo")).isEqualTo("foo"); - assertThat(results.get("fool")).isEqualTo("fool"); - assertThat(filter.isGetNamesCalled()).isTrue(); - } - - @Test - public void regexEndAnchor() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - Map results = filter.getResults("foo$"); - assertThat(results.get("foo")).isEqualTo("foo"); - assertThat(results.get("fool")).isNull(); - assertThat(filter.isGetNamesCalled()).isTrue(); - } - - @Test - public void regexStartAnchor() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - Map results = filter.getResults("^foo"); - assertThat(results.get("foo")).isEqualTo("foo"); - assertThat(results.get("fool")).isNull(); - assertThat(filter.isGetNamesCalled()).isTrue(); - } - - @Test - public void regexCharacterClass() { - MockNamePatternFilter filter = new MockNamePatternFilter(); - Map results = filter.getResults("fo[a-z]l"); - assertThat(results.get("foo")).isNull(); - assertThat(results.get("fool")).isEqualTo("fool"); - assertThat(filter.isGetNamesCalled()).isTrue(); - } - - private static class MockNamePatternFilter extends NamePatternFilter { - - MockNamePatternFilter() { - super(null); - } - - private boolean getNamesCalled; - - @Override - protected Object getOptionalValue(Object source, String name) { - return name; - } - - @Override - protected Object getValue(Object source, String name) { - return name; - } - - @Override - protected void getNames(Object source, NameCallback callback) { - this.getNamesCalled = true; - callback.addName("foo"); - callback.addName("fool"); - callback.addName("fume"); - } - - public boolean isGetNamesCalled() { - return this.getNamesCalled; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscovererTests.java deleted file mode 100644 index f835cce164..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/AnnotationEndpointDiscovererTests.java +++ /dev/null @@ -1,497 +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 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 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.function.Consumer; -import java.util.function.Function; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -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.OperationInvoker; -import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker; -import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvokerAdvisor; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; -import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInfo; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.AliasFor; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link AnnotationEndpointDiscoverer}. - * - * @author Andy Wilkinson - * @author Stephane Nicoll - */ -public class AnnotationEndpointDiscovererTests { - - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Test - public void discoverWorksWhenThereAreNoEndpoints() { - load(EmptyConfiguration.class, - (context) -> assertThat(new TestAnnotationEndpointDiscoverer(context) - .discoverEndpoints().isEmpty())); - } - - @Test - public void endpointIsDiscovered() { - load(TestEndpointConfiguration.class, hasTestEndpoint()); - } - - @Test - public void endpointInParentContextIsDiscovered() { - AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext( - TestEndpointConfiguration.class); - loadWithParent(parent, EmptyConfiguration.class, hasTestEndpoint()); - } - - private Consumer hasTestEndpoint() { - return (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context).discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - Map operations = mapOperations( - endpoints.get("test")); - assertThat(operations).hasSize(4); - assertThat(operations).containsKeys( - ReflectionUtils.findMethod(TestEndpoint.class, "getAll"), - ReflectionUtils.findMethod(TestEndpoint.class, "getOne", - String.class), - ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class, - String.class), - ReflectionUtils.findMethod(TestEndpoint.class, "deleteOne", - String.class)); - }; - } - - @Test - public void subclassedEndpointIsDiscovered() { - load(TestEndpointSubclassConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context).discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - Map operations = mapOperations( - endpoints.get("test")); - assertThat(operations).hasSize(5); - assertThat(operations).containsKeys( - ReflectionUtils.findMethod(TestEndpoint.class, "getAll"), - ReflectionUtils.findMethod(TestEndpoint.class, "getOne", - String.class), - ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class, - String.class), - ReflectionUtils.findMethod(TestEndpoint.class, "deleteOne", - String.class), - ReflectionUtils.findMethod(TestEndpointSubclass.class, - "updateWithMoreArguments", String.class, String.class, - String.class)); - }); - } - - @Test - public void discoveryFailsWhenTwoEndpointsHaveTheSameId() { - load(ClashingEndpointConfiguration.class, (context) -> { - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Found two endpoints with the id 'test': "); - new TestAnnotationEndpointDiscoverer(context).discoverEndpoints(); - }); - } - - @Test - public void endpointMainReadOperationIsNotCachedWithTtlSetToZero() { - Function timeToLive = (endpointId) -> 0L; - load(TestEndpointConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context, timeToLive) - .discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - Map operations = mapOperations( - endpoints.get("test")); - assertThat(operations).hasSize(4); - operations.values().forEach(operation -> assertThat(operation.getInvoker()) - .isNotInstanceOf(CachingOperationInvoker.class)); - }); - } - - @Test - public void endpointMainReadOperationIsNotCachedWithNonMatchingId() { - Function timeToLive = (id) -> (id.equals("foo") ? 500L : 0L); - load(TestEndpointConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context, timeToLive) - .discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - Map operations = mapOperations( - endpoints.get("test")); - assertThat(operations).hasSize(4); - operations.values().forEach(operation -> assertThat(operation.getInvoker()) - .isNotInstanceOf(CachingOperationInvoker.class)); - }); - } - - @Test - public void endpointMainReadOperationIsCachedWithMatchingId() { - Function timeToLive = (id) -> (id.equals("test") ? 500L : 0L); - load(TestEndpointConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context, timeToLive) - .discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - Map operations = mapOperations( - endpoints.get("test")); - OperationInvoker getAllOperationInvoker = operations - .get(ReflectionUtils.findMethod(TestEndpoint.class, "getAll")) - .getInvoker(); - assertThat(getAllOperationInvoker) - .isInstanceOf(CachingOperationInvoker.class); - assertThat(((CachingOperationInvoker) getAllOperationInvoker).getTimeToLive()) - .isEqualTo(500); - assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class, - "getOne", String.class)).getInvoker()) - .isNotInstanceOf(CachingOperationInvoker.class); - assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class, - "update", String.class, String.class)).getInvoker()) - .isNotInstanceOf(CachingOperationInvoker.class); - }); - } - - @Test - public void specializedEndpointsAreFilteredFromRegular() { - load(TestEndpointsConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new TestAnnotationEndpointDiscoverer(context).discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - }); - } - - @Test - public void specializedEndpointsAreNotFilteredFromSpecialized() { - load(TestEndpointsConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new SpecializedTestAnnotationEndpointDiscoverer(context) - .discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test", "specialized"); - }); - } - - @Test - public void extensionsAreApplied() { - load(TestEndpointsConfiguration.class, (context) -> { - Map> endpoints = mapEndpoints( - new SpecializedTestAnnotationEndpointDiscoverer(context) - .discoverEndpoints()); - Map operations = mapOperations( - endpoints.get("specialized")); - assertThat(operations).containsKeys( - ReflectionUtils.findMethod(SpecializedExtension.class, "getSpecial")); - }); - } - - @Test - public void filtersAreApplied() { - load(TestEndpointsConfiguration.class, (context) -> { - EndpointFilter filter = (info, - discoverer) -> !(info.getId().equals("specialized")); - Map> endpoints = mapEndpoints( - new SpecializedTestAnnotationEndpointDiscoverer(context, - Collections.singleton(filter)).discoverEndpoints()); - assertThat(endpoints).containsOnlyKeys("test"); - }); - } - - private Map> mapEndpoints( - Collection> endpoints) { - Map> endpointById = new LinkedHashMap<>(); - endpoints.forEach((endpoint) -> { - EndpointInfo existing = endpointById.put(endpoint.getId(), endpoint); - if (existing != null) { - throw new AssertionError(String.format( - "Found endpoints with duplicate id '%s'", endpoint.getId())); - } - }); - return endpointById; - } - - private Map mapOperations( - EndpointInfo endpoint) { - Map operationByMethod = new HashMap<>(); - endpoint.getOperations().forEach((operation) -> { - Method method = operation.getMethodInfo().getMethod(); - Operation existing = operationByMethod.put(method, operation); - if (existing != null) { - throw new AssertionError(String.format( - "Found endpoint with duplicate operation method '%s'", method)); - } - }); - return operationByMethod; - } - - private void load(Class configuration, - Consumer consumer) { - doLoad(null, configuration, consumer); - } - - private void loadWithParent(ApplicationContext parent, Class configuration, - Consumer consumer) { - doLoad(parent, configuration, consumer); - } - - private void doLoad(ApplicationContext parent, Class configuration, - Consumer consumer) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - if (parent != null) { - context.setParent(parent); - } - context.register(configuration); - context.refresh(); - try { - consumer.accept(context); - } - finally { - context.close(); - } - } - - @Configuration - static class EmptyConfiguration { - - } - - @Endpoint(id = "test") - static class TestEndpoint { - - @ReadOperation - public Object getAll() { - return null; - } - - @ReadOperation - public Object getOne(@Selector String id) { - return null; - } - - @WriteOperation - public void update(String foo, String bar) { - - } - - @DeleteOperation - public void deleteOne(@Selector String id) { - - } - - public void someOtherMethod() { - - } - - } - - @SpecializedEndpoint(id = "specialized") - static class SpecializedTestEndpoint { - - @ReadOperation - public Object getAll() { - return null; - } - - } - - static class TestEndpointSubclass extends TestEndpoint { - - @WriteOperation - public void updateWithMoreArguments(String foo, String bar, String baz) { - - } - - } - - @Configuration - static class TestEndpointConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - } - - @Configuration - static class TestEndpointSubclassConfiguration { - - @Bean - public TestEndpointSubclass testEndpointSubclass() { - return new TestEndpointSubclass(); - } - - } - - @Import({ TestEndpoint.class, SpecializedTestEndpoint.class, - SpecializedExtension.class }) - static class TestEndpointsConfiguration { - - } - - @Configuration - static class ClashingEndpointConfiguration { - - @Bean - public TestEndpoint testEndpointTwo() { - return new TestEndpoint(); - } - - @Bean - public TestEndpoint testEndpointOne() { - return new TestEndpoint(); - } - - } - - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Endpoint - @FilteredEndpoint(SpecializedEndpointFilter.class) - public @interface SpecializedEndpoint { - - @AliasFor(annotation = Endpoint.class) - String id(); - - } - - @EndpointExtension(endpoint = SpecializedTestEndpoint.class, filter = SpecializedEndpointFilter.class) - public static class SpecializedExtension { - - @ReadOperation - public Object getSpecial() { - return null; - } - - } - - static class SpecializedEndpointFilter - implements EndpointFilter { - - @Override - public boolean match(EndpointInfo info, - EndpointDiscoverer discoverer) { - return discoverer instanceof SpecializedTestAnnotationEndpointDiscoverer; - } - - } - - public static class TestAnnotationEndpointDiscoverer - extends AnnotationEndpointDiscoverer { - - TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext) { - this(applicationContext, (id) -> null, null); - } - - TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext, - Function timeToLive) { - this(applicationContext, timeToLive, null); - } - - TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext, - Function timeToLive, - Collection> filters) { - super(applicationContext, TestEndpointOperation::new, - TestEndpointOperation::getMethod, - new ConversionServiceParameterMapper(), - Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), - filters); - } - - } - - public static class SpecializedTestAnnotationEndpointDiscoverer extends - AnnotationEndpointDiscoverer { - - SpecializedTestAnnotationEndpointDiscoverer( - ApplicationContext applicationContext) { - this(applicationContext, (id) -> null, null); - } - - SpecializedTestAnnotationEndpointDiscoverer(ApplicationContext applicationContext, - Collection> filters) { - this(applicationContext, (id) -> null, filters); - } - - SpecializedTestAnnotationEndpointDiscoverer(ApplicationContext applicationContext, - Function timeToLive, - Collection> filters) { - super(applicationContext, SpecializedTestEndpointOperation::new, - SpecializedTestEndpointOperation::getMethod, - new ConversionServiceParameterMapper(), - Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), - filters); - } - - } - - public static class TestEndpointOperation extends Operation { - - private final OperationMethodInfo methodInfo; - - public TestEndpointOperation(String endpointId, OperationMethodInfo methodInfo, - Object target, OperationInvoker invoker) { - super(methodInfo.getOperationType(), invoker, true); - this.methodInfo = methodInfo; - } - - public Method getMethod() { - return this.methodInfo.getMethod(); - } - - public OperationMethodInfo getMethodInfo() { - return this.methodInfo; - } - - } - - public static class SpecializedTestEndpointOperation extends TestEndpointOperation { - - public SpecializedTestEndpointOperation(String endpointId, - OperationMethodInfo methodInfo, Object target, OperationInvoker invoker) { - super(endpointId, methodInfo, target, invoker); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java new file mode 100644 index 0000000000..bb28a9a59e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java @@ -0,0 +1,64 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DiscoveredOperationMethod}. + * + * @author Phillip Webb + */ +public class DiscoveredOperationMethodTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenAnnotationAttributesIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("AnnotationAttributes must not be null"); + Method method = ReflectionUtils.findMethod(getClass(), "example"); + new DiscoveredOperationMethod(method, OperationType.READ, null); + } + + @Test + public void getProducesMediaTypesShouldReturnMediaTypes() { + Method method = ReflectionUtils.findMethod(getClass(), "example"); + AnnotationAttributes annotationAttributes = new AnnotationAttributes(); + String[] produces = new String[] { "application/json" }; + annotationAttributes.put("produces", produces); + DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, + OperationType.READ, annotationAttributes); + assertThat(discovered.getProducesMediaTypes()) + .containsExactly("application/json"); + } + + public void example() { + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java new file mode 100644 index 0000000000..20b571fcbb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java @@ -0,0 +1,234 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; +import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; +import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DiscoveredOperationsFactory}. + * + * @author Phillip Webb + */ +public class DiscoveredOperationsFactoryTests { + + private TestDiscoveredOperationsFactory factory; + + private ParameterValueMapper parameterValueMapper; + + private List invokerAdvisors; + + @Before + public void setup() { + this.parameterValueMapper = (parameter, value) -> value.toString(); + this.invokerAdvisors = new ArrayList<>(); + this.factory = new TestDiscoveredOperationsFactory(this.parameterValueMapper, + this.invokerAdvisors); + } + + @Test + public void createOperationsWhenHasReadMethodShouldCreateOperation() { + Collection operations = this.factory.createOperations("test", + new ExampleRead()); + assertThat(operations).hasSize(1); + TestOperation operation = getFirst(operations); + assertThat(operation.getType()).isEqualTo(OperationType.READ); + } + + @Test + public void createOperationsWhenHasWriteMethodShouldCreateOperation() { + Collection operations = this.factory.createOperations("test", + new ExampleWrite()); + assertThat(operations).hasSize(1); + TestOperation operation = getFirst(operations); + assertThat(operation.getType()).isEqualTo(OperationType.WRITE); + } + + @Test + public void createOperationsWhenHasDeleteMethodShouldCreateOperation() { + Collection operations = this.factory.createOperations("test", + new ExampleDelete()); + assertThat(operations).hasSize(1); + TestOperation operation = getFirst(operations); + assertThat(operation.getType()).isEqualTo(OperationType.DELETE); + } + + @Test + public void createOperationsWhenMultipleShouldReturnMultiple() { + Collection operations = this.factory.createOperations("test", + new ExampleMultiple()); + assertThat(operations).hasSize(2); + assertThat(operations.stream().map(TestOperation::getType)) + .containsOnly(OperationType.READ, OperationType.WRITE); + } + + @Test + public void createOperationsShouldProvideOperationMethod() { + TestOperation operation = getFirst( + this.factory.createOperations("test", new ExampleWithParams())); + OperationMethod operationMethod = operation.getOperationMethod(); + assertThat(operationMethod.getMethod().getName()).isEqualTo("read"); + assertThat(operationMethod.getParameters().hasParameters()).isTrue(); + } + + @Test + public void createOperationsShouldProviderInvoker() { + TestOperation operation = getFirst( + this.factory.createOperations("test", new ExampleWithParams())); + Map params = Collections.singletonMap("name", 123); + Object result = operation.invoke(params); + assertThat(result).isEqualTo("123"); + } + + @Test + public void createOperationShouldApplyAdvisors() { + TestOperationInvokerAdvisor advisor = new TestOperationInvokerAdvisor(); + this.invokerAdvisors.add(advisor); + TestOperation operation = getFirst( + this.factory.createOperations("test", new ExampleRead())); + operation.invoke(Collections.emptyMap()); + assertThat(advisor.getEndpointId()).isEqualTo("test"); + assertThat(advisor.getOperationType()).isEqualTo(OperationType.READ); + assertThat(advisor.getParameters()).isEmpty(); + } + + private T getFirst(Iterable iterable) { + return iterable.iterator().next(); + } + + static class ExampleRead { + + @ReadOperation + public String read() { + return "read"; + } + + } + + static class ExampleWrite { + + @WriteOperation + public String write() { + return "write"; + } + + } + + static class ExampleDelete { + + @DeleteOperation + public String delete() { + return "delete"; + } + + } + + static class ExampleMultiple { + + @ReadOperation + public String read() { + return "read"; + } + + @WriteOperation + public String write() { + return "write"; + } + + } + + static class ExampleWithParams { + + @ReadOperation + public String read(String name) { + return name; + } + + } + + static class TestDiscoveredOperationsFactory + extends DiscoveredOperationsFactory { + + TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors) { + super(parameterValueMapper, invokerAdvisors); + } + + @Override + protected TestOperation createOperation(String endpointId, + DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { + return new TestOperation(endpointId, operationMethod, invoker); + } + + } + + static class TestOperation extends AbstractDiscoveredOperation { + + TestOperation(String endpointId, DiscoveredOperationMethod operationMethod, + OperationInvoker invoker) { + super(operationMethod, invoker); + } + + } + + static class TestOperationInvokerAdvisor implements OperationInvokerAdvisor { + + private String endpointId; + + private OperationType operationType; + + private OperationParameters parameters; + + @Override + public OperationInvoker apply(String endpointId, OperationType operationType, + OperationParameters parameters, OperationInvoker invoker) { + this.endpointId = endpointId; + this.operationType = operationType; + this.parameters = parameters; + return invoker; + } + + public String getEndpointId() { + return this.endpointId; + } + + public OperationType getOperationType() { + return this.operationType; + } + + public OperationParameters getParameters() { + return this.parameters; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilterTests.java new file mode 100644 index 0000000000..eb2ce70bee --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscovererEndpointFilterTests.java @@ -0,0 +1,109 @@ +/* + * 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.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.EndpointFilter; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.Operation; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; +import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DiscovererEndpointFilter}. + * + * @author Phillip Webb + */ +public class DiscovererEndpointFilterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenDiscovererIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Discoverer must not be null"); + new TestDiscovererEndpointFilter(null); + } + + @Test + public void matchWhenDiscoveredByDiscovererShouldReturnTrue() { + DiscovererEndpointFilter filter = new TestDiscovererEndpointFilter( + TestDiscovererA.class); + DiscoveredEndpoint endpoint = mockDiscoveredEndpoint(TestDiscovererA.class); + assertThat(filter.match(endpoint)).isTrue(); + } + + @Test + public void matchWhenNotDiscoveredByDiscovererShouldReturnFalse() { + DiscovererEndpointFilter filter = new TestDiscovererEndpointFilter( + TestDiscovererA.class); + DiscoveredEndpoint endpoint = mockDiscoveredEndpoint(TestDiscovererB.class); + assertThat(filter.match(endpoint)).isFalse(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private DiscoveredEndpoint mockDiscoveredEndpoint(Class discoverer) { + DiscoveredEndpoint endpoint = mock(DiscoveredEndpoint.class); + given(endpoint.wasDiscoveredBy(discoverer)).willReturn(true); + return endpoint; + } + + static class TestDiscovererEndpointFilter extends DiscovererEndpointFilter { + + TestDiscovererEndpointFilter( + Class> discoverer) { + super(discoverer); + } + + } + + abstract static class TestDiscovererA + extends EndpointDiscoverer, Operation> { + + TestDiscovererA(ApplicationContext applicationContext, + ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors, + Collection>> filters) { + super(applicationContext, parameterValueMapper, invokerAdvisors, filters); + } + + } + + abstract static class TestDiscovererB + extends EndpointDiscoverer, Operation> { + + TestDiscovererB(ApplicationContext applicationContext, + ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors, + Collection>> filters) { + super(applicationContext, parameterValueMapper, invokerAdvisors, filters); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java new file mode 100644 index 0000000000..d790e83d94 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java @@ -0,0 +1,579 @@ +/* + * 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.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.EndpointFilter; +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.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvoker; +import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EndpointDiscoverer}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class EndpointDiscovererTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenApplicationContextIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ApplicationContext must not be null"); + new TestEndpointDiscoverer(null, mock(ParameterValueMapper.class), + Collections.emptyList(), Collections.emptyList()); + } + + @Test + public void createWhenParameterValueMapperIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ParameterValueMapper must not be null"); + new TestEndpointDiscoverer(mock(ApplicationContext.class), null, + Collections.emptyList(), Collections.emptyList()); + } + + @Test + public void createWhenInvokerAdvisorsIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("InvokerAdvisors must not be null"); + new TestEndpointDiscoverer(mock(ApplicationContext.class), + mock(ParameterValueMapper.class), null, Collections.emptyList()); + } + + @Test + public void createWhenFiltersIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Filters must not be null"); + new TestEndpointDiscoverer(mock(ApplicationContext.class), + mock(ParameterValueMapper.class), Collections.emptyList(), null); + } + + @Test + public void getEndpointsWhenNoEndpointBeansShouldReturnEmptyCollection() { + load(EmptyConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context); + Collection endpoints = discoverer.getEndpoints(); + assertThat(endpoints).isEmpty(); + }); + } + + @Test + public void getEndpointsWhenHasEndpointShouldReturnEndpoint() { + load(TestEndpointConfiguration.class, this::hasTestEndpoint); + } + + @Test + public void getEndpointsWhenHasEndpointInParentContextShouldReturnEndpoint() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext( + TestEndpointConfiguration.class); + loadWithParent(parent, EmptyConfiguration.class, this::hasTestEndpoint); + } + + @Test + public void getEndpointsWhenHasSubclassedEndpointShouldReturnEndpoint() { + load(TestEndpointSubclassConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + Map operations = mapOperations(endpoints.get("test")); + assertThat(operations).hasSize(5); + assertThat(operations).containsKeys(testEndpointMethods()); + assertThat(operations).containsKeys(ReflectionUtils.findMethod( + TestEndpointSubclass.class, "updateWithMoreArguments", String.class, + String.class, String.class)); + }); + } + + @Test + public void getEndpointsWhenTwoEndpointsHaveTheSameIdShouldThrowException() { + load(ClashingEndpointConfiguration.class, (context) -> { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Found two endpoints with the id 'test': "); + new TestEndpointDiscoverer(context).getEndpoints(); + }); + } + + @Test + public void getEndpointsWhenTtlSetToZeroShouldNotCacheInvokeCalls() { + load(TestEndpointConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context, + (endpointId) -> 0L); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + Map operations = mapOperations(endpoints.get("test")); + operations.values().forEach((operation) -> assertThat(operation.getInvoker()) + .isNotInstanceOf(CachingOperationInvoker.class)); + }); + } + + @Test + public void getEndpointsWhenTtlSetByIdAndIdDoesntMatchShouldNotCacheInvokeCalls() { + load(TestEndpointConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context, + (endpointId) -> (endpointId.equals("foo") ? 500L : 0L)); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + Map operations = mapOperations(endpoints.get("test")); + operations.values().forEach((operation) -> assertThat(operation.getInvoker()) + .isNotInstanceOf(CachingOperationInvoker.class)); + }); + } + + @Test + public void getEndpointsWhenTtlSetByIdAndIdMatchesShouldCacheInvokeCalls() { + load(TestEndpointConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context, + (endpointId) -> (endpointId.equals("test") ? 500L : 0L)); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + Map operations = mapOperations(endpoints.get("test")); + TestOperation getAll = operations.get(findTestEndpointMethod("getAll")); + TestOperation getOne = operations + .get(findTestEndpointMethod("getOne", String.class)); + TestOperation update = operations.get(ReflectionUtils.findMethod( + TestEndpoint.class, "update", String.class, String.class)); + assertThat(((CachingOperationInvoker) getAll.getInvoker()).getTimeToLive()) + .isEqualTo(500); + assertThat(getOne.getInvoker()) + .isNotInstanceOf(CachingOperationInvoker.class); + assertThat(update.getInvoker()) + .isNotInstanceOf(CachingOperationInvoker.class); + }); + } + + @Test + public void getEndpointsWhenHasSpecializedFiltersInNonSpecializedDiscovererShouldFilterEndpoints() { + load(SpecializedEndpointsConfiguration.class, (context) -> { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + }); + } + + @Test + public void getEndpointsWhenHasSpecializedFiltersInSpecializedDiscovererShouldNotFilterEndpoints() { + load(SpecializedEndpointsConfiguration.class, (context) -> { + SpecializedEndpointDiscoverer discoverer = new SpecializedEndpointDiscoverer( + context); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test", "specialized"); + }); + } + + @Test + public void getEndpointsShouldApplyExtensions() { + load(SpecializedEndpointsConfiguration.class, (context) -> { + SpecializedEndpointDiscoverer discoverer = new SpecializedEndpointDiscoverer( + context); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + Map operations = mapOperations( + endpoints.get("specialized")); + assertThat(operations).containsKeys( + ReflectionUtils.findMethod(SpecializedExtension.class, "getSpecial")); + + }); + } + + @Test + public void getEndpointsShouldApplyFilters() { + load(SpecializedEndpointsConfiguration.class, (context) -> { + EndpointFilter filter = (endpoint) -> { + String id = endpoint.getId(); + return !id.equals("specialized"); + }; + SpecializedEndpointDiscoverer discoverer = new SpecializedEndpointDiscoverer( + context, Collections.singleton(filter)); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + }); + } + + private void hasTestEndpoint(AnnotationConfigApplicationContext context) { + TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); + assertThat(endpoints).containsOnlyKeys("test"); + Map operations = mapOperations(endpoints.get("test")); + assertThat(operations).hasSize(4); + assertThat(operations).containsKeys(); + } + + private Method[] testEndpointMethods() { + List methods = new ArrayList<>(); + methods.add(findTestEndpointMethod("getAll")); + methods.add(findTestEndpointMethod("getOne", String.class)); + methods.add(findTestEndpointMethod("update", String.class, String.class)); + methods.add(findTestEndpointMethod("deleteOne", String.class)); + return methods.toArray(new Method[] {}); + } + + private Method findTestEndpointMethod(String name, Class... paramTypes) { + return ReflectionUtils.findMethod(TestEndpoint.class, name, paramTypes); + } + + private > Map mapEndpoints( + Collection endpoints) { + Map byId = new LinkedHashMap<>(); + endpoints.forEach((endpoint) -> { + E existing = byId.put(endpoint.getId(), endpoint); + if (existing != null) { + throw new AssertionError(String.format( + "Found endpoints with duplicate id '%s'", endpoint.getId())); + } + }); + return byId; + } + + private Map mapOperations( + ExposableEndpoint endpoint) { + Map byMethod = new HashMap<>(); + endpoint.getOperations().forEach((operation) -> { + AbstractDiscoveredOperation discoveredOperation = (AbstractDiscoveredOperation) operation; + Method method = discoveredOperation.getOperationMethod().getMethod(); + O existing = byMethod.put(method, operation); + if (existing != null) { + throw new AssertionError(String.format( + "Found endpoint with duplicate operation method '%s'", method)); + } + }); + return byMethod; + } + + private void load(Class configuration, + Consumer consumer) { + load(null, configuration, consumer); + } + + private void loadWithParent(ApplicationContext parent, Class configuration, + Consumer consumer) { + load(parent, configuration, consumer); + } + + private void load(ApplicationContext parent, Class configuration, + Consumer consumer) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + if (parent != null) { + context.setParent(parent); + } + context.register(configuration); + context.refresh(); + try { + consumer.accept(context); + } + finally { + context.close(); + } + } + + @Configuration + static class EmptyConfiguration { + + } + + @Configuration + static class TestEndpointConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + } + + @Configuration + static class TestEndpointSubclassConfiguration { + + @Bean + public TestEndpointSubclass testEndpointSubclass() { + return new TestEndpointSubclass(); + } + + } + + @Configuration + static class ClashingEndpointConfiguration { + + @Bean + public TestEndpoint testEndpointTwo() { + return new TestEndpoint(); + } + + @Bean + public TestEndpoint testEndpointOne() { + return new TestEndpoint(); + } + + } + + @Import({ TestEndpoint.class, SpecializedTestEndpoint.class, + SpecializedExtension.class }) + static class SpecializedEndpointsConfiguration { + + } + + @Endpoint(id = "test") + static class TestEndpoint { + + @ReadOperation + public Object getAll() { + return null; + } + + @ReadOperation + public Object getOne(@Selector String id) { + return null; + } + + @WriteOperation + public void update(String foo, String bar) { + + } + + @DeleteOperation + public void deleteOne(@Selector String id) { + + } + + public void someOtherMethod() { + + } + + } + + static class TestEndpointSubclass extends TestEndpoint { + + @WriteOperation + public void updateWithMoreArguments(String foo, String bar, String baz) { + + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Endpoint + @FilteredEndpoint(SpecializedEndpointFilter.class) + public @interface SpecializedEndpoint { + + @AliasFor(annotation = Endpoint.class) + String id(); + + } + + @EndpointExtension(endpoint = SpecializedTestEndpoint.class, filter = SpecializedEndpointFilter.class) + public static class SpecializedExtension { + + @ReadOperation + public Object getSpecial() { + return null; + } + + } + + static class SpecializedEndpointFilter extends DiscovererEndpointFilter { + + SpecializedEndpointFilter() { + super(SpecializedEndpointDiscoverer.class); + } + + } + + @SpecializedEndpoint(id = "specialized") + static class SpecializedTestEndpoint { + + @ReadOperation + public Object getAll() { + return null; + } + + } + + static class TestEndpointDiscoverer + extends EndpointDiscoverer { + + TestEndpointDiscoverer(ApplicationContext applicationContext) { + this(applicationContext, (id) -> null); + } + + TestEndpointDiscoverer(ApplicationContext applicationContext, + Function timeToLive) { + this(applicationContext, timeToLive, Collections.emptyList()); + } + + TestEndpointDiscoverer(ApplicationContext applicationContext, + Function timeToLive, + Collection> filters) { + this(applicationContext, new ConversionServiceParameterValueMapper(), + Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), + filters); + } + + TestEndpointDiscoverer(ApplicationContext applicationContext, + ParameterValueMapper parameterValueMapper, + Collection invokerAdvisors, + Collection> filters) { + super(applicationContext, parameterValueMapper, invokerAdvisors, filters); + } + + @Override + protected TestExposableEndpoint createEndpoint(String id, + boolean enabledByDefault, Collection operations) { + return new TestExposableEndpoint(this, id, enabledByDefault, operations); + } + + @Override + protected TestOperation createOperation(String endpointId, + DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { + return new TestOperation(operationMethod, invoker); + } + + @Override + protected OperationKey createOperationKey(TestOperation operation) { + return new OperationKey(operation.getOperationMethod(), + () -> "TestOperation " + operation.getOperationMethod()); + } + + } + + static class SpecializedEndpointDiscoverer extends + EndpointDiscoverer { + + SpecializedEndpointDiscoverer(ApplicationContext applicationContext) { + this(applicationContext, Collections.emptyList()); + } + + SpecializedEndpointDiscoverer(ApplicationContext applicationContext, + Collection> filters) { + super(applicationContext, new ConversionServiceParameterValueMapper(), + Collections.emptyList(), filters); + } + + @Override + protected SpecializedExposableEndpoint createEndpoint(String id, + boolean enabledByDefault, Collection operations) { + return new SpecializedExposableEndpoint(this, id, enabledByDefault, + operations); + } + + @Override + protected SpecializedOperation createOperation(String endpointId, + DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { + return new SpecializedOperation(operationMethod, invoker); + } + + @Override + protected OperationKey createOperationKey(SpecializedOperation operation) { + return new OperationKey(operation.getOperationMethod(), + () -> "TestOperation " + operation.getOperationMethod()); + } + + } + + static class TestExposableEndpoint extends AbstractDiscoveredEndpoint { + + TestExposableEndpoint(EndpointDiscoverer discoverer, String id, + boolean enabledByDefault, + Collection operations) { + super(discoverer, id, enabledByDefault, operations); + } + + } + + static class SpecializedExposableEndpoint + extends AbstractDiscoveredEndpoint { + + SpecializedExposableEndpoint(EndpointDiscoverer discoverer, String id, + boolean enabledByDefault, + Collection operations) { + super(discoverer, id, enabledByDefault, operations); + } + + } + + static class TestOperation extends AbstractDiscoveredOperation { + + private final OperationInvoker invoker; + + TestOperation(DiscoveredOperationMethod operationMethod, + OperationInvoker invoker) { + super(operationMethod, invoker); + this.invoker = invoker; + } + + public OperationInvoker getInvoker() { + return this.invoker; + } + + } + + static class SpecializedOperation extends TestOperation { + + SpecializedOperation(DiscoveredOperationMethod operationMethod, + OperationInvoker invoker) { + super(operationMethod, invoker); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java similarity index 60% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapperTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java index d5c4c99db4..aea9ea17fc 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/ConversionServiceParameterMapperTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java @@ -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; @@ -22,7 +22,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -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.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.support.DefaultFormattingConversionService; @@ -36,11 +37,11 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; /** - * Tests for {@link ConversionServiceParameterMapper}. + * Tests for {@link ConversionServiceParameterValueMapper}. * * @author Phillip Webb */ -public class ConversionServiceParameterMapperTests { +public class ConversionServiceParameterValueMapperTests { @Rule public ExpectedException thrown = ExpectedException.none(); @@ -49,9 +50,10 @@ public class ConversionServiceParameterMapperTests { public void mapParameterShouldDelegateToConversionService() { DefaultFormattingConversionService conversionService = spy( new DefaultFormattingConversionService()); - ConversionServiceParameterMapper mapper = new ConversionServiceParameterMapper( + ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper( conversionService); - Integer mapped = mapper.mapParameter("123", Integer.class); + Object mapped = mapper + .mapParameterValue(new TestOperationParameter(Integer.class), "123"); assertThat(mapped).isEqualTo(123); verify(conversionService).convert("123", Integer.class); } @@ -61,34 +63,61 @@ public class ConversionServiceParameterMapperTests { ConversionService conversionService = mock(ConversionService.class); RuntimeException error = new RuntimeException(); given(conversionService.convert(any(), any())).willThrow(error); - ConversionServiceParameterMapper mapper = new ConversionServiceParameterMapper( + ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper( conversionService); try { - mapper.mapParameter("123", Integer.class); + mapper.mapParameterValue(new TestOperationParameter(Integer.class), "123"); fail("Did not throw"); } catch (ParameterMappingException ex) { - assertThat(ex.getInput()).isEqualTo("123"); - assertThat(ex.getType()).isEqualTo(Integer.class); + assertThat(ex.getValue()).isEqualTo("123"); + assertThat(ex.getParameter().getType()).isEqualTo(Integer.class); assertThat(ex.getCause()).isEqualTo(error); } } @Test public void createShouldRegisterIsoOffsetDateTimeConverter() { - ConversionServiceParameterMapper mapper = new ConversionServiceParameterMapper(); - OffsetDateTime mapped = mapper.mapParameter("2011-12-03T10:15:30+01:00", - OffsetDateTime.class); + ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper(); + Object mapped = mapper.mapParameterValue( + new TestOperationParameter(OffsetDateTime.class), + "2011-12-03T10:15:30+01:00"); assertThat(mapped).isNotNull(); } @Test public void createWithConversionServiceShouldNotRegisterIsoOffsetDateTimeConverter() { ConversionService conversionService = new DefaultConversionService(); - ConversionServiceParameterMapper mapper = new ConversionServiceParameterMapper( + ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper( conversionService); this.thrown.expect(ParameterMappingException.class); - mapper.mapParameter("2011-12-03T10:15:30+01:00", OffsetDateTime.class); + mapper.mapParameterValue(new TestOperationParameter(OffsetDateTime.class), + "2011-12-03T10:15:30+01:00"); + } + + private static class TestOperationParameter implements OperationParameter { + + private final Class type; + + TestOperationParameter(Class type) { + this.type = type; + } + + @Override + public String getName() { + return "test"; + } + + @Override + public Class getType() { + return this.type; + } + + @Override + public boolean isNullable() { + return true; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverterTests.java similarity index 95% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverterTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverterTests.java index f2dd860f32..ca9ae02388 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/convert/IsoOffsetDateTimeConverterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/IsoOffsetDateTimeConverterTests.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java new file mode 100644 index 0000000000..ebf5e8aab7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java @@ -0,0 +1,70 @@ +/* + * 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 org.junit.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OperationMethodParameter}. + * + * @author Phillip Webb + */ +public class OperationMethodParameterTests { + + private Method method = ReflectionUtils.findMethod(getClass(), "example", + String.class, String.class); + + @Test + public void getNameShouldReturnName() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.method.getParameters()[0]); + assertThat(parameter.getName()).isEqualTo("name"); + } + + @Test + public void getTypeShouldReturnTyoe() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.method.getParameters()[0]); + assertThat(parameter.getType()).isEqualTo(String.class); + } + + @Test + public void isNullableWhenNoAnnotationShouldReturnFalse() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.method.getParameters()[0]); + assertThat(parameter.isNullable()).isFalse(); + } + + @Test + public void isNullableWhenNullableAnnotationShouldReturnTrue() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.method.getParameters()[1]); + assertThat(parameter.isNullable()).isTrue(); + } + + void example(String one, @Nullable String two) { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java new file mode 100644 index 0000000000..d18d273381 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java @@ -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.invoke.reflect; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OperationMethodParameters}. + * + * @author Phillip Webb + */ +public class OperationMethodParametersTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Method exampleMethod = ReflectionUtils.findMethod(getClass(), "example", + String.class); + + private Method exampleNoParamsMethod = ReflectionUtils.findMethod(getClass(), + "exampleNoParams"); + + @Test + public void createWhenMethodIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Method must not be null"); + new OperationMethodParameters(null, mock(ParameterNameDiscoverer.class)); + } + + @Test + public void createWhenParameterNameDiscovererIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ParameterNameDiscoverer must not be null"); + new OperationMethodParameters(this.exampleMethod, null); + } + + @Test + public void createWhenParameterNameDiscovererReturnsNullShouldThrowException() { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Failed to extract parameter names"); + new OperationMethodParameters(this.exampleMethod, + mock(ParameterNameDiscoverer.class)); + } + + @Test + public void hasParametersWhenHasParametersShouldReturnTrue() { + OperationMethodParameters parameters = new OperationMethodParameters( + this.exampleMethod, new DefaultParameterNameDiscoverer()); + assertThat(parameters.hasParameters()).isTrue(); + } + + @Test + public void hasParametersWhenHasNoParametersShouldReturnFalse() { + OperationMethodParameters parameters = new OperationMethodParameters( + this.exampleNoParamsMethod, new DefaultParameterNameDiscoverer()); + assertThat(parameters.hasParameters()).isFalse(); + } + + @Test + public void getParameterCountShouldReturnParameterCount() { + OperationMethodParameters parameters = new OperationMethodParameters( + this.exampleMethod, new DefaultParameterNameDiscoverer()); + assertThat(parameters.getParameterCount()).isEqualTo(1); + } + + @Test + public void iteratorShouldIterateOperationParameters() { + OperationMethodParameters parameters = new OperationMethodParameters( + this.exampleMethod, new DefaultParameterNameDiscoverer()); + Iterator iterator = parameters.iterator(); + assertParameters(StreamSupport.stream( + Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), + false)); + } + + @Test + public void streamShouldStreamOperationParameters() { + OperationMethodParameters parameters = new OperationMethodParameters( + this.exampleMethod, new DefaultParameterNameDiscoverer()); + assertParameters(parameters.stream()); + } + + private void assertParameters(Stream stream) { + List parameters = stream.collect(Collectors.toList()); + assertThat(parameters).hasSize(1); + OperationParameter parameter = parameters.get(0); + assertThat(parameter.getName()).isEqualTo("name"); + assertThat(parameter.getType()).isEqualTo(String.class); + } + + String example(String name) { + return name; + } + + String exampleNoParams() { + return "example"; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java new file mode 100644 index 0000000000..c79bcd210f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java @@ -0,0 +1,84 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OperationMethod}. + * + * @author Phillip Webb + */ +public class OperationMethodTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Method exampleMethod = ReflectionUtils.findMethod(getClass(), "example", + String.class); + + @Test + public void createWhenMethodIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Method must not be null"); + new OperationMethod(null, OperationType.READ); + } + + @Test + public void createWhenOperationTypeIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("OperationType must not be null"); + new OperationMethod(this.exampleMethod, null); + } + + @Test + public void getMethodShouldReturnMethod() { + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, + OperationType.READ); + assertThat(operationMethod.getMethod()).isEqualTo(this.exampleMethod); + } + + @Test + public void getOperationTypeShouldReturnOperationType() { + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, + OperationType.READ); + assertThat(operationMethod.getOperationType()).isEqualTo(OperationType.READ); + } + + @Test + public void getParametersShouldReturnParameters() { + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, + OperationType.READ); + OperationParameters parameters = operationMethod.getParameters(); + assertThat(parameters.getParameterCount()).isEqualTo(1); + assertThat(parameters.iterator().next().getName()).isEqualTo("name"); + } + + String example(String name) { + return name; + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java new file mode 100644 index 0000000000..ac7f7ab1f5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java @@ -0,0 +1,128 @@ +/* + * 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.util.Collections; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; +import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectiveOperationInvoker}. + * + * @author Phillip Webb + */ +public class ReflectiveOperationInvokerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Example target; + + private OperationMethod operationMethod; + + private ParameterValueMapper parameterValueMapper; + + @Before + public void setup() { + this.target = new Example(); + this.operationMethod = new OperationMethod( + ReflectionUtils.findMethod(Example.class, "reverse", String.class), + OperationType.READ); + this.parameterValueMapper = (parameter, + value) -> (value == null ? null : value.toString()); + } + + @Test + public void createWhenTargetIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Target must not be null"); + new ReflectiveOperationInvoker(null, this.operationMethod, + this.parameterValueMapper); + } + + @Test + public void createWhenOperationMethodIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("OperationMethod must not be null"); + new ReflectiveOperationInvoker(this.target, null, this.parameterValueMapper); + } + + @Test + public void createWhenParamaterValueMapperIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ParameterValueMapper must not be null"); + new ReflectiveOperationInvoker(this.target, this.operationMethod, null); + } + + @Test + public void invokeShouldInvokeMethod() { + ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, + this.operationMethod, this.parameterValueMapper); + Object result = invoker.invoke(Collections.singletonMap("name", "boot")); + assertThat(result).isEqualTo("toob"); + } + + @Test + public void invokeWhenMissingNonNullableArgmentShouldThrowException() { + ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, + this.operationMethod, this.parameterValueMapper); + this.thrown.expect(MissingParametersException.class); + invoker.invoke(Collections.singletonMap("name", null)); + } + + @Test + public void invokeWhenMissingNullableArgumentShouldInvoke() { + OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod( + Example.class, "reverseNullable", String.class), OperationType.READ); + ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, + operationMethod, this.parameterValueMapper); + Object result = invoker.invoke(Collections.singletonMap("name", null)); + assertThat(result).isEqualTo("llun"); + } + + @Test + public void invokeShouldResolveParameters() { + ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, + this.operationMethod, this.parameterValueMapper); + Object result = invoker.invoke(Collections.singletonMap("name", 1234)); + assertThat(result).isEqualTo("4321"); + } + + static class Example { + + String reverse(String name) { + return new StringBuilder(name).reverse().toString(); + } + + String reverseNullable(@Nullable String name) { + return new StringBuilder(String.valueOf(name)).reverse().toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java similarity index 64% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisorTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java index 0e9948c574..cbedccee0a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerAdvisorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java @@ -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.cache; +package org.springframework.boot.actuate.endpoint.invoker.cache; import java.lang.reflect.Method; import java.util.function.Function; @@ -24,10 +24,10 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -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.core.annotation.AnnotationAttributes; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; +import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ReflectionUtils; @@ -59,53 +59,62 @@ public class CachingOperationInvokerAdvisorTests { @Test public void applyWhenOperationIsNotReadShouldNotAddAdvise() { - OperationMethodInfo info = mockInfo(OperationType.WRITE, "get"); - OperationInvoker advised = this.advisor.apply("foo", info, this.invoker); + OperationParameters parameters = getParameters("get"); + OperationInvoker advised = this.advisor.apply("foo", OperationType.WRITE, + parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); } @Test public void applyWhenHasParametersShouldNotAddAdvise() { - OperationMethodInfo info = mockInfo(OperationType.READ, "getWithParameter", - String.class); - OperationInvoker advised = this.advisor.apply("foo", info, this.invoker); + OperationParameters parameters = getParameters("getWithParameter", String.class); + OperationInvoker advised = this.advisor.apply("foo", OperationType.READ, + parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); } @Test public void applyWhenTimeToLiveReturnsNullShouldNotAddAdvise() { - OperationMethodInfo info = mockInfo(OperationType.READ, "get"); + OperationParameters parameters = getParameters("get"); given(this.timeToLive.apply(any())).willReturn(null); - OperationInvoker advised = this.advisor.apply("foo", info, this.invoker); + OperationInvoker advised = this.advisor.apply("foo", OperationType.READ, + parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); verify(this.timeToLive).apply("foo"); } @Test public void applyWhenTimeToLiveIsZeroShouldNotAddAdvise() { - OperationMethodInfo info = mockInfo(OperationType.READ, "get"); + OperationParameters parameters = getParameters("get"); given(this.timeToLive.apply(any())).willReturn(0L); - OperationInvoker advised = this.advisor.apply("foo", info, this.invoker); + OperationInvoker advised = this.advisor.apply("foo", OperationType.READ, + parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); verify(this.timeToLive).apply("foo"); } @Test public void applyShouldAddCacheAdvise() { - OperationMethodInfo info = mockInfo(OperationType.READ, "get"); + OperationParameters parameters = getParameters("get"); given(this.timeToLive.apply(any())).willReturn(100L); - OperationInvoker advised = this.advisor.apply("foo", info, this.invoker); + OperationInvoker advised = this.advisor.apply("foo", OperationType.READ, + parameters, this.invoker); assertThat(advised).isInstanceOf(CachingOperationInvoker.class); - assertThat(ReflectionTestUtils.getField(advised, "target")) + assertThat(ReflectionTestUtils.getField(advised, "invoker")) .isEqualTo(this.invoker); assertThat(ReflectionTestUtils.getField(advised, "timeToLive")).isEqualTo(100L); } - private OperationMethodInfo mockInfo(OperationType operationType, String methodName, + private OperationParameters getParameters(String methodName, + Class... parameterTypes) { + return getOperationMethod(methodName, parameterTypes).getParameters(); + } + + private OperationMethod getOperationMethod(String methodName, Class... parameterTypes) { Method method = ReflectionUtils.findMethod(TestOperations.class, methodName, parameterTypes); - return new OperationMethodInfo(method, operationType, new AnnotationAttributes()); + return new OperationMethod(method, OperationType.READ); } public static class TestOperations { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java similarity index 90% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java index 334687d660..dbfe0eb052 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/cache/CachingOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java @@ -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.cache; +package org.springframework.boot.actuate.endpoint.invoker.cache; import java.util.HashMap; import java.util.Map; @@ -23,7 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.boot.actuate.endpoint.OperationInvoker; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -44,7 +44,7 @@ public class CachingOperationInvokerTests { @Test public void createInstanceWithTtlSetToZero() { - this.thrown.expect(IllegalStateException.class); + this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("TimeToLive"); new CachingOperationInvoker(mock(OperationInvoker.class), 0); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java deleted file mode 100644 index b36857ebf1..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java +++ /dev/null @@ -1,131 +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.Collections; -import java.util.Map; - -import javax.management.MBeanInfo; -import javax.management.MBeanOperationInfo; -import javax.management.MBeanParameterInfo; - -import org.junit.Test; - -import org.springframework.boot.actuate.endpoint.EndpointInfo; -import org.springframework.boot.actuate.endpoint.OperationInvoker; -import org.springframework.boot.actuate.endpoint.OperationType; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -/** - * Tests for {@link EndpointMBeanInfoAssembler}. - * - * @author Stephane Nicoll - */ -public class EndpointMBeanInfoAssemblerTests { - - private final EndpointMBeanInfoAssembler mBeanInfoAssembler = new EndpointMBeanInfoAssembler( - new DummyOperationResponseMapper()); - - @Test - public void exposeSimpleReadOperation() { - JmxOperation operation = new JmxOperation(OperationType.READ, - new DummyOperationInvoker(), "getAll", Object.class, "Test operation", - Collections.emptyList()); - EndpointInfo endpoint = new EndpointInfo<>("test", true, - Collections.singletonList(operation)); - EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler - .createEndpointMBeanInfo(endpoint); - assertThat(endpointMBeanInfo).isNotNull(); - assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("test"); - assertThat(endpointMBeanInfo.getOperations()) - .containsOnly(entry("getAll", operation)); - MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo(); - assertThat(mbeanInfo).isNotNull(); - assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName()); - assertThat(mbeanInfo.getDescription()) - .isEqualTo("MBean operations for endpoint test"); - assertThat(mbeanInfo.getAttributes()).isEmpty(); - assertThat(mbeanInfo.getNotifications()).isEmpty(); - assertThat(mbeanInfo.getConstructors()).isEmpty(); - assertThat(mbeanInfo.getOperations()).hasSize(1); - MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0]; - assertThat(mBeanOperationInfo.getName()).isEqualTo("getAll"); - assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName()); - assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.INFO); - assertThat(mBeanOperationInfo.getSignature()).hasSize(0); - } - - @Test - public void exposeSimpleWriteOperation() { - JmxOperation operation = new JmxOperation(OperationType.WRITE, - new DummyOperationInvoker(), "update", Object.class, "Update operation", - Collections.singletonList(new JmxEndpointOperationParameterInfo("test", - String.class, "Test argument"))); - EndpointInfo endpoint = new EndpointInfo<>("another", true, - Collections.singletonList(operation)); - EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler - .createEndpointMBeanInfo(endpoint); - assertThat(endpointMBeanInfo).isNotNull(); - assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("another"); - assertThat(endpointMBeanInfo.getOperations()) - .containsOnly(entry("update", operation)); - MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo(); - assertThat(mbeanInfo).isNotNull(); - assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName()); - assertThat(mbeanInfo.getDescription()) - .isEqualTo("MBean operations for endpoint another"); - assertThat(mbeanInfo.getAttributes()).isEmpty(); - assertThat(mbeanInfo.getNotifications()).isEmpty(); - assertThat(mbeanInfo.getConstructors()).isEmpty(); - assertThat(mbeanInfo.getOperations()).hasSize(1); - MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0]; - assertThat(mBeanOperationInfo.getName()).isEqualTo("update"); - assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName()); - assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.ACTION); - assertThat(mBeanOperationInfo.getSignature()).hasSize(1); - MBeanParameterInfo mBeanParameterInfo = mBeanOperationInfo.getSignature()[0]; - assertThat(mBeanParameterInfo.getName()).isEqualTo("test"); - assertThat(mBeanParameterInfo.getType()).isEqualTo(String.class.getName()); - assertThat(mBeanParameterInfo.getDescription()).isEqualTo("Test argument"); - } - - private static class DummyOperationInvoker implements OperationInvoker { - - @Override - public Object invoke(Map arguments) { - return null; - } - - } - - private static class DummyOperationResponseMapper - implements JmxOperationResponseMapper { - - @Override - public Object mapResponse(Object response) { - return response; - } - - @Override - public Class mapResponseType(Class responseType) { - return responseType; - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrarTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrarTests.java deleted file mode 100644 index 4cbb06fdf8..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanRegistrarTests.java +++ /dev/null @@ -1,134 +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.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.jmx.JmxException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link EndpointMBeanRegistrar}. - * - * @author Stephane Nicoll - */ -public class EndpointMBeanRegistrarTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private MBeanServer mBeanServer = mock(MBeanServer.class); - - @Test - public void mBeanServerMustNotBeNull() { - this.thrown.expect(IllegalArgumentException.class); - new EndpointMBeanRegistrar(null, (e) -> new ObjectName("foo")); - } - - @Test - public void objectNameFactoryMustNotBeNull() { - this.thrown.expect(IllegalArgumentException.class); - new EndpointMBeanRegistrar(this.mBeanServer, null); - } - - @Test - public void endpointMustNotBeNull() { - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - (e) -> new ObjectName("foo")); - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Endpoint must not be null"); - registrar.registerEndpointMBean(null); - } - - @Test - public void registerEndpointInvokesObjectNameFactory() - throws MalformedObjectNameException { - EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class); - EndpointMBean endpointMBean = mock(EndpointMBean.class); - ObjectName objectName = mock(ObjectName.class); - given(factory.generate(endpointMBean)).willReturn(objectName); - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - factory); - ObjectName actualObjectName = registrar.registerEndpointMBean(endpointMBean); - assertThat(actualObjectName).isSameAs(objectName); - verify(factory).generate(endpointMBean); - } - - @Test - public void registerEndpointInvalidObjectName() throws MalformedObjectNameException { - EndpointMBean endpointMBean = mock(EndpointMBean.class); - given(endpointMBean.getEndpointId()).willReturn("test"); - EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class); - given(factory.generate(endpointMBean)) - .willThrow(new MalformedObjectNameException()); - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - factory); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Invalid ObjectName for endpoint with id 'test'"); - registrar.registerEndpointMBean(endpointMBean); - } - - @Test - public void registerEndpointFailure() throws Exception { - EndpointMBean endpointMBean = mock(EndpointMBean.class); - given(endpointMBean.getEndpointId()).willReturn("test"); - EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class); - ObjectName objectName = mock(ObjectName.class); - given(factory.generate(endpointMBean)).willReturn(objectName); - given(this.mBeanServer.registerMBean(endpointMBean, objectName)) - .willThrow(MBeanRegistrationException.class); - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - factory); - this.thrown.expect(JmxException.class); - this.thrown.expectMessage("Failed to register MBean for endpoint with id 'test'"); - registrar.registerEndpointMBean(endpointMBean); - } - - @Test - public void unregisterEndpoint() throws Exception { - ObjectName objectName = mock(ObjectName.class); - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - mock(EndpointObjectNameFactory.class)); - assertThat(registrar.unregisterEndpointMbean(objectName)).isTrue(); - verify(this.mBeanServer).unregisterMBean(objectName); - } - - @Test - public void unregisterUnknownEndpoint() throws Exception { - ObjectName objectName = mock(ObjectName.class); - willThrow(InstanceNotFoundException.class).given(this.mBeanServer) - .unregisterMBean(objectName); - EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer, - mock(EndpointObjectNameFactory.class)); - assertThat(registrar.unregisterEndpointMbean(objectName)).isFalse(); - verify(this.mBeanServer).unregisterMBean(objectName); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java index cbbc30c43f..dc70b4cd3b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java @@ -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,428 +16,136 @@ package org.springframework.boot.actuate.endpoint.jmx; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.UUID; -import java.util.function.Consumer; - import javax.management.Attribute; import javax.management.AttributeList; import javax.management.AttributeNotFoundException; -import javax.management.InstanceNotFoundException; +import javax.management.InvalidAttributeValueException; import javax.management.MBeanException; import javax.management.MBeanInfo; -import javax.management.MBeanOperationInfo; -import javax.management.MBeanParameterInfo; -import javax.management.MBeanServer; -import javax.management.MBeanServerFactory; -import javax.management.ObjectName; import javax.management.ReflectionException; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import reactor.core.publisher.Mono; -import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; -import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxAnnotationEndpointDiscoverer; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; - import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * Tests for {@link EndpointMBean}. * + * @author Phillip Webb * @author Stephane Nicoll */ public class EndpointMBeanTests { - private final JmxEndpointMBeanFactory jmxEndpointMBeanFactory = new JmxEndpointMBeanFactory( - new TestJmxOperationResponseMapper()); + private static final Object[] NO_PARAMS = {}; - private MBeanServer server; + private static final String[] NO_SIGNATURE = {}; - private EndpointMBeanRegistrar endpointMBeanRegistrar; + @Rule + public ExpectedException thrown = ExpectedException.none(); - private EndpointObjectNameFactory objectNameFactory = (endpoint) -> new ObjectName( - String.format("org.springframework.boot.test:type=Endpoint,name=%s", - UUID.randomUUID().toString())); + private TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( + new TestJmxOperation()); - @Before - public void createMBeanServer() { - this.server = MBeanServerFactory.createMBeanServer(); - this.endpointMBeanRegistrar = new EndpointMBeanRegistrar(this.server, - this.objectNameFactory); - } - - @After - public void disposeMBeanServer() { - if (this.server != null) { - MBeanServerFactory.releaseMBeanServer(this.server); - } - } + private TestJmxOperationResponseMapper responseMapper = new TestJmxOperationResponseMapper(); @Test - public void invokeSimpleEndpoint() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - // getAll - Object allResponse = this.server.invoke(objectName, "getAll", - new Object[0], new String[0]); - assertThat(allResponse).isEqualTo("[ONE, TWO]"); - - // getOne - Object oneResponse = this.server.invoke(objectName, "getOne", - new Object[] { "one" }, new String[] { String.class.getName() }); - assertThat(oneResponse).isEqualTo("ONE"); - - // update - Object updateResponse = this.server.invoke(objectName, "update", - new Object[] { "one", "1" }, - new String[] { String.class.getName(), String.class.getName() }); - assertThat(updateResponse).isNull(); - - // getOne validation after update - Object updatedOneResponse = this.server.invoke(objectName, "getOne", - new Object[] { "one" }, new String[] { String.class.getName() }); - assertThat(updatedOneResponse).isEqualTo("1"); - - // deleteOne - Object deleteResponse = this.server.invoke(objectName, "deleteOne", - new Object[] { "one" }, new String[] { String.class.getName() }); - assertThat(deleteResponse).isNull(); - - // getOne validation after delete - updatedOneResponse = this.server.invoke(objectName, "getOne", - new Object[] { "one" }, new String[] { String.class.getName() }); - assertThat(updatedOneResponse).isNull(); - } - catch (Exception ex) { - throw new AssertionError("Failed to invoke method on FooEndpoint", ex); - } - }); + public void createWhenResponseMapperIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ResponseMapper must not be null"); + new EndpointMBean(null, mock(ExposableJmxEndpoint.class)); } @Test - public void jmxTypesAreProperlyMapped() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - MBeanInfo mBeanInfo = this.server.getMBeanInfo(objectName); - Map operations = mapOperations(mBeanInfo); - assertThat(operations).containsOnlyKeys("getAll", "getOne", "update", - "deleteOne"); - assertOperation(operations.get("getAll"), String.class, - MBeanOperationInfo.INFO, new Class[0]); - assertOperation(operations.get("getOne"), String.class, - MBeanOperationInfo.INFO, new Class[] { String.class }); - assertOperation(operations.get("update"), Void.TYPE, - MBeanOperationInfo.ACTION, - new Class[] { String.class, String.class }); - assertOperation(operations.get("deleteOne"), Void.TYPE, - MBeanOperationInfo.ACTION, new Class[] { String.class }); - } - catch (Exception ex) { - throw new AssertionError("Failed to retrieve MBeanInfo of FooEndpoint", - ex); - } - }); - } - - private void assertOperation(MBeanOperationInfo operation, Class returnType, - int impact, Class[] types) { - assertThat(operation.getReturnType()).isEqualTo(returnType.getName()); - assertThat(operation.getImpact()).isEqualTo(impact); - MBeanParameterInfo[] signature = operation.getSignature(); - assertThat(signature).hasSize(types.length); - for (int i = 0; i < types.length; i++) { - assertThat(signature[i].getType()).isEqualTo(types[0].getName()); - } + public void createWhenEndpointIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Endpoint must not be null"); + new EndpointMBean(mock(JmxOperationResponseMapper.class), null); } @Test - public void invokeReactiveOperation() { - load(ReactiveEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "reactive"); - try { - Object allResponse = this.server.invoke(objectName, "getInfo", - new Object[0], new String[0]); - assertThat(allResponse).isInstanceOf(String.class); - assertThat(allResponse).isEqualTo("HELLO WORLD"); - } - catch (Exception ex) { - throw new AssertionError("Failed to invoke getInfo method", ex); - } - }); - + public void getMBeanInfoShouldReturnMBeanInfo() { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + MBeanInfo info = bean.getMBeanInfo(); + assertThat(info.getDescription()).isEqualTo("MBean operations for endpoint test"); } @Test - public void invokeUnknownOperation() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - this.server.invoke(objectName, "doesNotExist", new Object[0], - new String[0]); - throw new AssertionError( - "Should have failed to invoke unknown operation"); - } - catch (ReflectionException ex) { - assertThat(ex.getCause()).isInstanceOf(IllegalArgumentException.class); - assertThat(ex.getCause().getMessage()).contains("doesNotExist", "foo"); - } - catch (MBeanException | InstanceNotFoundException ex) { - throw new IllegalStateException(ex); - } - - }); + public void invokeShouldInvokeJmxOperation() + throws MBeanException, ReflectionException { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); + assertThat(result).isEqualTo("result"); } @Test - public void dynamicMBeanCannotReadAttribute() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - this.server.getAttribute(objectName, "foo"); - throw new AssertionError("Should have failed to read attribute foo"); - } - catch (Exception ex) { - assertThat(ex).isInstanceOf(AttributeNotFoundException.class); - } - }); + public void invokeWhenActionNameIsNotAnOperationShouldThrowException() + throws MBeanException, ReflectionException { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + this.thrown.expect(ReflectionException.class); + this.thrown.expectCause(instanceOf(IllegalArgumentException.class)); + this.thrown.expectMessage("no operation named missingOperation"); + bean.invoke("missingOperation", NO_PARAMS, NO_SIGNATURE); } @Test - public void dynamicMBeanCannotWriteAttribute() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - this.server.setAttribute(objectName, new Attribute("foo", "bar")); - throw new AssertionError("Should have failed to write attribute foo"); - } - catch (Exception ex) { - assertThat(ex).isInstanceOf(AttributeNotFoundException.class); - } - }); + public void invokeWhenMonoResultShouldBlockOnMono() + throws MBeanException, ReflectionException { + TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( + new TestJmxOperation((arguments) -> Mono.just("monoResult"))); + EndpointMBean bean = new EndpointMBean(this.responseMapper, endpoint); + Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); + assertThat(result).isEqualTo("monoResult"); } @Test - public void dynamicMBeanCannotReadAttributes() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - AttributeList attributes = this.server.getAttributes(objectName, - new String[] { "foo", "bar" }); - assertThat(attributes).isNotNull(); - assertThat(attributes).isEmpty(); - } - catch (Exception ex) { - throw new AssertionError("Failed to invoke getAttributes", ex); - } - }); + public void invokeShouldCallResponseMapper() + throws MBeanException, ReflectionException { + TestJmxOperationResponseMapper responseMapper = spy(this.responseMapper); + EndpointMBean bean = new EndpointMBean(responseMapper, this.endpoint); + bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); + verify(responseMapper).mapResponseType(String.class); + verify(responseMapper).mapResponse("result"); } @Test - public void dynamicMBeanCannotWriteAttributes() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - AttributeList attributes = new AttributeList(); - attributes.add(new Attribute("foo", 1)); - attributes.add(new Attribute("bar", 42)); - AttributeList attributesSet = this.server.setAttributes(objectName, - attributes); - assertThat(attributesSet).isNotNull(); - assertThat(attributesSet).isEmpty(); - } - catch (Exception ex) { - throw new AssertionError("Failed to invoke setAttributes", ex); - } - }); + public void getAttributeShouldThrowException() + throws AttributeNotFoundException, MBeanException, ReflectionException { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + this.thrown.expect(AttributeNotFoundException.class); + this.thrown.expectMessage("EndpointMBeans do not support attributes"); + bean.getAttribute("test"); } @Test - public void invokeWithParameterMappingExceptionMapsToIllegalArgumentException() { - load(FooEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "foo"); - try { - this.server.invoke(objectName, "getOne", new Object[] { "wrong" }, - new String[] { String.class.getName() }); - } - catch (Exception ex) { - assertThat(ex.getCause()) - .isExactlyInstanceOf(IllegalArgumentException.class); - assertThat(ex.getCause().getMessage()).isEqualTo( - String.format("Failed to map wrong of type " + "%s to type %s", - String.class, FooName.class)); - } - }); + public void setAttributeShouldThrowException() throws AttributeNotFoundException, + InvalidAttributeValueException, MBeanException, ReflectionException { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + this.thrown.expect(AttributeNotFoundException.class); + this.thrown.expectMessage("EndpointMBeans do not support attributes"); + bean.setAttribute(new Attribute("test", "test")); } @Test - public void invokeWithMissingRequiredParameterExceptionMapsToIllegalArgumentException() { - load(RequiredParametersEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "requiredparameters"); - try { - this.server.invoke(objectName, "read", new Object[] {}, - new String[] { String.class.getName() }); - } - catch (Exception ex) { - assertThat(ex.getCause()) - .isExactlyInstanceOf(IllegalArgumentException.class); - assertThat(ex.getCause().getMessage()) - .isEqualTo("Failed to invoke operation because the following " - + "required parameters were missing: foo,baz"); - } - }); + public void getAttributesShouldReturnEmptyAttributeList() { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + AttributeList attributes = bean.getAttributes(new String[] { "test" }); + assertThat(attributes).isEmpty(); } @Test - public void invokeWithMissingNullableParameter() { - load(RequiredParametersEndpoint.class, (discoverer) -> { - ObjectName objectName = registerEndpoint(discoverer, "requiredparameters"); - try { - this.server.invoke(objectName, "read", - new Object[] { null, "hello", "world" }, - new String[] { String.class.getName() }); - } - catch (Exception ex) { - throw new AssertionError("Nullable parameter should not be required."); - } - }); - } - - private ObjectName registerEndpoint(JmxAnnotationEndpointDiscoverer discoverer, - String endpointId) { - Collection mBeans = this.jmxEndpointMBeanFactory - .createMBeans(discoverer.discoverEndpoints()); - assertThat(mBeans).hasSize(1); - EndpointMBean endpointMBean = mBeans.iterator().next(); - assertThat(endpointMBean.getEndpointId()).isEqualTo(endpointId); - return this.endpointMBeanRegistrar.registerEndpointMBean(endpointMBean); - } - - private Map mapOperations(MBeanInfo info) { - Map operations = new HashMap<>(); - for (MBeanOperationInfo mBeanOperationInfo : info.getOperations()) { - operations.put(mBeanOperationInfo.getName(), mBeanOperationInfo); - } - return operations; - } - - private void load(Class configuration, - Consumer consumer) { - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - configuration)) { - ConversionServiceParameterMapper parameterMapper = new ConversionServiceParameterMapper( - DefaultConversionService.getSharedInstance()); - JmxAnnotationEndpointDiscoverer discoverer = new JmxAnnotationEndpointDiscoverer( - context, parameterMapper, null, null); - consumer.accept(discoverer); - } - } - - @Endpoint(id = "foo") - static class FooEndpoint { - - private final Map all = new LinkedHashMap<>(); - - FooEndpoint() { - this.all.put(FooName.ONE, new Foo("one")); - this.all.put(FooName.TWO, new Foo("two")); - } - - @ReadOperation - public Collection getAll() { - return this.all.values(); - } - - @ReadOperation - public Foo getOne(FooName name) { - return this.all.get(name); - } - - @WriteOperation - public void update(FooName name, String value) { - this.all.put(name, new Foo(value)); - } - - @DeleteOperation - public void deleteOne(FooName name) { - this.all.remove(name); - } - - } - - @Endpoint(id = "requiredparameters") - static class RequiredParametersEndpoint { - - @ReadOperation - public String read(@Nullable String bar, String foo, String baz) { - return foo; - } - - } - - @Endpoint(id = "reactive") - static class ReactiveEndpoint { - - @ReadOperation - public Mono getInfo() { - return Mono.defer(() -> Mono.just("Hello World")); - } - - } - - enum FooName { - - ONE, TWO, THREE - - } - - static class Foo { - - private final String name; - - Foo(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - - @Override - public String toString() { - return this.name; - } - - } - - private static class TestJmxOperationResponseMapper - implements JmxOperationResponseMapper { - - @Override - public Object mapResponse(Object response) { - return (response != null ? response.toString().toUpperCase() : null); - } - - @Override - public Class mapResponseType(Class responseType) { - if (responseType == Void.TYPE) { - return Void.TYPE; - } - return String.class; - } + public void setAttributesShouldReturnEmptyAttributeList() { + EndpointMBean bean = new EndpointMBean(this.responseMapper, this.endpoint); + AttributeList sourceAttributes = new AttributeList(); + sourceAttributes.add(new Attribute("test", "test")); + AttributeList attributes = bean.setAttributes(sourceAttributes); + assertThat(attributes).isEmpty(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java new file mode 100644 index 0000000000..2f3ffc1d37 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java @@ -0,0 +1,137 @@ +/* + * 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.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import org.springframework.boot.test.json.BasicJsonTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JacksonJmxOperationResponseMapper} + * + * @author Phillip Webb + */ +public class JacksonJmxOperationResponseMapperTests { + + private JacksonJmxOperationResponseMapper mapper = new JacksonJmxOperationResponseMapper( + null); + + private final BasicJsonTester json = new BasicJsonTester(getClass()); + + @Test + public void createWhenObjectMapperIsNullShouldUseDefaultObjectMapper() { + JacksonJmxOperationResponseMapper mapper = new JacksonJmxOperationResponseMapper( + null); + Object mapped = mapper.mapResponse(Collections.singleton("test")); + assertThat(this.json.from(mapped.toString())).isEqualToJson("[test]"); + } + + @Test + public void createWhenObjectMapperIsSpecifiedShouldUseObjectMapper() { + ObjectMapper objectMapper = spy(ObjectMapper.class); + JacksonJmxOperationResponseMapper mapper = new JacksonJmxOperationResponseMapper( + objectMapper); + Set response = Collections.singleton("test"); + mapper.mapResponse(response); + verify(objectMapper).convertValue(eq(response), any(JavaType.class)); + } + + @Test + public void mapResponseTypeWhenCharSequenceShouldReturnString() { + assertThat(this.mapper.mapResponseType(String.class)).isEqualTo(String.class); + assertThat(this.mapper.mapResponseType(StringBuilder.class)) + .isEqualTo(String.class); + } + + @Test + public void mapResponseTypeWhenArrayShouldReturnList() { + assertThat(this.mapper.mapResponseType(String[].class)).isEqualTo(List.class); + assertThat(this.mapper.mapResponseType(Object[].class)).isEqualTo(List.class); + } + + @Test + public void mapResponseTypeWhenCollectionShouldReturnList() { + assertThat(this.mapper.mapResponseType(Collection.class)).isEqualTo(List.class); + assertThat(this.mapper.mapResponseType(Set.class)).isEqualTo(List.class); + assertThat(this.mapper.mapResponseType(List.class)).isEqualTo(List.class); + } + + @Test + public void mapResponseTypeWhenOtherShouldReturnMap() { + assertThat(this.mapper.mapResponseType(ExampleBean.class)).isEqualTo(Map.class); + } + + @Test + public void mapResponseWhenNullShouldReturnNull() { + assertThat(this.mapper.mapResponse(null)).isNull(); + } + + @Test + public void mapResponseWhenCharSequenceShouldReturnString() { + assertThat(this.mapper.mapResponse(new StringBuilder("test"))).isEqualTo("test"); + } + + @Test + public void mapResponseWhenArrayShouldReturnJsonArray() { + Object mapped = this.mapper.mapResponse(new int[] { 1, 2, 3 }); + assertThat(this.json.from(mapped.toString())).isEqualToJson("[1,2,3]"); + } + + @Test + public void mapResponseWhenCollectionShouldReturnJsonArray() { + Object mapped = this.mapper.mapResponse(Arrays.asList("a", "b", "c")); + assertThat(this.json.from(mapped.toString())).isEqualToJson("[a,b,c]"); + } + + @Test + public void mapResponseWhenOtherShouldReturnMap() { + ExampleBean bean = new ExampleBean(); + bean.setName("boot"); + Object mapped = this.mapper.mapResponse(bean); + assertThat(this.json.from(mapped.toString())).isEqualToJson("{'name':'boot'}"); + } + + public static class ExampleBean { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java new file mode 100644 index 0000000000..29d9af6ca3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java @@ -0,0 +1,199 @@ +/* + * 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.ArrayList; +import java.util.List; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.jmx.JmxException; +import org.springframework.jmx.export.MBeanExportException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JmxEndpointExporter}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class JmxEndpointExporterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private MBeanServer mBeanServer; + + private EndpointObjectNameFactory objectNameFactory = spy( + new TestEndpointObjectNameFactory()); + + private JmxOperationResponseMapper responseMapper = new TestJmxOperationResponseMapper(); + + private List endpoints = new ArrayList<>(); + + @Captor + private ArgumentCaptor objectCaptor; + + @Captor + private ArgumentCaptor objectNameCaptor; + + private JmxEndpointExporter exporter; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + this.exporter = new JmxEndpointExporter(this.mBeanServer, this.objectNameFactory, + this.responseMapper, this.endpoints); + } + + @Test + public void createWhenMBeanServerIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("MBeanServer must not be null"); + new JmxEndpointExporter(null, this.objectNameFactory, this.responseMapper, + this.endpoints); + } + + @Test + public void createWhenObjectNameFactoryIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ObjectNameFactory must not be null"); + new JmxEndpointExporter(this.mBeanServer, null, this.responseMapper, + this.endpoints); + } + + @Test + public void createWhenResponseMapperIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ResponseMapper must not be null"); + new JmxEndpointExporter(this.mBeanServer, this.objectNameFactory, null, + this.endpoints); + } + + @Test + public void createWhenEndpointsIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Endpoints must not be null"); + new JmxEndpointExporter(this.mBeanServer, this.objectNameFactory, + this.responseMapper, null); + } + + @Test + public void afterPropertiesSetShouldRegisterMBeans() throws Exception { + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.exporter.afterPropertiesSet(); + verify(this.mBeanServer).registerMBean(this.objectCaptor.capture(), + this.objectNameCaptor.capture()); + assertThat(this.objectCaptor.getValue()).isInstanceOf(EndpointMBean.class); + assertThat(this.objectNameCaptor.getValue().getKeyProperty("name")) + .isEqualTo("test"); + } + + @Test + public void registerShouldUseObjectNameFactory() throws Exception { + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.exporter.afterPropertiesSet(); + verify(this.objectNameFactory).getObjectName(any(ExposableJmxEndpoint.class)); + } + + @Test + public void registerWhenObjectNameIsMalformedShouldThrowException() throws Exception { + given(this.objectNameFactory.getObjectName(any(ExposableJmxEndpoint.class))) + .willThrow(MalformedObjectNameException.class); + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Invalid ObjectName for endpoint 'test'"); + this.exporter.afterPropertiesSet(); + } + + @Test + public void registerWhenRegistrationFailsShouldThrowException() throws Exception { + given(this.mBeanServer.registerMBean(any(), any(ObjectName.class))) + .willThrow(new MBeanRegistrationException(new RuntimeException())); + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.thrown.expect(MBeanExportException.class); + this.thrown.expectMessage("Failed to register MBean for endpoint 'test"); + this.exporter.afterPropertiesSet(); + } + + @Test + public void destroyShouldUnregisterMBeans() throws Exception { + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.exporter.afterPropertiesSet(); + this.exporter.destroy(); + verify(this.mBeanServer).unregisterMBean(this.objectNameCaptor.capture()); + assertThat(this.objectNameCaptor.getValue().getKeyProperty("name")) + .isEqualTo("test"); + } + + @Test + public void unregisterWhenInstanceNotFoundShouldContinue() throws Exception { + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.exporter.afterPropertiesSet(); + willThrow(InstanceNotFoundException.class).given(this.mBeanServer) + .unregisterMBean(any(ObjectName.class)); + this.exporter.destroy(); + } + + @Test + public void unregisterWhenUnregisterThrowsExceptionShouldThrowException() + throws Exception { + this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); + this.exporter.afterPropertiesSet(); + willThrow(new MBeanRegistrationException(new RuntimeException())) + .given(this.mBeanServer).unregisterMBean(any(ObjectName.class)); + this.thrown.expect(JmxException.class); + this.thrown.expectMessage("Failed to unregister MBean with ObjectName 'boot"); + this.exporter.destroy(); + } + + /** + * Test {@link EndpointObjectNameFactory}. + */ + private static class TestEndpointObjectNameFactory + implements EndpointObjectNameFactory { + + @Override + public ObjectName getObjectName(ExposableJmxEndpoint endpoint) + throws MalformedObjectNameException { + return (endpoint == null ? null + : new ObjectName("boot:type=Endpoint,name=" + endpoint.getId())); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactoryTests.java new file mode 100644 index 0000000000..12c48f0ade --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/MBeanInfoFactoryTests.java @@ -0,0 +1,128 @@ +/* + * 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.ArrayList; +import java.util.List; + +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; + +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.OperationType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MBeanInfoFactory}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class MBeanInfoFactoryTests { + + private MBeanInfoFactory factory = new MBeanInfoFactory( + new TestJmxOperationResponseMapper()); + + @Test + public void getMBeanInfoShouldReturnMBeanInfo() { + MBeanInfo info = this.factory + .getMBeanInfo(new TestExposableJmxEndpoint(new TestJmxOperation())); + assertThat(info).isNotNull(); + assertThat(info.getClassName()).isEqualTo(EndpointMBean.class.getName()); + assertThat(info.getDescription()).isEqualTo("MBean operations for endpoint test"); + assertThat(info.getAttributes()).isEmpty(); + assertThat(info.getNotifications()).isEmpty(); + assertThat(info.getConstructors()).isEmpty(); + assertThat(info.getOperations()).hasSize(1); + MBeanOperationInfo operationInfo = info.getOperations()[0]; + assertThat(operationInfo.getName()).isEqualTo("testOperation"); + assertThat(operationInfo.getReturnType()).isEqualTo(String.class.getName()); + assertThat(operationInfo.getImpact()).isEqualTo(MBeanOperationInfo.INFO); + assertThat(operationInfo.getSignature()).hasSize(0); + } + + @Test + public void getMBeanInfoWhenReadOperationShouldHaveInfoImpact() { + MBeanInfo info = this.factory.getMBeanInfo( + new TestExposableJmxEndpoint(new TestJmxOperation(OperationType.READ))); + assertThat(info.getOperations()[0].getImpact()) + .isEqualTo(MBeanOperationInfo.INFO); + } + + @Test + public void getMBeanInfoWhenWriteOperationShouldHaveActionImpact() { + MBeanInfo info = this.factory.getMBeanInfo( + new TestExposableJmxEndpoint(new TestJmxOperation(OperationType.WRITE))); + assertThat(info.getOperations()[0].getImpact()) + .isEqualTo(MBeanOperationInfo.ACTION); + } + + @Test + public void getMBeanInfoWhenDeleteOperationShouldHaveActionImpact() { + MBeanInfo info = this.factory.getMBeanInfo( + new TestExposableJmxEndpoint(new TestJmxOperation(OperationType.DELETE))); + assertThat(info.getOperations()[0].getImpact()) + .isEqualTo(MBeanOperationInfo.ACTION); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void getMBeanInfoShouldUseJmxOperationResponseMapper() { + JmxOperationResponseMapper mapper = mock(JmxOperationResponseMapper.class); + given(mapper.mapResponseType(String.class)).willReturn((Class) Integer.class); + MBeanInfoFactory factory = new MBeanInfoFactory(mapper); + MBeanInfo info = factory + .getMBeanInfo(new TestExposableJmxEndpoint(new TestJmxOperation())); + MBeanOperationInfo operationInfo = info.getOperations()[0]; + assertThat(operationInfo.getReturnType()).isEqualTo(Integer.class.getName()); + } + + @Test + public void getMBeanShouldMapOperationParameters() { + List parameters = new ArrayList<>(); + parameters.add(mockParameter("one", String.class, "myone")); + parameters.add(mockParameter("two", Object.class, null)); + TestJmxOperation operation = new TestJmxOperation(parameters); + MBeanInfo info = this.factory + .getMBeanInfo(new TestExposableJmxEndpoint(operation)); + MBeanOperationInfo operationInfo = info.getOperations()[0]; + MBeanParameterInfo[] signature = operationInfo.getSignature(); + assertThat(signature).hasSize(2); + assertThat(signature[0].getName()).isEqualTo("one"); + assertThat(signature[0].getType()).isEqualTo(String.class.getName()); + assertThat(signature[0].getDescription()).isEqualTo("myone"); + assertThat(signature[1].getName()).isEqualTo("two"); + assertThat(signature[1].getType()).isEqualTo(Object.class.getName()); + assertThat(signature[1].getDescription()).isNull(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private JmxOperationParameter mockParameter(String name, Class type, + String description) { + JmxOperationParameter parameter = mock(JmxOperationParameter.class); + given(parameter.getName()).willReturn(name); + given(parameter.getType()).willReturn((Class) type); + given(parameter.getDescription()).willReturn(description); + return parameter; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestExposableJmxEndpoint.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestExposableJmxEndpoint.java new file mode 100644 index 0000000000..1f20bcdab6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestExposableJmxEndpoint.java @@ -0,0 +1,54 @@ +/* + * 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.Arrays; +import java.util.Collection; + +/** + * Test {@link ExposableJmxEndpoint} implementation. + * + * @author Phillip Webb + */ +public class TestExposableJmxEndpoint implements ExposableJmxEndpoint { + + private final Collection operations; + + public TestExposableJmxEndpoint(JmxOperation... operations) { + this(Arrays.asList(operations)); + } + + public TestExposableJmxEndpoint(Collection operations) { + this.operations = operations; + } + + @Override + public String getId() { + return "test"; + } + + @Override + public boolean isEnableByDefault() { + return true; + } + + @Override + public Collection getOperations() { + return this.operations; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperation.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperation.java new file mode 100644 index 0000000000..56c387cd71 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperation.java @@ -0,0 +1,93 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.boot.actuate.endpoint.OperationType; + +/** + * Test {@link JmxOperation} implementation. + * + * @author Phillip Webb + */ +public class TestJmxOperation implements JmxOperation { + + private final OperationType operationType; + + private final Function, Object> invoke; + + private final List parameters; + + public TestJmxOperation() { + this.operationType = OperationType.READ; + this.invoke = null; + this.parameters = Collections.emptyList(); + } + + public TestJmxOperation(OperationType operationType) { + this.operationType = operationType; + this.invoke = null; + this.parameters = Collections.emptyList(); + } + + public TestJmxOperation(Function, Object> invoke) { + this.operationType = OperationType.READ; + this.invoke = invoke; + this.parameters = Collections.emptyList(); + } + + public TestJmxOperation(List parameters) { + this.operationType = OperationType.READ; + this.invoke = null; + this.parameters = parameters; + } + + @Override + public OperationType getType() { + return this.operationType; + } + + @Override + public Object invoke(Map arguments) { + return (this.invoke == null ? "result" : this.invoke.apply(arguments)); + } + + @Override + public String getName() { + return "testOperation"; + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public String getDescription() { + return "Test JMX operation"; + } + + @Override + public List getParameters() { + return this.parameters; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperationResponseMapper.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperationResponseMapper.java new file mode 100644 index 0000000000..cb06ec9ed9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/TestJmxOperationResponseMapper.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Test {@link JmxOperationResponseMapper} implementation. + * + * @author Stephane Nicoll + */ +class TestJmxOperationResponseMapper implements JmxOperationResponseMapper { + + @Override + public Object mapResponse(Object response) { + return response; + } + + @Override + public Class mapResponseType(Class responseType) { + return responseType; + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperationTests.java new file mode 100644 index 0000000000..107432b399 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/DiscoveredJmxOperationTests.java @@ -0,0 +1,168 @@ +/* + * 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.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; +import org.springframework.boot.actuate.endpoint.jmx.JmxOperationParameter; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedOperationParameter; +import org.springframework.jmx.export.annotation.ManagedOperationParameters; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DiscoveredJmxOperation}. + * + * @author Phillip Webb + */ +public class DiscoveredJmxOperationTests { + + @Test + public void getNameShouldReturnMethodName() { + DiscoveredJmxOperation operation = getOperation("getEnum"); + assertThat(operation.getName()).isEqualTo("getEnum"); + } + + @Test + public void getOutputTypeShouldReturnJmxType() { + assertThat(getOperation("getEnum").getOutputType()).isEqualTo(String.class); + assertThat(getOperation("getDate").getOutputType()).isEqualTo(String.class); + assertThat(getOperation("getInstant").getOutputType()).isEqualTo(String.class); + assertThat(getOperation("getInteger").getOutputType()).isEqualTo(Integer.class); + assertThat(getOperation("getVoid").getOutputType()).isEqualTo(void.class); + assertThat(getOperation("getApplicationContext").getOutputType()) + .isEqualTo(Object.class); + } + + @Test + public void getDescriptionWhenHasManagedOperationDescriptionShouldUseValueFromAnnotation() { + DiscoveredJmxOperation operation = getOperation( + "withManagedOperationDescription"); + assertThat(operation.getDescription()).isEqualTo("fromannotation"); + } + + @Test + public void getDescriptionWhenHasNoManagedOperationShouldGenerateDescription() { + DiscoveredJmxOperation operation = getOperation("getEnum"); + assertThat(operation.getDescription()) + .isEqualTo("Invoke getEnum for endpoint test"); + } + + @Test + public void getParametersWhenHasNoParametersShouldReturnEmptyList() { + DiscoveredJmxOperation operation = getOperation("getEnum"); + assertThat(operation.getParameters()).isEmpty(); + } + + @Test + public void getParametersShouldReturnJmxTypes() { + DiscoveredJmxOperation operation = getOperation("params"); + List parameters = operation.getParameters(); + assertThat(parameters.get(0).getType()).isEqualTo(String.class); + assertThat(parameters.get(1).getType()).isEqualTo(String.class); + assertThat(parameters.get(2).getType()).isEqualTo(String.class); + assertThat(parameters.get(3).getType()).isEqualTo(Integer.class); + assertThat(parameters.get(4).getType()).isEqualTo(Object.class); + } + + @Test + public void getParametersWhenHasManagedOperationParameterShouldUseValuesFromAnnotation() { + DiscoveredJmxOperation operation = getOperation("withManagedOperationParameters"); + List parameters = operation.getParameters(); + assertThat(parameters.get(0).getName()).isEqualTo("a1"); + assertThat(parameters.get(1).getName()).isEqualTo("a2"); + assertThat(parameters.get(0).getDescription()).isEqualTo("d1"); + assertThat(parameters.get(1).getDescription()).isEqualTo("d2"); + } + + @Test + public void getParametersWhenHasNoManagedOperationParameterShouldDeducedValuesName() { + DiscoveredJmxOperation operation = getOperation("params"); + List parameters = operation.getParameters(); + assertThat(parameters.get(0).getName()).isEqualTo("enumParam"); + assertThat(parameters.get(1).getName()).isEqualTo("dateParam"); + assertThat(parameters.get(2).getName()).isEqualTo("instantParam"); + assertThat(parameters.get(3).getName()).isEqualTo("integerParam"); + assertThat(parameters.get(4).getName()).isEqualTo("applicationContextParam"); + assertThat(parameters.get(0).getDescription()).isNull(); + assertThat(parameters.get(1).getDescription()).isNull(); + assertThat(parameters.get(2).getDescription()).isNull(); + assertThat(parameters.get(3).getDescription()).isNull(); + assertThat(parameters.get(4).getDescription()).isNull(); + } + + private DiscoveredJmxOperation getOperation(String methodName) { + Method method = findMethod(methodName); + AnnotationAttributes annotationAttributes = new AnnotationAttributes(); + annotationAttributes.put("produces", "application/xml"); + DiscoveredOperationMethod operationMethod = new DiscoveredOperationMethod(method, + OperationType.READ, annotationAttributes); + DiscoveredJmxOperation operation = new DiscoveredJmxOperation("test", + operationMethod, mock(OperationInvoker.class)); + return operation; + } + + private Method findMethod(String methodName) { + Map methods = new HashMap<>(); + ReflectionUtils.doWithMethods(Example.class, + (method) -> methods.put(method.getName(), method)); + return methods.get(methodName); + } + + interface Example { + + OperationType getEnum(); + + Date getDate(); + + Instant getInstant(); + + Integer getInteger(); + + void getVoid(); + + ApplicationContext getApplicationContext(); + + Object params(OperationType enumParam, Date dateParam, Instant instantParam, + Integer integerParam, ApplicationContext applicationContextParam); + + @ManagedOperation(description = "fromannotation") + Object withManagedOperationDescription(); + + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "a1", description = "d1"), + @ManagedOperationParameter(name = "a2", description = "d2") }) + Object withManagedOperationParameters(Object one, Object two); + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java similarity index 70% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscovererTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java index 954b5ee0a0..9b92b4c28e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxAnnotationEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java @@ -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,16 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; -import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker; -import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvokerAdvisor; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; -import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointOperationParameterInfo; +import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvoker; +import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; +import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint; import org.springframework.boot.actuate.endpoint.jmx.JmxOperation; -import org.springframework.boot.actuate.endpoint.reflect.ReflectiveOperationInvoker; +import org.springframework.boot.actuate.endpoint.jmx.JmxOperationParameter; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -47,30 +46,31 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedOperationParameter; import org.springframework.jmx.export.annotation.ManagedOperationParameters; -import org.springframework.util.ReflectionUtils; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link JmxAnnotationEndpointDiscoverer}. + * Tests for {@link JmxEndpointDiscoverer}. * * @author Stephane Nicoll + * @author Phillip Webb */ -public class JmxAnnotationEndpointDiscovererTests { +public class JmxEndpointDiscovererTests { @Rule public final ExpectedException thrown = ExpectedException.none(); @Test - public void discoveryWorksWhenThereAreNoEndpoints() { + public void getEndpointsWhenNoEndpointBeansShouldReturnEmptyCollection() { load(EmptyConfiguration.class, - (discoverer) -> assertThat(discoverer.discoverEndpoints()).isEmpty()); + (discoverer) -> assertThat(discoverer.getEndpoints()).isEmpty()); } @Test - public void standardEndpointIsDiscovered() { + public void getEndpointsShouldDiscoverStandardEndpoints() { load(TestEndpoint.class, (discoverer) -> { - Map> endpoints = discover(discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test"); Map operationByName = mapOperations( endpoints.get("test").getOperations()); @@ -81,8 +81,6 @@ public class JmxAnnotationEndpointDiscovererTests { .isEqualTo("Invoke getAll for endpoint test"); assertThat(getAll.getOutputType()).isEqualTo(Object.class); assertThat(getAll.getParameters()).isEmpty(); - assertThat(getAll.getInvoker()) - .isInstanceOf(ReflectiveOperationInvoker.class); JmxOperation getSomething = operationByName.get("getSomething"); assertThat(getSomething.getDescription()) .isEqualTo("Invoke getSomething for endpoint test"); @@ -103,42 +101,41 @@ public class JmxAnnotationEndpointDiscovererTests { assertThat(deleteSomething.getParameters()).hasSize(1); hasDefaultParameter(deleteSomething, 0, String.class); }); - } @Test - public void onlyJmxEndpointsAreDiscovered() { + public void getEndpointsWhenHasFilteredEndpointShouldOnlyDiscoverJmxEndpoints() { load(MultipleEndpointsConfiguration.class, (discoverer) -> { - Map> endpoints = discover(discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test", "jmx"); }); } @Test - public void jmxExtensionMustHaveEndpoint() { + public void getEndpointsWhenJmxExtensionIsMissingEndpointShouldThrowException() { load(TestJmxEndpointExtension.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Invalid extension"); - this.thrown.expectMessage(TestJmxEndpointExtension.class.getName()); - this.thrown.expectMessage("no endpoint found"); - this.thrown.expectMessage(TestEndpoint.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage( + "Invalid extension 'jmxEndpointDiscovererTests.TestJmxEndpointExtension': " + + "no endpoint found with type '" + + TestEndpoint.class.getName() + "'"); + discoverer.getEndpoints(); }); } @Test - public void jmxEndpointOverridesStandardEndpoint() { + public void getEndpointsWhenHasJmxExtensionShouldOverrideStandardEndpoint() { load(OverriddenOperationJmxEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = discover(discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test"); assertJmxTestEndpoint(endpoints.get("test")); }); } @Test - public void jmxEndpointAddsExtraOperation() { + public void getEndpointsWhenHasJmxExtensionWithNewOperationAddsExtraOperation() { load(AdditionalOperationJmxEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = discover(discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test"); Map operationByName = mapOperations( endpoints.get("test").getOperations()); @@ -152,131 +149,120 @@ public class JmxAnnotationEndpointDiscovererTests { } @Test - public void endpointMainReadOperationIsCachedWithMatchingId() { + public void getEndpointsWhenHasCacheWithTtlShouldCacheReadOperationWithTtlValue() { load(TestEndpoint.class, (id) -> 500L, (discoverer) -> { - Map> endpoints = discover(discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test"); Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update", "deleteSomething"); JmxOperation getAll = operationByName.get("getAll"); - assertThat(getAll.getInvoker()).isInstanceOf(CachingOperationInvoker.class); - assertThat(((CachingOperationInvoker) getAll.getInvoker()).getTimeToLive()) + assertThat(getInvoker(getAll)).isInstanceOf(CachingOperationInvoker.class); + assertThat(((CachingOperationInvoker) getInvoker(getAll)).getTimeToLive()) .isEqualTo(500); }); } @Test - public void extraReadOperationsAreCached() { + public void getEndpointsShouldCacheReadOperations() { load(AdditionalOperationJmxEndpointConfiguration.class, (id) -> 500L, (discoverer) -> { - Map> endpoints = discover( - discoverer); + Map endpoints = discover(discoverer); assertThat(endpoints).containsOnlyKeys("test"); Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update", "deleteSomething", "getAnother"); JmxOperation getAll = operationByName.get("getAll"); - assertThat(getAll.getInvoker()) + assertThat(getInvoker(getAll)) .isInstanceOf(CachingOperationInvoker.class); - assertThat(((CachingOperationInvoker) getAll.getInvoker()) + assertThat(((CachingOperationInvoker) getInvoker(getAll)) .getTimeToLive()).isEqualTo(500); JmxOperation getAnother = operationByName.get("getAnother"); - assertThat(getAnother.getInvoker()) + assertThat(getInvoker(getAnother)) .isInstanceOf(CachingOperationInvoker.class); - assertThat(((CachingOperationInvoker) getAnother.getInvoker()) + assertThat(((CachingOperationInvoker) getInvoker(getAnother)) .getTimeToLive()).isEqualTo(500); }); } @Test - public void discoveryFailsWhenTwoExtensionsHaveTheSameEndpointType() { + public void getEndpointsWhenTwoExtensionsHaveTheSameEndpointTypeShouldThrowException() { load(ClashingJmxEndpointConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Found two extensions for the same endpoint"); - this.thrown.expectMessage(TestEndpoint.class.getName()); - this.thrown.expectMessage(TestJmxEndpointExtension.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Found multiple extensions for the endpoint bean " + + "testEndpoint (testExtensionOne, testExtensionTwo)"); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenTwoStandardEndpointsHaveTheSameId() { + public void getEndpointsWhenTwoStandardEndpointsHaveTheSameIdShouldThrowException() { load(ClashingStandardEndpointConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Found two endpoints with the id 'test': "); - discoverer.discoverEndpoints(); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenEndpointHasTwoOperationsWithTheSameName() { + public void getEndpointsWhenWhenEndpointHasTwoOperationsWithTheSameNameShouldThrowException() { load(ClashingOperationsEndpoint.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Found multiple JMX operations with the same name"); - this.thrown.expectMessage("getAll"); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsEndpoint.class, "getAll").toString()); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsEndpoint.class, "getAll", String.class) - .toString()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Unable to map duplicate endpoint operations: " + + "[MBean call 'getAll'] to jmxEndpointDiscovererTests.ClashingOperationsEndpoint"); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenExtensionHasTwoOperationsWithTheSameName() { + public void getEndpointsWhenWhenExtensionHasTwoOperationsWithTheSameNameShouldThrowException() { load(AdditionalClashingOperationsConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Found multiple JMX operations with the same name"); - this.thrown.expectMessage("getAll"); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsJmxEndpointExtension.class, "getAll") - .toString()); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsJmxEndpointExtension.class, "getAll", - String.class) - .toString()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Unable to map duplicate endpoint operations: " + + "[MBean call 'getAll'] to testEndpoint (clashingOperationsJmxEndpointExtension)"); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenExtensionIsNotCompatibleWithTheEndpointType() { + public void getEndpointsWhenExtensionIsNotCompatibleWithTheEndpointTypeShouldThrowException() { load(InvalidJmxExtensionConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Invalid extension"); - this.thrown.expectMessage(NonJmxJmxEndpointExtension.class.getName()); - this.thrown.expectMessage(NonJmxEndpoint.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Endpoint bean 'nonJmxEndpoint' cannot support the " + + "extension bean 'nonJmxJmxEndpointExtension'"); + discoverer.getEndpoints(); }); } - private void assertJmxTestEndpoint(EndpointInfo endpoint) { - Map operationByName = mapOperations( + private Object getInvoker(JmxOperation operation) { + return ReflectionTestUtils.getField(operation, "invoker"); + } + + private void assertJmxTestEndpoint(ExposableJmxEndpoint endpoint) { + Map operationsByName = mapOperations( endpoint.getOperations()); - assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update", + assertThat(operationsByName).containsOnlyKeys("getAll", "getSomething", "update", "deleteSomething"); - JmxOperation getAll = operationByName.get("getAll"); + JmxOperation getAll = operationsByName.get("getAll"); assertThat(getAll.getDescription()).isEqualTo("Get all the things"); assertThat(getAll.getOutputType()).isEqualTo(Object.class); assertThat(getAll.getParameters()).isEmpty(); - JmxOperation getSomething = operationByName.get("getSomething"); + JmxOperation getSomething = operationsByName.get("getSomething"); assertThat(getSomething.getDescription()) .isEqualTo("Get something based on a timeUnit"); assertThat(getSomething.getOutputType()).isEqualTo(String.class); assertThat(getSomething.getParameters()).hasSize(1); hasDocumentedParameter(getSomething, 0, "unitMs", Long.class, "Number of milliseconds"); - JmxOperation update = operationByName.get("update"); + JmxOperation update = operationsByName.get("update"); assertThat(update.getDescription()).isEqualTo("Update something based on bar"); assertThat(update.getOutputType()).isEqualTo(Void.TYPE); assertThat(update.getParameters()).hasSize(2); hasDocumentedParameter(update, 0, "foo", String.class, "Foo identifier"); hasDocumentedParameter(update, 1, "bar", String.class, "Bar value"); - JmxOperation deleteSomething = operationByName.get("deleteSomething"); + JmxOperation deleteSomething = operationsByName.get("deleteSomething"); assertThat(deleteSomething.getDescription()) .isEqualTo("Delete something based on a timeUnit"); assertThat(deleteSomething.getOutputType()).isEqualTo(Void.TYPE); @@ -285,58 +271,172 @@ public class JmxAnnotationEndpointDiscovererTests { "Number of milliseconds"); } - private void hasDefaultParameter(JmxOperation operation, int index, Class type) { - assertThat(index).isLessThan(operation.getParameters().size()); - JmxEndpointOperationParameterInfo parameter = operation.getParameters() - .get(index); - assertThat(parameter.getType()).isEqualTo(type); - assertThat(parameter.getDescription()).isNull(); - } - private void hasDocumentedParameter(JmxOperation operation, int index, String name, Class type, String description) { assertThat(index).isLessThan(operation.getParameters().size()); - JmxEndpointOperationParameterInfo parameter = operation.getParameters() - .get(index); + JmxOperationParameter parameter = operation.getParameters().get(index); assertThat(parameter.getName()).isEqualTo(name); assertThat(parameter.getType()).isEqualTo(type); assertThat(parameter.getDescription()).isEqualTo(description); } - private Map> discover( - JmxAnnotationEndpointDiscoverer discoverer) { - Map> endpointsById = new HashMap<>(); - discoverer.discoverEndpoints() - .forEach((endpoint) -> endpointsById.put(endpoint.getId(), endpoint)); - return endpointsById; + // FIXME rename + private void hasDefaultParameter(JmxOperation operation, int index, Class type) { + JmxOperationParameter parameter = operation.getParameters().get(index); + assertThat(parameter.getType()).isEqualTo(type); + } + + private Map discover(JmxEndpointDiscoverer discoverer) { + Map byId = new HashMap<>(); + discoverer.getEndpoints() + .forEach((endpoint) -> byId.put(endpoint.getId(), endpoint)); + return byId; } private Map mapOperations(Collection operations) { - Map operationByName = new HashMap<>(); - operations.forEach((operation) -> operationByName - .put(operation.getOperationName(), operation)); - return operationByName; + Map byName = new HashMap<>(); + operations.forEach((operation) -> byName.put(operation.getName(), operation)); + return byName; } - private void load(Class configuration, - Consumer consumer) { + private void load(Class configuration, Consumer consumer) { load(configuration, (id) -> null, consumer); } private void load(Class configuration, Function timeToLive, - Consumer consumer) { + Consumer consumer) { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( configuration)) { - ConversionServiceParameterMapper parameterMapper = new ConversionServiceParameterMapper( + ConversionServiceParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); - JmxAnnotationEndpointDiscoverer discoverer = new JmxAnnotationEndpointDiscoverer( - context, parameterMapper, + JmxEndpointDiscoverer discoverer = new JmxEndpointDiscoverer(context, + parameterMapper, Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), - null); + Collections.emptyList()); consumer.accept(discoverer); } } + @Configuration + static class EmptyConfiguration { + + } + + @Configuration + static class MultipleEndpointsConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + @Bean + public TestJmxEndpoint testJmxEndpoint() { + return new TestJmxEndpoint(); + } + + @Bean + public NonJmxEndpoint nonJmxEndpoint() { + return new NonJmxEndpoint(); + } + + } + + @Configuration + static class OverriddenOperationJmxEndpointConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + @Bean + public TestJmxEndpointExtension testJmxEndpointExtension() { + return new TestJmxEndpointExtension(); + } + + } + + @Configuration + static class AdditionalOperationJmxEndpointConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + @Bean + public AdditionalOperationJmxEndpointExtension additionalOperationJmxEndpointExtension() { + return new AdditionalOperationJmxEndpointExtension(); + } + + } + + @Configuration + static class AdditionalClashingOperationsConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + @Bean + public ClashingOperationsJmxEndpointExtension clashingOperationsJmxEndpointExtension() { + return new ClashingOperationsJmxEndpointExtension(); + } + + } + + @Configuration + static class ClashingJmxEndpointConfiguration { + + @Bean + public TestEndpoint testEndpoint() { + return new TestEndpoint(); + } + + @Bean + public TestJmxEndpointExtension testExtensionOne() { + return new TestJmxEndpointExtension(); + } + + @Bean + public TestJmxEndpointExtension testExtensionTwo() { + return new TestJmxEndpointExtension(); + } + + } + + @Configuration + static class ClashingStandardEndpointConfiguration { + + @Bean + public TestEndpoint testEndpointTwo() { + return new TestEndpoint(); + } + + @Bean + public TestEndpoint testEndpointOne() { + return new TestEndpoint(); + } + + } + + @Configuration + static class InvalidJmxExtensionConfiguration { + + @Bean + public NonJmxEndpoint nonJmxEndpoint() { + return new NonJmxEndpoint(); + } + + @Bean + public NonJmxJmxEndpointExtension nonJmxJmxEndpointExtension() { + return new NonJmxJmxEndpointExtension(); + } + + } + @Endpoint(id = "test") private static class TestEndpoint { @@ -469,124 +569,4 @@ public class JmxAnnotationEndpointDiscovererTests { } - @Configuration - static class EmptyConfiguration { - - } - - @Configuration - static class MultipleEndpointsConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - @Bean - public TestJmxEndpoint testJmxEndpoint() { - return new TestJmxEndpoint(); - } - - @Bean - public NonJmxEndpoint nonJmxEndpoint() { - return new NonJmxEndpoint(); - } - - } - - @Configuration - static class OverriddenOperationJmxEndpointConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - @Bean - public TestJmxEndpointExtension testJmxEndpointExtension() { - return new TestJmxEndpointExtension(); - } - - } - - @Configuration - static class AdditionalOperationJmxEndpointConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - @Bean - public AdditionalOperationJmxEndpointExtension additionalOperationJmxEndpointExtension() { - return new AdditionalOperationJmxEndpointExtension(); - } - - } - - @Configuration - static class AdditionalClashingOperationsConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - @Bean - public ClashingOperationsJmxEndpointExtension clashingOperationsJmxEndpointExtension() { - return new ClashingOperationsJmxEndpointExtension(); - } - - } - - @Configuration - static class ClashingJmxEndpointConfiguration { - - @Bean - public TestEndpoint testEndpoint() { - return new TestEndpoint(); - } - - @Bean - public TestJmxEndpointExtension testExtensionOne() { - return new TestJmxEndpointExtension(); - } - - @Bean - public TestJmxEndpointExtension testExtensionTwo() { - return new TestJmxEndpointExtension(); - } - - } - - @Configuration - static class ClashingStandardEndpointConfiguration { - - @Bean - public TestEndpoint testEndpointTwo() { - return new TestEndpoint(); - } - - @Bean - public TestEndpoint testEndpointOne() { - return new TestEndpoint(); - } - - } - - @Configuration - static class InvalidJmxExtensionConfiguration { - - @Bean - public NonJmxEndpoint nonJmxEndpoint() { - return new NonJmxEndpoint(); - } - - @Bean - public NonJmxJmxEndpointExtension nonJmxJmxEndpointExtension() { - return new NonJmxJmxEndpointExtension(); - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java index 6b4395389b..48826753b3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java @@ -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,17 +16,19 @@ package org.springframework.boot.actuate.endpoint.web; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import org.assertj.core.api.Condition; import org.junit.Test; -import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.OperationType; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link EndpointLinksResolver}. @@ -57,13 +59,16 @@ public class EndpointLinksResolverTests { @Test public void resolvedLinksContainsALinkForEachEndpointOperation() { + List operations = new ArrayList<>(); + operations.add(operationWithPath("/alpha", "alpha")); + operations.add(operationWithPath("/alpha/{name}", "alpha-name")); + ExposableWebEndpoint endpoint = mock(ExposableWebEndpoint.class); + given(endpoint.getId()).willReturn("alpha"); + given(endpoint.isEnableByDefault()).willReturn(true); + given(endpoint.getOperations()).willReturn(operations); + String requestUrl = "https://api.example.com/actuator"; Map links = this.linksResolver - .resolveLinks( - Arrays.asList(new EndpointInfo<>("alpha", true, - Arrays.asList(operationWithPath("/alpha", "alpha"), - operationWithPath("/alpha/{name}", - "alpha-name")))), - "https://api.example.com/actuator"); + .resolveLinks(Collections.singletonList(endpoint), requestUrl); assertThat(links).hasSize(3); assertThat(links).hasEntrySatisfying("self", linkWithHref("https://api.example.com/actuator")); @@ -74,10 +79,14 @@ public class EndpointLinksResolverTests { } private WebOperation operationWithPath(String path, String id) { - return new WebOperation(OperationType.READ, null, false, - new OperationRequestPredicate(path, WebEndpointHttpMethod.GET, - Collections.emptyList(), Collections.emptyList()), - id); + WebOperationRequestPredicate predicate = new WebOperationRequestPredicate(path, + WebEndpointHttpMethod.GET, Collections.emptyList(), + Collections.emptyList()); + WebOperation operation = mock(WebOperation.class); + given(operation.getId()).willReturn(id); + given(operation.getType()).willReturn(OperationType.READ); + given(operation.getRequestPredicate()).willReturn(predicate); + return operation; } private Condition linkWithHref(String href) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java new file mode 100644 index 0000000000..b36640143b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java @@ -0,0 +1,69 @@ +/* + * 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 java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EndpointMediaTypes}. + * + * @author Phillip Webb + */ +public class EndpointMediaTypesTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenProducedIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Produced must not be null"); + new EndpointMediaTypes(null, Collections.emptyList()); + } + + @Test + public void createWhenConsumedIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Consumed must not be null"); + new EndpointMediaTypes(Collections.emptyList(), null); + } + + @Test + public void getProducedShouldReturnProduced() { + List produced = Arrays.asList("a", "b", "c"); + EndpointMediaTypes types = new EndpointMediaTypes(produced, + Collections.emptyList()); + assertThat(types.getProduced()).isEqualTo(produced); + } + + @Test + public void getConsumedShouldReturnConsumed() { + List consumed = Arrays.asList("a", "b", "c"); + EndpointMediaTypes types = new EndpointMediaTypes(Collections.emptyList(), + consumed); + assertThat(types.getConsumed()).isEqualTo(consumed); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/LinkTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/LinkTests.java new file mode 100644 index 0000000000..50ce59971d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/LinkTests.java @@ -0,0 +1,63 @@ +/* + * 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.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Link}. + * + * @author Phillip Webb + */ +public class LinkTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenHrefIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("HREF must not be null"); + new Link(null); + } + + @Test + public void getHrefShouldReturnHref() { + String href = "http://example.com"; + Link link = new Link(href); + assertThat(link.getHref()).isEqualTo(href); + } + + @Test + public void isTemplatedWhenContainsPlaceholderShouldReturnTrue() { + String href = "http://example.com/{path}"; + Link link = new Link(href); + assertThat(link.isTemplated()).isTrue(); + } + + @Test + public void isTemplatedWhenContainsNoPlaceholderShouldReturnFalse() { + String href = "http://example.com/path"; + Link link = new Link(href); + assertThat(link.isTemplated()).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponseTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponseTests.java new file mode 100644 index 0000000000..5fa5441507 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponseTests.java @@ -0,0 +1,58 @@ +/* + * 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.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebEndpointResponse}. + * + * @author Phillip Webb + */ +public class WebEndpointResponseTests { + + @Test + public void createWithNoParamsShouldReturn200() { + WebEndpointResponse response = new WebEndpointResponse<>(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getBody()).isNull(); + } + + @Test + public void createWithStatusShouldReturnStatus() { + WebEndpointResponse response = new WebEndpointResponse<>(404); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getBody()).isNull(); + } + + @Test + public void createWithBodyShouldReturnBody() { + WebEndpointResponse response = new WebEndpointResponse<>("body"); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getBody()).isEqualTo("body"); + } + + @Test + public void createWhithBodyAndStatusShouldReturnStatusAndBody() { + WebEndpointResponse response = new WebEndpointResponse<>("body", 500); + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.getBody()).isEqualTo("body"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicateTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java similarity index 86% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicateTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java index 4a0e02dc99..73f3119303 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/OperationRequestPredicateTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java @@ -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,11 +23,11 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link OperationRequestPredicate}. + * Tests for {@link WebOperationRequestPredicate}. * * @author Andy Wilkinson */ -public class OperationRequestPredicateTests { +public class WebOperationRequestPredicateTests { @Test public void predicatesWithIdenticalPathsAreEqual() { @@ -63,8 +63,8 @@ public class OperationRequestPredicateTests { .isEqualTo(predicateWithPath("/path/{foo2}/more/{bar2}")); } - private OperationRequestPredicate predicateWithPath(String path) { - return new OperationRequestPredicate(path, WebEndpointHttpMethod.GET, + private WebOperationRequestPredicate predicateWithPath(String path) { + return new WebOperationRequestPredicate(path, WebEndpointHttpMethod.GET, Collections.emptyList(), Collections.emptyList()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java similarity index 91% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index 2563b1906c..1bce93ac22 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.actuate.endpoint.web; +package org.springframework.boot.actuate.endpoint.web.annotation; import java.net.InetSocketAddress; import java.time.Duration; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -29,22 +28,17 @@ import java.util.function.Consumer; import org.junit.Test; import reactor.core.publisher.Mono; -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; 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.web.annotation.WebAnnotationEndpointDiscoverer; -import org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.MapPropertySource; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -53,7 +47,6 @@ import org.springframework.lang.Nullable; import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** @@ -362,47 +355,6 @@ public abstract class AbstractWebEndpointIntegrationTests clientConsumer.accept(client)); } - @Configuration - static class BaseConfiguration { - - @Bean - public EndpointDelegate endpointDelegate() { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - if (classLoader instanceof TomcatEmbeddedWebappClassLoader) { - Thread.currentThread().setContextClassLoader(classLoader.getParent()); - } - try { - return mock(EndpointDelegate.class); - } - finally { - Thread.currentThread().setContextClassLoader(classLoader); - } - } - - @Bean - public EndpointMediaTypes endpointMediaTypes() { - List mediaTypes = Arrays.asList("application/vnd.test+json", - "application/json"); - return new EndpointMediaTypes(mediaTypes, mediaTypes); - } - - @Bean - public WebAnnotationEndpointDiscoverer webEndpointDiscoverer( - ApplicationContext applicationContext) { - ParameterMapper parameterMapper = new ConversionServiceParameterMapper( - DefaultConversionService.getSharedInstance()); - return new WebAnnotationEndpointDiscoverer(applicationContext, - parameterMapper, endpointMediaTypes(), - EndpointPathResolver.useEndpointId(), null, null); - } - - @Bean - public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { - return new PropertyPlaceholderConfigurer(); - } - - } - @Configuration @Import(BaseConfiguration.class) protected static class TestEndpointConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java new file mode 100644 index 0000000000..44d6aaa0f8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java @@ -0,0 +1,80 @@ +/* + * 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.annotation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +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.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.support.DefaultConversionService; + +import static org.mockito.Mockito.mock; + +/** + * Base configuration shared by tests. + * + * @author Andy Wilkinson + */ +@Configuration +class BaseConfiguration { + + @Bean + public AbstractWebEndpointIntegrationTests.EndpointDelegate endpointDelegate() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader instanceof TomcatEmbeddedWebappClassLoader) { + Thread.currentThread().setContextClassLoader(classLoader.getParent()); + } + try { + return mock(AbstractWebEndpointIntegrationTests.EndpointDelegate.class); + } + finally { + Thread.currentThread().setContextClassLoader(classLoader); + } + } + + @Bean + public EndpointMediaTypes endpointMediaTypes() { + List mediaTypes = Arrays.asList("application/vnd.test+json", + "application/json"); + return new EndpointMediaTypes(mediaTypes, mediaTypes); + } + + @Bean + public WebEndpointDiscoverer webEndpointDiscoverer( + ApplicationContext applicationContext) { + ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( + DefaultConversionService.getSharedInstance()); + return new WebEndpointDiscoverer(applicationContext, parameterMapper, + endpointMediaTypes(), EndpointPathResolver.useEndpointId(), + Collections.emptyList(), Collections.emptyList()); + } + + @Bean + public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { + return new PropertyPlaceholderConfigurer(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java similarity index 70% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java index 704226c63d..d985530ca2 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java @@ -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.web; +package org.springframework.boot.actuate.endpoint.web.annotation; import java.util.ArrayList; import java.util.Arrays; @@ -33,20 +33,21 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.boot.actuate.endpoint.EndpointInfo; -import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; 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.cache.CachingOperationInvoker; -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.CachingOperationInvoker; +import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint; -import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests.BaseConfiguration; -import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; -import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; +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.WebEndpointHttpMethod; +import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,55 +55,55 @@ import org.springframework.context.annotation.Import; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; -import org.springframework.util.ReflectionUtils; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link WebAnnotationEndpointDiscoverer}. + * Tests for {@link WebEndpointDiscoverer}. * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Phillip Webb */ -public class WebAnnotationEndpointDiscovererTests { +public class WebEndpointDiscovererTests { @Rule public final ExpectedException thrown = ExpectedException.none(); @Test - public void discoveryWorksWhenThereAreNoEndpoints() { + public void getEndpointsWhenNoEndpointBeansShouldReturnEmptyCollection() { load(EmptyConfiguration.class, - (discoverer) -> assertThat(discoverer.discoverEndpoints()).isEmpty()); + (discoverer) -> assertThat(discoverer.getEndpoints()).isEmpty()); } @Test - public void webExtensionMustHaveEndpoint() { + public void getEndpointsWhenWebExtensionIsMissingEndpointShouldThrowException() { load(TestWebEndpointExtensionConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Invalid extension"); - this.thrown.expectMessage(TestWebEndpointExtension.class.getName()); - this.thrown.expectMessage("no endpoint found"); - this.thrown.expectMessage(TestEndpoint.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage( + "Invalid extension 'endpointExtension': no endpoint found with type '" + + TestEndpoint.class.getName() + "'"); + discoverer.getEndpoints(); }); } @Test - public void onlyWebEndpointsAreDiscovered() { + public void getEndpointsWhenHasFilteredEndpointShouldOnlyDiscoverWebEndpoints() { load(MultipleEndpointsConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("test"); }); } @Test - public void oneOperationIsDiscoveredWhenExtensionOverridesOperation() { + public void getEndpointsWhenHasWebExtensionShouldOverrideStandardEndpoint() { load(OverriddenOperationWebEndpointExtensionConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("test"); - EndpointInfo endpoint = endpoints.get("test"); + ExposableWebEndpoint endpoint = endpoints.get("test"); assertThat(requestPredicates(endpoint)).has( requestPredicates(path("test").httpMethod(WebEndpointHttpMethod.GET) .consumes().produces("application/json"))); @@ -110,12 +111,12 @@ public class WebAnnotationEndpointDiscovererTests { } @Test - public void twoOperationsAreDiscoveredWhenExtensionAddsOperation() { + public void getEndpointsWhenExtensionAddsOperationShouldHaveBothOperations() { load(AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("test"); - EndpointInfo endpoint = endpoints.get("test"); + ExposableWebEndpoint endpoint = endpoints.get("test"); assertThat(requestPredicates(endpoint)).has(requestPredicates( path("test").httpMethod(WebEndpointHttpMethod.GET).consumes() .produces("application/json"), @@ -125,12 +126,12 @@ public class WebAnnotationEndpointDiscovererTests { } @Test - public void predicateForWriteOperationThatReturnsVoidHasNoProducedMediaTypes() { + public void getEndpointsWhenPredicateForWriteOperationThatReturnsVoidShouldHaveNoProducedMediaTypes() { load(VoidWriteOperationEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("voidwrite"); - EndpointInfo endpoint = endpoints.get("voidwrite"); + ExposableWebEndpoint endpoint = endpoints.get("voidwrite"); assertThat(requestPredicates(endpoint)).has(requestPredicates( path("voidwrite").httpMethod(WebEndpointHttpMethod.POST).produces() .consumes("application/json"))); @@ -138,91 +139,78 @@ public class WebAnnotationEndpointDiscovererTests { } @Test - public void discoveryFailsWhenTwoExtensionsHaveTheSameEndpointType() { + public void getEndpointsWhenTwoExtensionsHaveTheSameEndpointTypeShouldThrowException() { load(ClashingWebEndpointConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Found two extensions for the same endpoint"); - this.thrown.expectMessage(TestEndpoint.class.getName()); - this.thrown.expectMessage(TestWebEndpointExtension.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Found multiple extensions for the endpoint bean " + + "testEndpoint (testExtensionOne, testExtensionTwo)"); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenTwoStandardEndpointsHaveTheSameId() { + public void getEndpointsWhenTwoStandardEndpointsHaveTheSameIdShouldThrowException() { load(ClashingStandardEndpointConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Found two endpoints with the id 'test': "); - discoverer.discoverEndpoints(); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenEndpointHasClashingOperations() { + public void getEndpointsWhenWhenEndpointHasTwoOperationsWithTheSameNameShouldThrowException() { load(ClashingOperationsEndpointConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage( - "Found multiple web operations with matching request predicates:"); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsEndpoint.class, "getAll").toString()); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingOperationsEndpoint.class, "getAgain").toString()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Unable to map duplicate endpoint operations: " + + "[web request predicate GET to path 'test' " + + "produces: application/json] to clashingOperationsEndpoint"); + discoverer.getEndpoints(); }); } @Test - public void discoveryFailsWhenExtensionIsNotCompatibleWithTheEndpointType() { + public void getEndpointsWhenExtensionIsNotCompatibleWithTheEndpointTypeShouldThrowException() { load(InvalidWebExtensionConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Invalid extension"); - this.thrown.expectMessage(NonWebWebEndpointExtension.class.getName()); - this.thrown.expectMessage(NonWebEndpoint.class.getName()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Endpoint bean 'nonWebEndpoint' cannot support the " + + "extension bean 'nonWebWebEndpointExtension'"); + discoverer.getEndpoints(); }); } @Test - public void twoOperationsOnSameEndpointClashWhenSelectorsHaveDifferentNames() { + public void getEndpointsWhenWhenExtensionHasTwoOperationsWithTheSameNameShouldThrowException() { load(ClashingSelectorsWebEndpointExtensionConfiguration.class, (discoverer) -> { this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage( - "Found multiple web operations with matching request predicates:"); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingSelectorsWebEndpointExtension.class, "readOne", - String.class, String.class) - .toString()); - this.thrown.expectMessage(ReflectionUtils - .findMethod(ClashingSelectorsWebEndpointExtension.class, "readTwo", - String.class, String.class) - .toString()); - discoverer.discoverEndpoints(); + this.thrown.expectMessage("Unable to map duplicate endpoint operations"); + this.thrown.expectMessage("to testEndpoint (clashingSelectorsExtension)"); + discoverer.getEndpoints(); }); } @Test - public void endpointMainReadOperationIsCachedWithMatchingId() { + public void getEndpointsWhenHasCacheWithTtlShouldCacheReadOperationWithTtlValue() { load((id) -> 500L, (id) -> id, TestEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("test"); - EndpointInfo endpoint = endpoints.get("test"); + ExposableWebEndpoint endpoint = endpoints.get("test"); assertThat(endpoint.getOperations()).hasSize(1); - OperationInvoker operationInvoker = endpoint.getOperations().iterator().next() - .getInvoker(); - assertThat(operationInvoker).isInstanceOf(CachingOperationInvoker.class); - assertThat(((CachingOperationInvoker) operationInvoker).getTimeToLive()) + WebOperation operation = endpoint.getOperations().iterator().next(); + Object invoker = ReflectionTestUtils.getField(operation, "invoker"); + assertThat(invoker).isInstanceOf(CachingOperationInvoker.class); + assertThat(((CachingOperationInvoker) invoker).getTimeToLive()) .isEqualTo(500); }); } @Test - public void operationsThatReturnResourceProduceApplicationOctetStream() { + public void getEndpointsWhenOperationReturnsResourceShouldProduceApplicationOctetStream() { load(ResourceEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("resource"); - EndpointInfo endpoint = endpoints.get("resource"); + ExposableWebEndpoint endpoint = endpoints.get("resource"); assertThat(requestPredicates(endpoint)).has(requestPredicates( path("resource").httpMethod(WebEndpointHttpMethod.GET).consumes() .produces("application/octet-stream"))); @@ -230,12 +218,12 @@ public class WebAnnotationEndpointDiscovererTests { } @Test - public void operationCanProduceCustomMediaTypes() { + public void getEndpointsWhenHasCustomMediaTypeShouldProduceCustomMediaType() { load(CustomMediaTypesEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("custommediatypes"); - EndpointInfo endpoint = endpoints.get("custommediatypes"); + ExposableWebEndpoint endpoint = endpoints.get("custommediatypes"); assertThat(requestPredicates(endpoint)).has(requestPredicates( path("custommediatypes").httpMethod(WebEndpointHttpMethod.GET) .consumes().produces("text/plain"), @@ -247,14 +235,14 @@ public class WebAnnotationEndpointDiscovererTests { } @Test - public void endpointPathCanBeCustomized() { + public void getEndpointsWhenHasCustomPathShouldReturnCustomPath() { load((id) -> null, (id) -> "custom/" + id, AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> { - Map> endpoints = mapEndpoints( - discoverer.discoverEndpoints()); + Map endpoints = mapEndpoints( + discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys("test"); - EndpointInfo endpoint = endpoints.get("test"); - Condition> expected = requestPredicates( + ExposableWebEndpoint endpoint = endpoints.get("test"); + Condition> expected = requestPredicates( path("custom/test").httpMethod(WebEndpointHttpMethod.GET) .consumes().produces("application/json"), path("custom/test/{id}").httpMethod(WebEndpointHttpMethod.GET) @@ -263,26 +251,25 @@ public class WebAnnotationEndpointDiscovererTests { }); } - private void load(Class configuration, - Consumer consumer) { + private void load(Class configuration, Consumer consumer) { this.load((id) -> null, (id) -> id, configuration, consumer); } private void load(Function timeToLive, EndpointPathResolver endpointPathResolver, Class configuration, - Consumer consumer) { + Consumer 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")); - WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( - context, parameterMapper, mediaTypes, endpointPathResolver, + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(context, + parameterMapper, mediaTypes, endpointPathResolver, Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), - null); + Collections.emptyList()); consumer.accept(discoverer); } finally { @@ -290,27 +277,27 @@ public class WebAnnotationEndpointDiscovererTests { } } - private Map> mapEndpoints( - Collection> endpoints) { - Map> endpointById = new HashMap<>(); + private Map mapEndpoints( + Collection endpoints) { + Map endpointById = new HashMap<>(); endpoints.forEach((endpoint) -> endpointById.put(endpoint.getId(), endpoint)); return endpointById; } - private List requestPredicates( - EndpointInfo endpoint) { + private List requestPredicates( + ExposableWebEndpoint endpoint) { return endpoint.getOperations().stream().map(WebOperation::getRequestPredicate) .collect(Collectors.toList()); } - private Condition> requestPredicates( + private Condition> requestPredicates( RequestPredicateMatcher... matchers) { return new Condition<>((predicates) -> { if (predicates.size() != matchers.length) { return false; } - Map matchCounts = new HashMap<>(); - for (OperationRequestPredicate predicate : predicates) { + Map matchCounts = new HashMap<>(); + for (WebOperationRequestPredicate predicate : predicates) { matchCounts.put(predicate, Stream.of(matchers) .filter(matcher -> matcher.matches(predicate)).count()); } @@ -327,165 +314,6 @@ public class WebAnnotationEndpointDiscovererTests { } - @EndpointWebExtension(endpoint = TestEndpoint.class) - static class TestWebEndpointExtension { - - @ReadOperation - public Object getAll() { - return null; - } - - @ReadOperation - public Object getOne(@Selector String id) { - return null; - } - - @WriteOperation - public void update(String foo, String bar) { - - } - - public void someOtherMethod() { - - } - - } - - @Endpoint(id = "test") - static class TestEndpoint { - - @ReadOperation - public Object getAll() { - return null; - } - - } - - @EndpointWebExtension(endpoint = TestEndpoint.class) - static class OverriddenOperationWebEndpointExtension { - - @ReadOperation - public Object getAll() { - return null; - } - - } - - @EndpointWebExtension(endpoint = TestEndpoint.class) - static class AdditionalOperationWebEndpointExtension { - - @ReadOperation - public Object getOne(@Selector String id) { - return null; - } - - } - - @Endpoint(id = "test") - static class ClashingOperationsEndpoint { - - @ReadOperation - public Object getAll() { - return null; - } - - @ReadOperation - public Object getAgain() { - return null; - } - - } - - @EndpointWebExtension(endpoint = TestEndpoint.class) - static class ClashingOperationsWebEndpointExtension { - - @ReadOperation - public Object getAll() { - return null; - } - - @ReadOperation - public Object getAgain() { - return null; - } - - } - - @EndpointWebExtension(endpoint = TestEndpoint.class) - static class ClashingSelectorsWebEndpointExtension { - - @ReadOperation - public Object readOne(@Selector String oneA, @Selector String oneB) { - return null; - } - - @ReadOperation - public Object readTwo(@Selector String twoA, @Selector String twoB) { - return null; - } - - } - - @JmxEndpoint(id = "nonweb") - static class NonWebEndpoint { - - @ReadOperation - public Object getData() { - return null; - } - - } - - @EndpointWebExtension(endpoint = NonWebEndpoint.class) - static class NonWebWebEndpointExtension { - - @ReadOperation - public Object getSomething(@Selector String name) { - return null; - } - - } - - @Endpoint(id = "voidwrite") - static class VoidWriteOperationEndpoint { - - @WriteOperation - public void write(String foo, String bar) { - } - - } - - @Endpoint(id = "resource") - static class ResourceEndpoint { - - @ReadOperation - public Resource read() { - return new ByteArrayResource(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); - } - - } - - @Endpoint(id = "custommediatypes") - static class CustomMediaTypesEndpoint { - - @ReadOperation(produces = "text/plain") - public String read() { - return "read"; - } - - @WriteOperation(produces = { "a/b", "c/d" }) - public String write() { - return "write"; - - } - - @DeleteOperation(produces = "text/plain") - public String delete() { - return "delete"; - } - - } - @Configuration static class MultipleEndpointsConfiguration { @@ -660,6 +488,165 @@ public class WebAnnotationEndpointDiscovererTests { } + @EndpointWebExtension(endpoint = TestEndpoint.class) + static class TestWebEndpointExtension { + + @ReadOperation + public Object getAll() { + return null; + } + + @ReadOperation + public Object getOne(@Selector String id) { + return null; + } + + @WriteOperation + public void update(String foo, String bar) { + + } + + public void someOtherMethod() { + + } + + } + + @Endpoint(id = "test") + static class TestEndpoint { + + @ReadOperation + public Object getAll() { + return null; + } + + } + + @EndpointWebExtension(endpoint = TestEndpoint.class) + static class OverriddenOperationWebEndpointExtension { + + @ReadOperation + public Object getAll() { + return null; + } + + } + + @EndpointWebExtension(endpoint = TestEndpoint.class) + static class AdditionalOperationWebEndpointExtension { + + @ReadOperation + public Object getOne(@Selector String id) { + return null; + } + + } + + @Endpoint(id = "test") + static class ClashingOperationsEndpoint { + + @ReadOperation + public Object getAll() { + return null; + } + + @ReadOperation + public Object getAgain() { + return null; + } + + } + + @EndpointWebExtension(endpoint = TestEndpoint.class) + static class ClashingOperationsWebEndpointExtension { + + @ReadOperation + public Object getAll() { + return null; + } + + @ReadOperation + public Object getAgain() { + return null; + } + + } + + @EndpointWebExtension(endpoint = TestEndpoint.class) + static class ClashingSelectorsWebEndpointExtension { + + @ReadOperation + public Object readOne(@Selector String oneA, @Selector String oneB) { + return null; + } + + @ReadOperation + public Object readTwo(@Selector String twoA, @Selector String twoB) { + return null; + } + + } + + @JmxEndpoint(id = "nonweb") + static class NonWebEndpoint { + + @ReadOperation + public Object getData() { + return null; + } + + } + + @EndpointWebExtension(endpoint = NonWebEndpoint.class) + static class NonWebWebEndpointExtension { + + @ReadOperation + public Object getSomething(@Selector String name) { + return null; + } + + } + + @Endpoint(id = "voidwrite") + static class VoidWriteOperationEndpoint { + + @WriteOperation + public void write(String foo, String bar) { + } + + } + + @Endpoint(id = "resource") + static class ResourceEndpoint { + + @ReadOperation + public Resource read() { + return new ByteArrayResource(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + } + + } + + @Endpoint(id = "custommediatypes") + static class CustomMediaTypesEndpoint { + + @ReadOperation(produces = "text/plain") + public String read() { + return "read"; + } + + @WriteOperation(produces = { "a/b", "c/d" }) + public String write() { + return "write"; + + } + + @DeleteOperation(produces = "text/plain") + public String delete() { + return "delete"; + } + + } + private static final class RequestPredicateMatcher { private final String path; @@ -689,7 +676,7 @@ public class WebAnnotationEndpointDiscovererTests { return this; } - private boolean matches(OperationRequestPredicate predicate) { + private boolean matches(WebOperationRequestPredicate predicate) { return (this.path == null || this.path.equals(predicate.getPath())) && (this.httpMethod == null || this.httpMethod == predicate.getHttpMethod()) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java index 44f8eedc9f..6899c3f4fd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -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,10 +27,9 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.servlet.ServletContainer; -import org.springframework.boot.actuate.endpoint.EndpointDiscoverer; -import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; -import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; +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.ServletRegistrationBean; @@ -82,13 +81,13 @@ public class JerseyWebEndpointIntegrationTests extends @Bean public ResourceConfig resourceConfig(Environment environment, - EndpointDiscoverer endpointDiscoverer, + WebEndpointDiscoverer endpointDiscoverer, EndpointMediaTypes endpointMediaTypes) { ResourceConfig resourceConfig = new ResourceConfig(); Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources( new EndpointMapping(environment.getProperty("endpointPath")), - endpointDiscoverer.discoverEndpoints(), endpointMediaTypes); + endpointDiscoverer.getEndpoints(), endpointMediaTypes); resourceConfig.registerResources(new HashSet<>(resources)); resourceConfig.register(JacksonFeature.class); resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java index 79fa3b5fe7..4f6e29e310 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java @@ -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,10 +20,9 @@ import java.util.Arrays; import org.junit.Test; -import org.springframework.boot.actuate.endpoint.EndpointDiscoverer; -import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; -import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; +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; @@ -112,15 +111,14 @@ public class WebFluxEndpointIntegrationTests @Bean public WebFluxEndpointHandlerMapping webEndpointHandlerMapping( - Environment environment, - EndpointDiscoverer endpointDiscoverer, + Environment environment, WebEndpointDiscoverer endpointDiscoverer, EndpointMediaTypes endpointMediaTypes) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); return new WebFluxEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), - endpointDiscoverer.discoverEndpoints(), endpointMediaTypes, + endpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java index b2adad5edd..b062b24a9f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java @@ -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,10 +20,9 @@ import java.util.Arrays; import org.junit.Test; -import org.springframework.boot.actuate.endpoint.EndpointDiscoverer; -import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; -import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; +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; @@ -105,15 +104,14 @@ public class MvcWebEndpointIntegrationTests extends @Bean public WebMvcEndpointHandlerMapping webEndpointHandlerMapping( - Environment environment, - EndpointDiscoverer webEndpointDiscoverer, + Environment environment, WebEndpointDiscoverer endpointDiscoverer, EndpointMediaTypes endpointMediaTypes) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); return new WebMvcEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), - webEndpointDiscoverer.discoverEndpoints(), endpointMediaTypes, + endpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java index 9f447a92a9..2ab86383bc 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java @@ -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,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web.test; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -28,11 +29,11 @@ import org.glassfish.jersey.server.model.Resource; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.InitializationError; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +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.annotation.WebAnnotationEndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -97,12 +98,13 @@ class JerseyEndpointsRunner extends AbstractWebEndpointRunner { ActuatorMediaType.V2_JSON); EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); - WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( - this.applicationContext, new ConversionServiceParameterMapper(), - endpointMediaTypes, EndpointPathResolver.useEndpointId(), null, null); + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer( + this.applicationContext, new ConversionServiceParameterValueMapper(), + endpointMediaTypes, EndpointPathResolver.useEndpointId(), + Collections.emptyList(), Collections.emptyList()); Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources(new EndpointMapping("/actuator"), - discoverer.discoverEndpoints(), endpointMediaTypes); + discoverer.getEndpoints(), endpointMediaTypes); config.registerResources(new HashSet<>(resources)); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java index 5afe990737..5e836cde13 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java @@ -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,16 +17,17 @@ package org.springframework.boot.actuate.endpoint.web.test; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.InitializationError; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +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.annotation.WebAnnotationEndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -103,11 +104,12 @@ class WebFluxEndpointsRunner extends AbstractWebEndpointRunner { ActuatorMediaType.V2_JSON); EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); - WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( - this.applicationContext, new ConversionServiceParameterMapper(), - endpointMediaTypes, EndpointPathResolver.useEndpointId(), null, null); + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer( + this.applicationContext, new ConversionServiceParameterValueMapper(), + endpointMediaTypes, EndpointPathResolver.useEndpointId(), + Collections.emptyList(), Collections.emptyList()); return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), - discoverer.discoverEndpoints(), endpointMediaTypes, + discoverer.getEndpoints(), endpointMediaTypes, new CorsConfiguration()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java index e3258c8cfc..d0ae1a972d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java @@ -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,16 +17,17 @@ package org.springframework.boot.actuate.endpoint.web.test; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.InitializationError; -import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +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.annotation.WebAnnotationEndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -86,11 +87,12 @@ class WebMvcEndpointRunner extends AbstractWebEndpointRunner { ActuatorMediaType.V2_JSON); EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); - WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( - this.applicationContext, new ConversionServiceParameterMapper(), - endpointMediaTypes, EndpointPathResolver.useEndpointId(), null, null); + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer( + this.applicationContext, new ConversionServiceParameterValueMapper(), + endpointMediaTypes, EndpointPathResolver.useEndpointId(), + Collections.emptyList(), Collections.emptyList()); return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), - discoverer.discoverEndpoints(), endpointMediaTypes, + discoverer.getEndpoints(), endpointMediaTypes, new CorsConfiguration()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java index 3915cb2e7c..98fe46fec3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java @@ -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,6 +37,18 @@ public class EndpointMapping { this.path = normalizePath(path); } + /** + * Returns the path to which endpoints should be mapped. + * @return the path + */ + public String getPath() { + return this.path; + } + + public String createSubPath(String path) { + return this.path + normalizePath(path); + } + private static String normalizePath(String path) { if (!StringUtils.hasText(path)) { return path; @@ -51,16 +63,4 @@ public class EndpointMapping { return normalizedPath; } - /** - * Returns the path to which endpoints should be mapped. - * @return the path - */ - public String getPath() { - return this.path; - } - - public String createSubPath(String path) { - return this.path + normalizePath(path); - } - } diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index 9e6b827892..c608631b15 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -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,7 +23,9 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; @@ -32,18 +34,19 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Madhura Bhave + * @author Stephane Nicoll */ @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SampleActuatorCustomSecurityApplicationTests { @Autowired - private TestRestTemplate restTemplate; + private Environment environment; @Test public void homeIsSecure() { @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate.getForEntity("/", Map.class); + ResponseEntity entity = restTemplate().getForEntity("/", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); @SuppressWarnings("unchecked") Map body = entity.getBody(); @@ -54,7 +57,7 @@ public class SampleActuatorCustomSecurityApplicationTests { @Test public void testInsecureApplicationPath() { @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate.getForEntity("/foo", Map.class); + ResponseEntity entity = restTemplate().getForEntity("/foo", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); @SuppressWarnings("unchecked") Map body = entity.getBody(); @@ -64,26 +67,64 @@ public class SampleActuatorCustomSecurityApplicationTests { @Test public void testInsecureStaticResources() { - ResponseEntity entity = this.restTemplate + ResponseEntity entity = restTemplate() .getForEntity("/css/bootstrap.min.css", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("body"); } @Test - public void insecureActuator() { - ResponseEntity entity = this.restTemplate.getForEntity("/actuator/health", + public void actuatorInsecureEndpoint() { + ResponseEntity entity = restTemplate().getForEntity("/actuator/health", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).contains("\"status\":\"UP\""); } @Test - public void secureActuator() { - @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate.getForEntity("/actuator/env", - Map.class); + public void actuatorSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity("/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void actuatorSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity("/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void actuatorSecureEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity("/actuator/env", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void actuatorCustomMvcSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate() + .getForEntity("/actuator/example/echo?text={t}", String.class, "test"); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } + private TestRestTemplate restTemplate() { + return configure(new TestRestTemplate()); + } + + private TestRestTemplate adminRestTemplate() { + return configure(new TestRestTemplate("admin", "admin")); + } + + private TestRestTemplate userRestTemplate() { + return configure(new TestRestTemplate("user", "password")); + } + + private TestRestTemplate configure(TestRestTemplate restTemplate) { + restTemplate + .setUriTemplateHandler(new LocalHostUriTemplateHandler(this.environment)); + return restTemplate; + } + }