org.springframework.messaging.handler.method.AbstractMethodMessageHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.messaging.handler.method.AbstractMethodMessageHandler.java

Source

/*
 * Copyright 2002-2013 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.messaging.handler.method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.*;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Abstract base class for HandlerMethod-based message handling. Provides most of
 * the logic required to discover handler methods at startup, find a matching handler
 * method at runtime for a given message and invoke it.
 * <p>
 * Also supports discovering and invoking exception handling methods to process
 * exceptions raised during message handling.
 *
 * @param <T> the type of the Object that contains information mapping a
 * {@link HandlerMethod} to incoming messages
 *
 * @author Rossen Stoyanchev
 * @since 4.0
 */
public abstract class AbstractMethodMessageHandler<T>
        implements MessageHandler, ApplicationContextAware, InitializingBean {

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

    public static final String LOOKUP_DESTINATION_HEADER = "lookupDestination";

    private Collection<String> destinationPrefixes = new ArrayList<String>();

    private List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
    private List<HandlerMethodReturnValueHandler> customReturnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();

    private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
    private HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite();

    private ApplicationContext applicationContext;

    private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();

    private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<String, T>();

    private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<Class<?>, AbstractExceptionHandlerMethodResolver>(
            64);

    /**
     * Configure one or more prefixes to match to the destinations of handled messages.
     * Messages whose destination does not start with one of the configured prefixes
     * are ignored. When a destination matches one of the configured prefixes, the
     * matching part is removed from destination before performing a lookup for a matching
     * message handling method. Prefixes without a trailing slash will have one appended
     * automatically.
     * <p>By default the list of prefixes is empty in which case all destinations match.
     */
    public void setDestinationPrefixes(Collection<String> prefixes) {
        this.destinationPrefixes.clear();
        if (prefixes != null) {
            for (String prefix : prefixes) {
                prefix = prefix.trim();
                if (!prefix.endsWith("/")) {
                    prefix += "/";
                }
                this.destinationPrefixes.add(prefix);
            }
        }
    }

    public Collection<String> getDestinationPrefixes() {
        return this.destinationPrefixes;
    }

    /**
     * Sets the list of custom {@code HandlerMethodArgumentResolver}s that will be used
     * after resolvers for supported argument type.
     * @param customArgumentResolvers the list of resolvers; never {@code null}.
     */
    public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> customArgumentResolvers) {
        Assert.notNull(customArgumentResolvers, "The 'customArgumentResolvers' cannot be null.");
        this.customArgumentResolvers = customArgumentResolvers;
    }

    public List<HandlerMethodArgumentResolver> getCustomArgumentResolvers() {
        return this.customArgumentResolvers;
    }

    /**
     * Set the list of custom {@code HandlerMethodReturnValueHandler}s that will be used
     * after return value handlers for known types.
     * @param customReturnValueHandlers the list of custom return value handlers, never {@code null}.
     */
    public void setCustomReturnValueHandlers(List<HandlerMethodReturnValueHandler> customReturnValueHandlers) {
        Assert.notNull(customReturnValueHandlers, "The 'customReturnValueHandlers' cannot be null.");
        this.customReturnValueHandlers = customReturnValueHandlers;
    }

    public List<HandlerMethodReturnValueHandler> getCustomReturnValueHandlers() {
        return this.customReturnValueHandlers;
    }

    /**
     * Configure the complete list of supported argument types effectively overriding
     * the ones configured by default. This is an advanced option. For most use cases
     * it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}.
     */
    public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        if (argumentResolvers == null) {
            this.argumentResolvers.clear();
            return;
        }
        this.argumentResolvers.addResolvers(argumentResolvers);
    }

    public List<HandlerMethodArgumentResolver> getArgumentResolvers() {
        return this.argumentResolvers.getResolvers();
    }

    /**
     * Configure the complete list of supported return value types effectively overriding
     * the ones configured by default. This is an advanced option. For most use cases
     * it should be sufficient to use {@link #setCustomReturnValueHandlers(java.util.List)}
     */
    public void setReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
        if (returnValueHandlers == null) {
            this.returnValueHandlers.clear();
            return;
        }
        this.returnValueHandlers.addHandlers(returnValueHandlers);
    }

    public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
        return this.returnValueHandlers.getReturnValueHandlers();
    }

    /**
     * Return a map with all handler methods and their mappings.
     */
    public Map<T, HandlerMethod> getHandlerMethods() {
        return Collections.unmodifiableMap(this.handlerMethods);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public ApplicationContext getApplicationContext() {
        return this.applicationContext;
    }

    @Override
    public void afterPropertiesSet() {

        if (this.argumentResolvers.getResolvers().isEmpty()) {
            this.argumentResolvers.addResolvers(initArgumentResolvers());
        }

        if (this.returnValueHandlers.getReturnValueHandlers().isEmpty()) {
            this.returnValueHandlers.addHandlers(initReturnValueHandlers());
        }

        for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) {
            if (isHandler(this.applicationContext.getType(beanName))) {
                detectHandlerMethods(beanName);
            }
        }
    }

    /**
     * Return the list of argument resolvers to use. Invoked only if the resolvers
     * have not already been set via {@link #setArgumentResolvers(java.util.List)}.
     * <p>Sub-classes should also take into account custom argument types configured via
     * {@link #setCustomArgumentResolvers(java.util.List)}.
     */
    protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers();

    /**
     * Return the list of return value handlers to use. Invoked only if the return
     * value handlers have not already been set via {@link #setReturnValueHandlers(java.util.List)}.
     * <p>Sub-classes should also take into account custom return value types configured
     * via {@link #setCustomReturnValueHandlers(java.util.List)}.
     */
    protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers();

    /**
     * Whether the given bean type should be introspected for messaging handling methods.
     */
    protected abstract boolean isHandler(Class<?> beanType);

    /**
     * Detect if the given handler has any methods that can handle messages and if
     * so register it with the extracted mapping information.
     * @param handler the handler to check, either an instance of a Spring bean name
     */
    protected final void detectHandlerMethods(Object handler) {

        Class<?> handlerType = (handler instanceof String) ? this.applicationContext.getType((String) handler)
                : handler.getClass();

        final Class<?> userType = ClassUtils.getUserClass(handlerType);

        Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new ReflectionUtils.MethodFilter() {
            @Override
            public boolean matches(Method method) {
                return getMappingForMethod(method, userType) != null;
            }
        });

        for (Method method : methods) {
            T mapping = getMappingForMethod(method, userType);
            registerHandlerMethod(handler, method, mapping);
        }
    }

    /**
     * Provide the mapping for a handler method.
     * @param method the method to provide a mapping for
     * @param handlerType the handler type, possibly a sub-type of the method's declaring class
     * @return the mapping, or {@code null} if the method is not mapped
     */
    protected abstract T getMappingForMethod(Method method, Class<?> handlerType);

    /**
     * Register a handler method and its unique mapping.
     * @param handler the bean name of the handler or the handler instance
     * @param method the method to register
     * @param mapping the mapping conditions associated with the handler method
     * @throws IllegalStateException if another method was already registered
     * under the same mapping
     */
    protected void registerHandlerMethod(Object handler, Method method, T mapping) {

        HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
        HandlerMethod oldHandlerMethod = handlerMethods.get(mapping);

        if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
            throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean()
                    + "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '"
                    + oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
        }

        this.handlerMethods.put(mapping, newHandlerMethod);
        if (logger.isInfoEnabled()) {
            logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
        }

        for (String pattern : getDirectLookupDestinations(mapping)) {
            this.destinationLookup.add(pattern, mapping);
        }
    }

    /**
     * Create a HandlerMethod instance from an Object handler that is either a handler
     * instance or a String-based bean name.
     */
    protected HandlerMethod createHandlerMethod(Object handler, Method method) {
        HandlerMethod handlerMethod;
        if (handler instanceof String) {
            String beanName = (String) handler;
            handlerMethod = new HandlerMethod(beanName, this.applicationContext, method);
        } else {
            handlerMethod = new HandlerMethod(handler, method);
        }
        return handlerMethod;
    }

    /**
     * Return destinations contained in the mapping that are not patterns and are
     * therefore suitable for direct lookups.
     */
    protected abstract Set<String> getDirectLookupDestinations(T mapping);

    @Override
    public void handleMessage(Message<?> message) throws MessagingException {

        String destination = getDestination(message);
        if (destination == null) {
            logger.trace("Ignoring message, no destination");
            return;
        }

        String lookupDestination = getLookupDestination(destination);
        if (lookupDestination == null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Ignoring message to destination=" + destination);
            }
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Handling message, lookupDestination=" + lookupDestination);
        }

        message = MessageBuilder.fromMessage(message).setHeader(LOOKUP_DESTINATION_HEADER, lookupDestination)
                .build();

        handleMessageInternal(message, lookupDestination);
    }

    protected abstract String getDestination(Message<?> message);

    /**
     * Find if the given destination matches any of the configured allowed destination
     * prefixes and if a match is found return the destination with the prefix removed.
     * <p>If no destination prefixes are configured, the destination is returned as is.
     * @return the destination to use to find matching message handling methods
     *       or {@code null} if the destination does not match
     */
    protected String getLookupDestination(String destination) {
        if (destination == null) {
            return null;
        }
        if (CollectionUtils.isEmpty(this.destinationPrefixes)) {
            return destination;
        }
        for (String prefix : this.destinationPrefixes) {
            if (destination.startsWith(prefix)) {
                return destination.substring(prefix.length() - 1);
            }
        }
        return null;
    }

    protected void handleMessageInternal(Message<?> message, String lookupDestination) {

        List<Match> matches = new ArrayList<Match>();

        List<T> mappingsByUrl = this.destinationLookup.get(lookupDestination);
        if (mappingsByUrl != null) {
            addMatchesToCollection(mappingsByUrl, message, matches);
        }

        if (matches.isEmpty()) {
            // No direct hits, go through all mappings
            Set<T> allMappings = this.handlerMethods.keySet();
            addMatchesToCollection(allMappings, message, matches);
        }

        if (matches.isEmpty()) {
            handleNoMatch(handlerMethods.keySet(), lookupDestination, message);
            return;
        }

        Comparator<Match> comparator = new MatchComparator(getMappingComparator(message));
        Collections.sort(matches, comparator);

        if (logger.isTraceEnabled()) {
            logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupDestination + "] : "
                    + matches);
        }

        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            Match secondBestMatch = matches.get(1);
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                Method m1 = bestMatch.handlerMethod.getMethod();
                Method m2 = secondBestMatch.handlerMethod.getMethod();
                throw new IllegalStateException("Ambiguous handler methods mapped for destination '"
                        + lookupDestination + "': {" + m1 + ", " + m2 + "}");
            }
        }

        handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message);
    }

    private void addMatchesToCollection(Collection<T> mappingsToCheck, Message<?> message, List<Match> matches) {
        for (T mapping : mappingsToCheck) {
            T match = getMatchingMapping(mapping, message);
            if (match != null) {
                matches.add(new Match(match, handlerMethods.get(mapping)));
            }
        }
    }

    /**
     * Check if a mapping matches the current message and return a possibly
     * new mapping with conditions relevant to the current request.
     * @param mapping the mapping to get a match for
     * @param message the message being handled
     * @return the match or {@code null} if there is no match
     */
    protected abstract T getMatchingMapping(T mapping, Message<?> message);

    /**
     * Return a comparator for sorting matching mappings.
     * The returned comparator should sort 'better' matches higher.
     * @param message the current Message
     * @return the comparator, never {@code null}
     */
    protected abstract Comparator<T> getMappingComparator(Message<?> message);

    protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookupDestination,
            Message<?> message) {

        if (logger.isDebugEnabled()) {
            logger.debug("Message matched to " + handlerMethod);
        }

        handlerMethod = handlerMethod.createWithResolvedBean();
        InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
        invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);

        try {
            Object returnValue = invocable.invoke(message);

            MethodParameter returnType = handlerMethod.getReturnType();
            if (void.class.equals(returnType.getParameterType())) {
                return;
            }
            this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
        } catch (Exception ex) {
            processHandlerMethodException(handlerMethod, ex, message);
        } catch (Throwable ex) {
            logger.error("Error while processing message " + message, ex);
        }
    }

    protected void processHandlerMethodException(HandlerMethod handlerMethod, Exception ex, Message<?> message) {

        Class<?> beanType = handlerMethod.getBeanType();
        AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
        if (resolver == null) {
            resolver = createExceptionHandlerMethodResolverFor(beanType);
            this.exceptionHandlerCache.put(beanType, resolver);
        }

        Method method = resolver.resolveMethod(ex);
        if (method == null) {
            logger.error("Unhandled exception", ex);
            return;
        }

        InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod.getBean(), method);
        invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);

        try {
            Object returnValue = invocable.invoke(message, ex);

            MethodParameter returnType = invocable.getReturnType();
            if (void.class.equals(returnType.getParameterType())) {
                return;
            }
            this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
        } catch (Throwable t) {
            logger.error("Error while handling exception", t);
            return;
        }

    }

    protected abstract AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(
            Class<?> beanType);

    protected void handleNoMatch(Set<T> ts, String lookupDestination, Message<?> message) {
        if (logger.isDebugEnabled()) {
            logger.debug("No matching method found");
        }
    }

    /**
     * A thin wrapper around a matched HandlerMethod and its matched mapping for
     * the purpose of comparing the best match with a comparator in the context
     * of a message.
     */
    private class Match {

        private final T mapping;

        private final HandlerMethod handlerMethod;

        private Match(T mapping, HandlerMethod handlerMethod) {
            this.mapping = mapping;
            this.handlerMethod = handlerMethod;
        }

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

    private class MatchComparator implements Comparator<Match> {

        private final Comparator<T> comparator;

        public MatchComparator(Comparator<T> comparator) {
            this.comparator = comparator;
        }

        @Override
        public int compare(Match match1, Match match2) {
            return this.comparator.compare(match1.mapping, match2.mapping);
        }
    }

}