Java tutorial
/* * Copyright 2012-2019 the original author 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 * * https://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.servlet; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; 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.bind.annotation.ResponseStatus; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; /** * A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints} * available over HTTP using Spring MVC. * * @author Andy Wilkinson * @author Madhura Bhave * @author Phillip Webb * @author Brian Clozel * @since 2.0.0 */ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappingInfoHandlerMapping implements InitializingBean, MatchableHandlerMapping { private final EndpointMapping endpointMapping; private final Collection<ExposableWebEndpoint> endpoints; private final EndpointMediaTypes endpointMediaTypes; private final CorsConfiguration corsConfiguration; private final boolean shouldRegisterLinksMapping; private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle", HttpServletRequest.class, Map.class); private static final RequestMappingInfo.BuilderConfiguration builderConfig = getBuilderConfig(); /** * 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 endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @param shouldRegisterLinksMapping whether the links endpoint should be registered */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes, boolean shouldRegisterLinksMapping) { this(endpointMapping, endpoints, endpointMediaTypes, null, shouldRegisterLinksMapping); } /** * Creates a new {@code AbstractWebMvcEndpointHandlerMapping} that provides mappings * for the operations of the given endpoints. * @param endpointMapping the base mapping for all endpoints * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints or {@code null} * @param shouldRegisterLinksMapping whether the links endpoint should be registered */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) { this.endpointMapping = endpointMapping; this.endpoints = endpoints; this.endpointMediaTypes = endpointMediaTypes; this.corsConfiguration = corsConfiguration; this.shouldRegisterLinksMapping = shouldRegisterLinksMapping; setOrder(-100); } @Override protected void initHandlerMethods() { for (ExposableWebEndpoint endpoint : this.endpoints) { for (WebOperation operation : endpoint.getOperations()) { registerMappingForOperation(endpoint, operation); } } if (this.shouldRegisterLinksMapping) { registerLinksMapping(); } } @Override protected HandlerMethod createHandlerMethod(Object handler, Method method) { HandlerMethod handlerMethod = super.createHandlerMethod(handler, method); return new WebMvcEndpointHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod()); } @Override public RequestMatchResult match(HttpServletRequest request, String pattern) { RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(builderConfig).build(); RequestMappingInfo matchingInfo = info.getMatchingCondition(request); if (matchingInfo == null) { return null; } Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns(); String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); } private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() { RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); config.setUrlPathHelper(null); config.setPathMatcher(null); config.setSuffixPatternMatch(false); config.setTrailingSlashMatch(true); return config; } private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); String path = predicate.getPath(); String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable(); if (matchAllRemainingPathSegmentsVariable != null) { path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", "**"); } ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation, new ServletWebOperationAdapter(operation)); registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation), this.handleMethod); } /** * 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 ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint, WebOperation operation, ServletWebOperation servletWebOperation) { return servletWebOperation; } private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) { PatternsRequestCondition patterns = patternsRequestConditionForPattern(path); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( RequestMethod.valueOf(predicate.getHttpMethod().name())); ConsumesRequestCondition consumes = new ConsumesRequestCondition( StringUtils.toStringArray(predicate.getConsumes())); ProducesRequestCondition produces = new ProducesRequestCondition( StringUtils.toStringArray(predicate.getProduces())); return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null); } private void registerLinksMapping() { PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(RequestMethod.GET); ProducesRequestCondition produces = new ProducesRequestCondition(this.endpointMediaTypes.getProduced() .toArray(StringUtils.toStringArray(this.endpointMediaTypes.getProduced()))); RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, null, produces, null); LinksHandler linksHandler = getLinksHandler(); registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links", HttpServletRequest.class, HttpServletResponse.class)); } private PatternsRequestCondition patternsRequestConditionForPattern(String path) { String[] patterns = new String[] { this.endpointMapping.createSubPath(path) }; return new PatternsRequestCondition(patterns, builderConfig.getUrlPathHelper(), builderConfig.getPathMatcher(), builderConfig.useSuffixPatternMatch(), builderConfig.useTrailingSlashMatch()); } @Override protected boolean hasCorsConfigurationSource(Object handler) { return this.corsConfiguration != null; } @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) { return this.corsConfiguration; } @Override protected boolean isHandler(Class<?> beanType) { return false; } @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { return null; } @Override protected void extendInterceptors(List<Object> interceptors) { interceptors.add(new SkipPathExtensionContentNegotiation()); } /** * Return the Handler providing actuator links at the root endpoint. * @return the links handler */ protected abstract LinksHandler getLinksHandler(); /** * Return the web endpoints being mapped. * @return the endpoints */ public Collection<ExposableWebEndpoint> getEndpoints() { return this.endpoints; } /** * Handler providing actuator links at the root endpoint. */ @FunctionalInterface protected interface LinksHandler { Object links(HttpServletRequest request, HttpServletResponse response); } /** * A servlet web operation that can be handled by Spring MVC. */ @FunctionalInterface protected interface ServletWebOperation { Object handle(HttpServletRequest request, Map<String, String> body); } /** * Adapter class to convert an {@link OperationInvoker} into a * {@link ServletWebOperation}. */ private class ServletWebOperationAdapter implements ServletWebOperation { private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR; private final WebOperation operation; ServletWebOperationAdapter(WebOperation operation) { this.operation = operation; } @Override public Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) { Map<String, Object> arguments = getArguments(request, body); try { return handleResult( this.operation .invoke(new InvocationContext(new ServletSecurityContext(request), arguments)), HttpMethod.valueOf(request.getMethod())); } catch (InvalidEndpointRequestException ex) { throw new BadOperationRequestException(ex.getReason()); } } @Override public String toString() { return "Actuator web endpoint '" + this.operation.getId() + "'"; } private Map<String, Object> getArguments(HttpServletRequest request, Map<String, String> body) { Map<String, Object> arguments = new LinkedHashMap<>(); arguments.putAll(getTemplateVariables(request)); String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate() .getMatchAllRemainingPathSegmentsVariable(); if (matchAllRemainingPathSegmentsVariable != null) { arguments.put(matchAllRemainingPathSegmentsVariable, getRemainingPathSegments(request)); } if (body != null && HttpMethod.POST.name().equals(request.getMethod())) { arguments.putAll(body); } request.getParameterMap().forEach((name, values) -> arguments.put(name, (values.length != 1) ? Arrays.asList(values) : values[0])); return arguments; } private Object getRemainingPathSegments(HttpServletRequest request) { String[] pathTokens = tokenize(request, HandlerMapping.LOOKUP_PATH, true); String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false); int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1; Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments"); String[] remainingPathSegments = new String[numberOfRemainingPathSegments]; System.arraycopy(pathTokens, patternTokens.length - 1, remainingPathSegments, 0, numberOfRemainingPathSegments); return remainingPathSegments; } private String[] tokenize(HttpServletRequest request, String attributeName, boolean decode) { String value = (String) request.getAttribute(attributeName); String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true); if (decode) { for (int i = 0; i < segments.length; i++) { if (segments[i].contains("%")) { segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8); } } } return segments; } @SuppressWarnings("unchecked") private Map<String, String> getTemplateVariables(HttpServletRequest request) { return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); } private Object handleResult(Object result, HttpMethod httpMethod) { if (result == null) { return new ResponseEntity<>( (httpMethod != HttpMethod.GET) ? HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND); } if (!(result instanceof WebEndpointResponse)) { return result; } WebEndpointResponse<?> response = (WebEndpointResponse<?>) result; return new ResponseEntity<Object>(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 Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) { return this.operation.handle(request, body); } @Override public String toString() { return this.operation.toString(); } } /** * {@link HandlerMethod} subclass for endpoint information logging. */ private static class WebMvcEndpointHandlerMethod extends HandlerMethod { WebMvcEndpointHandlerMethod(Object bean, Method method) { super(bean, method); } @Override public String toString() { return getBean().toString(); } @Override public HandlerMethod createWithResolvedBean() { return this; } } @ResponseStatus(code = HttpStatus.BAD_REQUEST) private static class BadOperationRequestException extends RuntimeException { BadOperationRequestException(String message) { super(message); } } private static final class ServletSecurityContext implements SecurityContext { private final HttpServletRequest request; private ServletSecurityContext(HttpServletRequest request) { this.request = request; } @Override public Principal getPrincipal() { return this.request.getUserPrincipal(); } @Override public boolean isUserInRole(String role) { return this.request.isUserInRole(role); } } }