org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.boot.actuate.endpoint.annotation.AnnotationEndpointDiscoverer.java

Source

/*
 * 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.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.actuate.endpoint.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

/**
 * A base {@link EndpointDiscoverer} implementation that discovers
 * {@link Endpoint @Endpoint} beans and {@link EndpointExtension @EndpointExtension} beans
 * in an application context.
 *
 * @param <K> the type of the operation key
 * @param <T> the type of the operation
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @author Phillip Webb
 * @since 2.0.0
 */
public abstract class AnnotationEndpointDiscoverer<K, T extends Operation> implements EndpointDiscoverer<T> {

    private final Log logger = LogFactory.getLog(getClass());

    private final ApplicationContext applicationContext;

    private final Function<T, K> operationKeyFactory;

    private final OperationsFactory<T> operationsFactory;

    private final List<EndpointFilter<T>> filters;

    /**
     * Create a new {@link AnnotationEndpointDiscoverer} instance.
     * @param applicationContext the application context
     * @param operationFactory a factory used to create operations
     * @param operationKeyFactory a factory used to create a key for an operation
     * @param parameterMapper the {@link ParameterMapper} used to convert arguments when
     * an operation is invoked
     * @param invokerAdvisors advisors used to add additional invoker advise
     * @param filters filters that must match for an endpoint to be exposed
     */
    protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
            OperationFactory<T> operationFactory, Function<T, K> operationKeyFactory,
            ParameterMapper parameterMapper, Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
            Collection<? extends EndpointFilter<T>> filters) {
        Assert.notNull(applicationContext, "Application Context must not be null");
        Assert.notNull(operationFactory, "Operation Factory must not be null");
        Assert.notNull(operationKeyFactory, "Operation Key Factory must not be null");
        Assert.notNull(parameterMapper, "Parameter Mapper must not be null");
        this.applicationContext = applicationContext;
        this.operationKeyFactory = operationKeyFactory;
        this.operationsFactory = new OperationsFactory<>(operationFactory, parameterMapper, invokerAdvisors);
        this.filters = (filters == null ? Collections.emptyList() : new ArrayList<>(filters));
    }

    @Override
    public final Collection<EndpointInfo<T>> discoverEndpoints() {
        Class<T> operationType = getOperationType();
        Map<Class<?>, DiscoveredEndpoint> endpoints = getEndpoints(operationType);
        Map<Class<?>, DiscoveredExtension> extensions = getExtensions(operationType, endpoints);
        Collection<DiscoveredEndpoint> exposed = mergeExposed(endpoints, extensions);
        verify(exposed);
        return exposed.stream().map(DiscoveredEndpoint::getInfo).collect(Collectors.toCollection(ArrayList::new));
    }

    /**
     * Return the operation type being discovered. By default this method will resolve the
     * class generic "{@code <T>}".
     * @return the operation type
     */
    @SuppressWarnings("unchecked")
    protected Class<T> getOperationType() {
        return (Class<T>) ResolvableType.forClass(AnnotationEndpointDiscoverer.class, getClass()).resolveGeneric(1);
    }

    private Map<Class<?>, DiscoveredEndpoint> getEndpoints(Class<T> operationType) {
        Map<Class<?>, DiscoveredEndpoint> endpoints = new LinkedHashMap<>();
        Map<String, DiscoveredEndpoint> endpointsById = new LinkedHashMap<>();
        String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
                Endpoint.class);
        for (String beanName : beanNames) {
            addEndpoint(endpoints, endpointsById, beanName);
        }
        return endpoints;
    }

    private void addEndpoint(Map<Class<?>, DiscoveredEndpoint> endpoints,
            Map<String, DiscoveredEndpoint> endpointsById, String beanName) {
        Class<?> endpointType = this.applicationContext.getType(beanName);
        Object target = this.applicationContext.getBean(beanName);
        DiscoveredEndpoint endpoint = createEndpoint(target, endpointType);
        String id = endpoint.getInfo().getId();
        DiscoveredEndpoint previous = endpointsById.putIfAbsent(id, endpoint);
        Assert.state(previous == null,
                () -> "Found two endpoints with the id '" + id + "': " + endpoint + " and " + previous);
        endpoints.put(endpointType, endpoint);
    }

    private DiscoveredEndpoint createEndpoint(Object target, Class<?> endpointType) {
        AnnotationAttributes annotationAttributes = AnnotatedElementUtils
                .findMergedAnnotationAttributes(endpointType, Endpoint.class, true, true);
        String id = annotationAttributes.getString("id");
        Assert.state(StringUtils.hasText(id), "No @Endpoint id attribute specified for " + endpointType.getName());
        boolean enabledByDefault = (Boolean) annotationAttributes.get("enableByDefault");
        Collection<T> operations = this.operationsFactory.createOperations(id, target, endpointType).values();
        EndpointInfo<T> endpointInfo = new EndpointInfo<>(id, enabledByDefault, operations);
        boolean exposed = isEndpointExposed(endpointType, endpointInfo);
        return new DiscoveredEndpoint(endpointType, endpointInfo, exposed);
    }

    private Map<Class<?>, DiscoveredExtension> getExtensions(Class<T> operationType,
            Map<Class<?>, DiscoveredEndpoint> endpoints) {
        Map<Class<?>, DiscoveredExtension> extensions = new LinkedHashMap<>();
        String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
                EndpointExtension.class);
        for (String beanName : beanNames) {
            addExtension(endpoints, extensions, beanName);
        }
        return extensions;
    }

    private void addExtension(Map<Class<?>, DiscoveredEndpoint> endpoints,
            Map<Class<?>, DiscoveredExtension> extensions, String beanName) {
        Class<?> extensionType = this.applicationContext.getType(beanName);
        Class<?> endpointType = getEndpointType(extensionType);
        DiscoveredEndpoint endpoint = getExtendingEndpoint(endpoints, extensionType, endpointType);
        if (isExtensionExposed(endpointType, extensionType, endpoint.getInfo())) {
            Assert.state(endpoint.isExposed() || isEndpointFiltered(endpoint.getInfo()),
                    () -> "Invalid extension " + extensionType.getName() + "': endpoint '" + endpointType.getName()
                            + "' does not support such extension");
            Object target = this.applicationContext.getBean(beanName);
            Map<Method, T> operations = this.operationsFactory.createOperations(endpoint.getInfo().getId(), target,
                    extensionType);
            DiscoveredExtension extension = new DiscoveredExtension(extensionType, operations.values());
            DiscoveredExtension previous = extensions.putIfAbsent(endpointType, extension);
            Assert.state(previous == null,
                    () -> "Found two extensions for the same endpoint '" + endpointType.getName() + "': "
                            + extension.getExtensionType().getName() + " and "
                            + previous.getExtensionType().getName());
        }
    }

    private Class<?> getEndpointType(Class<?> extensionType) {
        AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(extensionType,
                EndpointExtension.class);
        Class<?> endpointType = attributes.getClass("endpoint");
        Assert.state(!endpointType.equals(Void.class),
                () -> "Extension " + endpointType.getName() + " does not specify an endpoint");
        return endpointType;
    }

    private DiscoveredEndpoint getExtendingEndpoint(Map<Class<?>, DiscoveredEndpoint> endpoints,
            Class<?> extensionType, Class<?> endpointType) {
        DiscoveredEndpoint endpoint = endpoints.get(endpointType);
        Assert.state(endpoint != null, () -> "Invalid extension '" + extensionType.getName()
                + "': no endpoint found with type '" + endpointType.getName() + "'");
        return endpoint;
    }

    private boolean isEndpointExposed(Class<?> endpointType, EndpointInfo<T> endpointInfo) {
        if (isEndpointFiltered(endpointInfo)) {
            return false;
        }
        AnnotationAttributes annotationAttributes = AnnotatedElementUtils
                .getMergedAnnotationAttributes(endpointType, FilteredEndpoint.class);
        if (annotationAttributes == null) {
            return true;
        }
        Class<?> filterClass = annotationAttributes.getClass("value");
        return isFilterMatch(filterClass, endpointInfo);
    }

    private boolean isEndpointFiltered(EndpointInfo<T> endpointInfo) {
        for (EndpointFilter<T> filter : this.filters) {
            if (!isFilterMatch(filter, endpointInfo)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if an extension is exposed.
     * @param endpointType the endpoint type
     * @param extensionType the extension type
     * @param endpointInfo the endpoint info
     * @return if the extension is exposed
     */
    protected boolean isExtensionExposed(Class<?> endpointType, Class<?> extensionType,
            EndpointInfo<T> endpointInfo) {
        AnnotationAttributes annotationAttributes = AnnotatedElementUtils
                .getMergedAnnotationAttributes(extensionType, EndpointExtension.class);
        Class<?> filterClass = annotationAttributes.getClass("filter");
        return isFilterMatch(filterClass, endpointInfo);
    }

    @SuppressWarnings("unchecked")
    private boolean isFilterMatch(Class<?> filterClass, EndpointInfo<T> endpointInfo) {
        Class<?> generic = ResolvableType.forClass(EndpointFilter.class, filterClass).resolveGeneric(0);
        if (generic == null || generic.isAssignableFrom(getOperationType())) {
            EndpointFilter<T> filter = (EndpointFilter<T>) BeanUtils.instantiateClass(filterClass);
            return isFilterMatch(filter, endpointInfo);
        }
        return false;
    }

    private boolean isFilterMatch(EndpointFilter<T> filter, EndpointInfo<T> endpointInfo) {
        try {
            return filter.match(endpointInfo, this);
        } catch (ClassCastException ex) {
            String msg = ex.getMessage();
            if (msg == null || msg.startsWith(endpointInfo.getClass().getName())) {
                // Possibly a lambda-defined listener which we could not resolve the
                // generic event type for
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Non-matching info type for filter: " + filter, ex);
                }
                return false;
            }
            throw ex;
        }

    }

    private Collection<DiscoveredEndpoint> mergeExposed(Map<Class<?>, DiscoveredEndpoint> endpoints,
            Map<Class<?>, DiscoveredExtension> extensions) {
        List<DiscoveredEndpoint> result = new ArrayList<>();
        endpoints.forEach((endpointClass, endpoint) -> {
            if (endpoint.isExposed()) {
                DiscoveredExtension extension = extensions.remove(endpointClass);
                result.add(endpoint.merge(extension));
            }
        });
        return result;
    }

    /**
     * Allows subclasses to verify that the descriptors are correctly configured.
     * @param exposedEndpoints the discovered endpoints to verify before exposing
     */
    protected void verify(Collection<DiscoveredEndpoint> exposedEndpoints) {
    }

    /**
     * A discovered endpoint (which may not be valid and might not ultimately be exposed).
     */
    protected final class DiscoveredEndpoint {

        private final EndpointInfo<T> info;

        private final boolean exposed;

        private final Map<OperationKey, List<T>> operations;

        private DiscoveredEndpoint(Class<?> type, EndpointInfo<T> info, boolean exposed) {
            Assert.notNull(info, "Info must not be null");
            this.info = info;
            this.exposed = exposed;
            this.operations = indexEndpointOperations(type, info);
        }

        private Map<OperationKey, List<T>> indexEndpointOperations(Class<?> endpointType, EndpointInfo<T> info) {
            return Collections.unmodifiableMap(indexOperations(info.getId(), endpointType, info.getOperations()));
        }

        private DiscoveredEndpoint(EndpointInfo<T> info, boolean exposed, Map<OperationKey, List<T>> operations) {
            Assert.notNull(info, "Info must not be null");
            this.info = info;
            this.exposed = exposed;
            this.operations = operations;
        }

        /**
         * Return the {@link EndpointInfo} for the discovered endpoint.
         * @return the endpoint info
         */
        public EndpointInfo<T> getInfo() {
            return this.info;
        }

        /**
         * Return {@code true} if the endpoint is exposed.
         * @return if the is exposed
         */
        private boolean isExposed() {
            return this.exposed;
        }

        /**
         * Return all operation that were discovered. These might be different to the ones
         * that are in {@link #getInfo()}.
         * @return the endpoint operations
         */
        public Map<OperationKey, List<T>> getOperations() {
            return this.operations;
        }

        /**
         * Find any duplicate operations.
         * @return any duplicate operations
         */
        public Map<OperationKey, List<T>> findDuplicateOperations() {
            return this.operations.entrySet().stream().filter((entry) -> entry.getValue().size() > 1)
                    .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (u, v) -> v, LinkedHashMap::new));
        }

        private DiscoveredEndpoint merge(DiscoveredExtension extension) {
            if (extension == null) {
                return this;
            }
            Map<OperationKey, List<T>> operations = mergeOperations(extension);
            EndpointInfo<T> info = new EndpointInfo<>(this.info.getId(), this.info.isEnableByDefault(),
                    flatten(operations).values());
            return new DiscoveredEndpoint(info, this.exposed, operations);
        }

        private Map<OperationKey, List<T>> mergeOperations(DiscoveredExtension extension) {
            MultiValueMap<OperationKey, T> operations = new LinkedMultiValueMap<>(this.operations);
            operations.addAll(
                    indexOperations(getInfo().getId(), extension.getExtensionType(), extension.getOperations()));
            return Collections.unmodifiableMap(operations);
        }

        private Map<K, T> flatten(Map<OperationKey, List<T>> operations) {
            Map<K, T> flattened = new LinkedHashMap<>();
            operations.forEach((operationKey, value) -> flattened.put(operationKey.getKey(), getLastValue(value)));
            return Collections.unmodifiableMap(flattened);
        }

        private T getLastValue(List<T> value) {
            return value.get(value.size() - 1);
        }

        private MultiValueMap<OperationKey, T> indexOperations(String endpointId, Class<?> target,
                Collection<T> operations) {
            LinkedMultiValueMap<OperationKey, T> result = new LinkedMultiValueMap<>();
            operations.forEach((operation) -> {
                K key = getOperationKey(operation);
                result.add(new OperationKey(endpointId, target, key), operation);
            });
            return result;
        }

        private K getOperationKey(T operation) {
            return AnnotationEndpointDiscoverer.this.operationKeyFactory.apply(operation);
        }

        @Override
        public String toString() {
            return getInfo().toString();
        }

    }

    /**
     * A discovered extension.
     */
    protected final class DiscoveredExtension {

        private final Class<?> extensionType;

        private final Collection<T> operations;

        private DiscoveredExtension(Class<?> extensionType, Collection<T> operations) {
            this.extensionType = extensionType;
            this.operations = operations;
        }

        public Class<?> getExtensionType() {
            return this.extensionType;
        }

        public Collection<T> getOperations() {
            return this.operations;
        }

        @Override
        public String toString() {
            return this.extensionType.getName();
        }

    }

    /**
     * Define the key of an operation in the context of an operation's implementation.
     */
    protected final class OperationKey {

        private final String endpointId;

        private final Class<?> target;

        private final K key;

        public OperationKey(String endpointId, Class<?> target, K key) {
            this.endpointId = endpointId;
            this.target = target;
            this.key = key;
        }

        public K getKey() {
            return this.key;
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            OperationKey other = (OperationKey) o;
            Boolean result = true;
            result = result && this.endpointId.equals(other.endpointId);
            result = result && this.target.equals(other.target);
            result = result && this.key.equals(other.key);
            return result;
        }

        @Override
        public int hashCode() {
            int result = this.endpointId.hashCode();
            result = 31 * result + this.target.hashCode();
            result = 31 * result + this.key.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return new ToStringCreator(this).append("endpointId", this.endpointId).append("target", this.target)
                    .append("key", this.key).toString();
        }

    }

}