001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.messaging.handler.invocation.reactive;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.function.Function;
030import java.util.function.Predicate;
031import java.util.stream.Collectors;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import reactor.core.publisher.Mono;
036
037import org.springframework.beans.factory.BeanNameAware;
038import org.springframework.beans.factory.InitializingBean;
039import org.springframework.context.ApplicationContext;
040import org.springframework.context.ApplicationContextAware;
041import org.springframework.core.MethodIntrospector;
042import org.springframework.core.ReactiveAdapterRegistry;
043import org.springframework.lang.Nullable;
044import org.springframework.messaging.Message;
045import org.springframework.messaging.MessagingException;
046import org.springframework.messaging.ReactiveMessageHandler;
047import org.springframework.messaging.handler.HandlerMethod;
048import org.springframework.messaging.handler.MessagingAdviceBean;
049import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
050import org.springframework.util.Assert;
051import org.springframework.util.ClassUtils;
052import org.springframework.util.CollectionUtils;
053import org.springframework.util.LinkedMultiValueMap;
054import org.springframework.util.MultiValueMap;
055import org.springframework.util.ObjectUtils;
056import org.springframework.util.RouteMatcher;
057
058/**
059 * Abstract base class for reactive HandlerMethod-based message handling.
060 * Provides most of the logic required to discover handler methods at startup,
061 * find a matching handler method at runtime for a given message and invoke it.
062 *
063 * <p>Also supports discovering and invoking exception handling methods to process
064 * exceptions raised during message handling.
065 *
066 * @author Rossen Stoyanchev
067 * @since 5.2
068 * @param <T> the type of the Object that contains information mapping information
069 */
070public abstract class AbstractMethodMessageHandler<T>
071                implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean, BeanNameAware {
072
073        /**
074         * Bean name prefix for target beans behind scoped proxies. Used to exclude those
075         * targets from handler method detection, in favor of the corresponding proxies.
076         * <p>We're not checking the autowire-candidate status here, which is how the
077         * proxy target filtering problem is being handled at the autowiring level,
078         * since autowire-candidate may have been turned to {@code false} for other
079         * reasons, while still expecting the bean to be eligible for handler methods.
080         * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils}
081         * but duplicated here to avoid a hard dependency on the spring-aop module.
082         */
083        private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
084
085
086        protected final Log logger = LogFactory.getLog(getClass());
087
088
089        @Nullable
090        private Predicate<Class<?>> handlerPredicate;
091
092        @Nullable
093        List<Object> handlers;
094
095        private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer();
096
097        private ReturnValueHandlerConfigurer returnValueHandlerConfigurer = new ReturnValueHandlerConfigurer();
098
099        private final InvocableHelper invocableHelper = new InvocableHelper(this::createExceptionMethodResolverFor);
100
101        @Nullable
102        private ApplicationContext applicationContext;
103
104        @Nullable
105        private String beanName;
106
107        private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<>(64);
108
109        private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<>(64);
110
111
112        /**
113         * Configure a predicate for selecting which Spring beans to check for the
114         * presence of message handler methods.
115         * <p>This is not set by default. However sub-classes may initialize it to
116         * some default strategy (e.g. {@code @Controller} classes).
117         * @see #setHandlers(List)
118         */
119        public void setHandlerPredicate(@Nullable Predicate<Class<?>> handlerPredicate) {
120                this.handlerPredicate = handlerPredicate;
121        }
122
123        /**
124         * Return the {@link #setHandlerPredicate configured} handler predicate.
125         */
126        @Nullable
127        public Predicate<Class<?>> getHandlerPredicate() {
128                return this.handlerPredicate;
129        }
130
131        /**
132         * Manually configure the handlers to check for the presence of message
133         * handling methods, which also disables auto-detection via a
134         * {@link #setHandlerPredicate(Predicate) handlerPredicate}. If you do not
135         * want to disable auto-detection, then call this method first, and then set
136         * the handler predicate.
137         * @param handlers the handlers to check
138         */
139        public void setHandlers(List<Object> handlers) {
140                this.handlers = handlers;
141                this.handlerPredicate = null;
142        }
143
144        /**
145         * Configure custom resolvers for handler method arguments.
146         */
147        public void setArgumentResolverConfigurer(ArgumentResolverConfigurer configurer) {
148                Assert.notNull(configurer, "HandlerMethodArgumentResolver is required");
149                this.argumentResolverConfigurer = configurer;
150        }
151
152        /**
153         * Return the configured custom resolvers for handler method arguments.
154         */
155        public ArgumentResolverConfigurer getArgumentResolverConfigurer() {
156                return this.argumentResolverConfigurer;
157        }
158
159        /**
160         * Configure custom return value handlers for handler methods.
161         */
162        public void setReturnValueHandlerConfigurer(ReturnValueHandlerConfigurer configurer) {
163                Assert.notNull(configurer, "ReturnValueHandlerConfigurer is required");
164                this.returnValueHandlerConfigurer = configurer;
165        }
166
167        /**
168         * Return the configured return value handlers.
169         */
170        public ReturnValueHandlerConfigurer getReturnValueHandlerConfigurer() {
171                return this.returnValueHandlerConfigurer;
172        }
173
174        /**
175         * Configure the registry for adapting various reactive types.
176         * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
177         * default settings.
178         */
179        public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
180                this.invocableHelper.setReactiveAdapterRegistry(registry);
181        }
182
183        /**
184         * Return the configured registry for adapting reactive types.
185         */
186        public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
187                return this.invocableHelper.getReactiveAdapterRegistry();
188        }
189
190        @Override
191        public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
192                this.applicationContext = applicationContext;
193        }
194
195        @Nullable
196        public ApplicationContext getApplicationContext() {
197                return this.applicationContext;
198        }
199
200        @Override
201        public void setBeanName(String name) {
202                this.beanName = name;
203        }
204
205        public String getBeanName() {
206                return (this.beanName != null ? this.beanName :
207                                getClass().getSimpleName() + "@" + ObjectUtils.getIdentityHexString(this));
208        }
209
210        /**
211         * Subclasses can invoke this method to populate the MessagingAdviceBean cache
212         * (e.g. to support "global" {@code @MessageExceptionHandler}).
213         */
214        protected void registerExceptionHandlerAdvice(
215                        MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) {
216
217                this.invocableHelper.registerExceptionHandlerAdvice(bean, resolver);
218        }
219
220        /**
221         * Return a read-only map with all handler methods and their mappings.
222         */
223        public Map<T, HandlerMethod> getHandlerMethods() {
224                return Collections.unmodifiableMap(this.handlerMethods);
225        }
226
227        /**
228         * Return a read-only multi-value map with a direct lookup of mappings,
229         * (e.g. for non-pattern destinations).
230         */
231        public MultiValueMap<String, T> getDestinationLookup() {
232                return CollectionUtils.unmodifiableMultiValueMap(this.destinationLookup);
233        }
234
235        /**
236         * Return the argument resolvers initialized during {@link #afterPropertiesSet()}.
237         * Primarily for internal use in sub-classes.
238         * @since 5.2.2
239         */
240        protected HandlerMethodArgumentResolverComposite getArgumentResolvers() {
241                return this.invocableHelper.getArgumentResolvers();
242        }
243
244
245        @Override
246        public void afterPropertiesSet() {
247
248                List<? extends HandlerMethodArgumentResolver> resolvers = initArgumentResolvers();
249                if (resolvers.isEmpty()) {
250                        resolvers = new ArrayList<>(this.argumentResolverConfigurer.getCustomResolvers());
251                }
252                this.invocableHelper.addArgumentResolvers(resolvers);
253
254                List<? extends HandlerMethodReturnValueHandler> handlers = initReturnValueHandlers();
255                if (handlers.isEmpty()) {
256                        handlers = new ArrayList<>(this.returnValueHandlerConfigurer.getCustomHandlers());
257                }
258                this.invocableHelper.addReturnValueHandlers(handlers);
259
260                initHandlerMethods();
261        }
262
263        /**
264         * Return the list of argument resolvers to use.
265         * <p>Subclasses should also take into account custom argument types configured via
266         * {@link #setArgumentResolverConfigurer}.
267         */
268        protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers();
269
270        /**
271         * Return the list of return value handlers to use.
272         * <p>Subclasses should also take into account custom return value types configured
273         * via {@link #setReturnValueHandlerConfigurer}.
274         */
275        protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers();
276
277
278        private void initHandlerMethods() {
279                if (this.handlers != null) {
280                        for (Object handler : this.handlers) {
281                                detectHandlerMethods(handler);
282                        }
283                }
284                Predicate<Class<?>> predicate = this.handlerPredicate;
285                if (predicate == null) {
286                        if (logger.isDebugEnabled()) {
287                                logger.debug("[" + getBeanName() + "] Skip auto-detection of message handling methods");
288                        }
289                        return;
290                }
291                if (this.applicationContext == null) {
292                        logger.warn("No ApplicationContext for auto-detection of beans with message handling methods.");
293                        return;
294                }
295                for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) {
296                        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
297                                Class<?> beanType = null;
298                                try {
299                                        beanType = this.applicationContext.getType(beanName);
300                                }
301                                catch (Throwable ex) {
302                                        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
303                                        if (logger.isDebugEnabled()) {
304                                                logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
305                                        }
306                                }
307                                if (beanType != null && predicate.test(beanType)) {
308                                        detectHandlerMethods(beanName);
309                                }
310                        }
311                }
312        }
313
314        /**
315         * Detect if the given handler has any methods that can handle messages and if
316         * so register it with the extracted mapping information.
317         * <p><strong>Note:</strong> This method is protected and can be invoked by
318         * subclasses, but this should be done on startup only as documented in
319         * {@link #registerHandlerMethod}.
320         * @param handler the handler to check, either an instance of a Spring bean name
321         */
322        protected final void detectHandlerMethods(Object handler) {
323                Class<?> handlerType;
324                if (handler instanceof String) {
325                        ApplicationContext context = getApplicationContext();
326                        Assert.state(context != null, "ApplicationContext is required for resolving handler bean names");
327                        handlerType = context.getType((String) handler);
328                }
329                else {
330                        handlerType = handler.getClass();
331                }
332                if (handlerType != null) {
333                        final Class<?> userType = ClassUtils.getUserClass(handlerType);
334                        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
335                                        (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
336                        if (logger.isDebugEnabled()) {
337                                logger.debug(formatMappings(userType, methods));
338                        }
339                        methods.forEach((key, value) -> registerHandlerMethod(handler, key, value));
340                }
341        }
342
343        private String formatMappings(Class<?> userType, Map<Method, T> methods) {
344                String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\."))
345                                .map(p -> p.substring(0, 1))
346                                .collect(Collectors.joining(".", "", "." + userType.getSimpleName()));
347                Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes())
348                                .map(Class::getSimpleName)
349                                .collect(Collectors.joining(",", "(", ")"));
350                return methods.entrySet().stream()
351                                .map(e -> {
352                                        Method method = e.getKey();
353                                        return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
354                                })
355                                .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
356        }
357
358        /**
359         * Obtain the mapping for the given method, if any.
360         * @param method the method to check
361         * @param handlerType the handler type, possibly a sub-type of the method's declaring class
362         * @return the mapping, or {@code null} if the method is not mapped
363         */
364        @Nullable
365        protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
366
367        /**
368         * Register a handler method and its unique mapping.
369         * <p><strong>Note:</strong> This method is protected and can be invoked by
370         * subclasses. Keep in mind however that the registration is not protected
371         * for concurrent use, and is expected to be done on startup.
372         * @param handler the bean name of the handler or the handler instance
373         * @param method the method to register
374         * @param mapping the mapping conditions associated with the handler method
375         * @throws IllegalStateException if another method was already registered
376         * under the same mapping
377         */
378        protected final void registerHandlerMethod(Object handler, Method method, T mapping) {
379                Assert.notNull(mapping, "Mapping must not be null");
380                HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
381                HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
382
383                if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
384                        throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() +
385                                        "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
386                                        oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
387                }
388
389                mapping = extendMapping(mapping, newHandlerMethod);
390                this.handlerMethods.put(mapping, newHandlerMethod);
391
392                for (String pattern : getDirectLookupMappings(mapping)) {
393                        this.destinationLookup.add(pattern, mapping);
394                }
395        }
396
397        /**
398         * Create a HandlerMethod instance from an Object handler that is either a handler
399         * instance or a String-based bean name.
400         */
401        private HandlerMethod createHandlerMethod(Object handler, Method method) {
402                HandlerMethod handlerMethod;
403                if (handler instanceof String) {
404                        ApplicationContext context = getApplicationContext();
405                        Assert.state(context != null, "ApplicationContext is required for resolving handler bean names");
406                        String beanName = (String) handler;
407                        handlerMethod = new HandlerMethod(beanName, context.getAutowireCapableBeanFactory(), method);
408                }
409                else {
410                        handlerMethod = new HandlerMethod(handler, method);
411                }
412                return handlerMethod;
413        }
414
415        /**
416         * This method is invoked just before mappings are added. It allows
417         * sub-classes to update the mapping with the {@link HandlerMethod} in mind.
418         * This can be useful when the method signature is used to refine the
419         * mapping, e.g. based on the cardinality of input and output.
420         * <p>By default this method returns the mapping that is passed in.
421         * @param mapping the mapping to be added
422         * @param handlerMethod the target handler for the mapping
423         * @return a new mapping or the same
424         * @since 5.2.2
425         */
426        protected T extendMapping(T mapping, HandlerMethod handlerMethod) {
427                return mapping;
428        }
429
430        /**
431         * Return String-based destinations for the given mapping, if any, that can
432         * be used to find matches with a direct lookup (i.e. non-patterns).
433         * <p><strong>Note:</strong> This is completely optional. The mapping
434         * metadata for a subclass may support neither direct lookups, nor String
435         * based destinations.
436         */
437        protected abstract Set<String> getDirectLookupMappings(T mapping);
438
439
440        @Override
441        public Mono<Void> handleMessage(Message<?> message) throws MessagingException {
442                Match<T> match = null;
443                try {
444                        match = getHandlerMethod(message);
445                }
446                catch (Exception ex) {
447                        return Mono.error(ex);
448                }
449                if (match == null) {
450                        // handleNoMatch would have been invoked already
451                        return Mono.empty();
452                }
453                return handleMatch(match.mapping, match.handlerMethod, message);
454        }
455
456        protected Mono<Void> handleMatch(T mapping, HandlerMethod handlerMethod, Message<?> message) {
457                handlerMethod = handlerMethod.createWithResolvedBean();
458                return this.invocableHelper.handleMessage(handlerMethod, message);
459        }
460
461        @Nullable
462        private Match<T> getHandlerMethod(Message<?> message) {
463                List<Match<T>> matches = new ArrayList<>();
464
465                RouteMatcher.Route destination = getDestination(message);
466                List<T> mappingsByUrl = (destination != null ? this.destinationLookup.get(destination.value()) : null);
467                if (mappingsByUrl != null) {
468                        addMatchesToCollection(mappingsByUrl, message, matches);
469                }
470                if (matches.isEmpty()) {
471                        // No direct hits, go through all mappings
472                        Set<T> allMappings = this.handlerMethods.keySet();
473                        addMatchesToCollection(allMappings, message, matches);
474                }
475                if (matches.isEmpty()) {
476                        handleNoMatch(destination, message);
477                        return null;
478                }
479                Comparator<Match<T>> comparator = new MatchComparator(getMappingComparator(message));
480                matches.sort(comparator);
481                if (logger.isTraceEnabled()) {
482                        logger.trace("Found " + matches.size() + " handler methods: " + matches);
483                }
484                Match<T> bestMatch = matches.get(0);
485                if (matches.size() > 1) {
486                        Match<T> secondBestMatch = matches.get(1);
487                        if (comparator.compare(bestMatch, secondBestMatch) == 0) {
488                                HandlerMethod m1 = bestMatch.handlerMethod;
489                                HandlerMethod m2 = secondBestMatch.handlerMethod;
490                                throw new IllegalStateException("Ambiguous handler methods mapped for destination '" +
491                                                (destination != null ? destination.value() : "") + "': {" +
492                                                m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}");
493                        }
494                }
495                return bestMatch;
496        }
497
498        /**
499         * Extract the destination from the given message.
500         * @see #getDirectLookupMappings(Object)
501         */
502        @Nullable
503        protected abstract RouteMatcher.Route getDestination(Message<?> message);
504
505        private void addMatchesToCollection(
506                        Collection<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) {
507
508                for (T mapping : mappingsToCheck) {
509                        T match = getMatchingMapping(mapping, message);
510                        if (match != null) {
511                                matches.add(new Match<T>(match, this.handlerMethods.get(mapping)));
512                        }
513                }
514        }
515
516        /**
517         * Check if a mapping matches the current message and return a possibly
518         * new mapping with conditions relevant to the current request.
519         * @param mapping the mapping to get a match for
520         * @param message the message being handled
521         * @return the match or {@code null} if there is no match
522         */
523        @Nullable
524        protected abstract T getMatchingMapping(T mapping, Message<?> message);
525
526        /**
527         * Return a comparator for sorting matching mappings.
528         * The returned comparator should sort 'better' matches higher.
529         * @param message the current Message
530         * @return the comparator, never {@code null}
531         */
532        protected abstract Comparator<T> getMappingComparator(Message<?> message);
533
534        /**
535         * Invoked when no matching handler is found.
536         * @param destination the destination
537         * @param message the message
538         */
539        protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message<?> message) {
540                logger.debug("No handlers for destination '" +
541                                (destination != null ? destination.value() : "") + "'");
542        }
543
544        /**
545         * Create a concrete instance of {@link AbstractExceptionHandlerMethodResolver}
546         * that finds exception handling methods based on some criteria, e.g. based
547         * on the presence of {@code @MessageExceptionHandler}.
548         * @param beanType the class in which an exception occurred during handling
549         * @return the resolver to use
550         */
551        protected abstract AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType);
552
553
554        /**
555         * Container for matched mapping and HandlerMethod. Used for best match
556         * comparison and for access to mapping information.
557         */
558        private static class Match<T> {
559
560                private final T mapping;
561
562                private final HandlerMethod handlerMethod;
563
564                Match(T mapping, HandlerMethod handlerMethod) {
565                        this.mapping = mapping;
566                        this.handlerMethod = handlerMethod;
567                }
568
569                @Override
570                public String toString() {
571                        return this.mapping.toString();
572                }
573        }
574
575
576        private class MatchComparator implements Comparator<Match<T>> {
577
578                private final Comparator<T> comparator;
579
580                MatchComparator(Comparator<T> comparator) {
581                        this.comparator = comparator;
582                }
583
584                @Override
585                public int compare(Match<T> match1, Match<T> match2) {
586                        return this.comparator.compare(match1.mapping, match2.mapping);
587                }
588        }
589
590}