/* * 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.simp.handler; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; 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.core.annotation.AnnotationUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.support.MessageBodyArgumentResolver; import org.springframework.messaging.handler.annotation.support.MessageExceptionHandlerMethodResolver; import org.springframework.messaging.handler.method.InvocableMessageHandlerMethod; import org.springframework.messaging.handler.method.MessageArgumentResolverComposite; import org.springframework.messaging.handler.method.MessageReturnValueHandlerComposite; import org.springframework.messaging.simp.MessageHolder; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.annotation.SubscribeEvent; import org.springframework.messaging.simp.annotation.UnsubscribeEvent; import org.springframework.messaging.simp.annotation.support.MessageSendingReturnValueHandler; import org.springframework.messaging.simp.annotation.support.PrincipalMessageArgumentResolver; import org.springframework.messaging.support.converter.MessageConverter; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; /** * @author Rossen Stoyanchev * @since 4.0 */ public class AnnotationSimpMessageHandler extends AbstractSimpMessageHandler implements ApplicationContextAware, InitializingBean { private final MessageChannel outboundChannel; private MessageConverter messageConverter; private ApplicationContext applicationContext; private Map messageMethods = new HashMap(); private Map subscribeMethods = new HashMap(); private Map unsubscribeMethods = new HashMap(); private final Map, MessageExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap, MessageExceptionHandlerMethodResolver>(64); private MessageArgumentResolverComposite argumentResolvers = new MessageArgumentResolverComposite(); private MessageReturnValueHandlerComposite returnValueHandlers = new MessageReturnValueHandlerComposite(); /** * @param inboundChannel a channel for processing incoming messages from clients * @param outboundChannel a channel for messages going out to clients */ public AnnotationSimpMessageHandler(MessageChannel outboundChannel) { Assert.notNull(outboundChannel, "outboundChannel is required"); this.outboundChannel = outboundChannel; } /** * TODO: multiple converters with 'content-type' header */ public void setMessageConverter(MessageConverter converter) { this.messageConverter = converter; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override protected Collection getSupportedMessageTypes() { return Arrays.asList(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE, SimpMessageType.UNSUBSCRIBE); } @Override public void afterPropertiesSet() { initHandlerMethods(); this.argumentResolvers.addResolver(new PrincipalMessageArgumentResolver()); this.argumentResolvers.addResolver(new MessageBodyArgumentResolver(this.messageConverter)); this.returnValueHandlers.addHandler( new MessageSendingReturnValueHandler(this.outboundChannel, this.messageConverter)); } protected void initHandlerMethods() { String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (isHandler(this.applicationContext.getType(beanName))){ detectHandlerMethods(beanName); } } } protected boolean isHandler(Class beanType) { return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) || (AnnotationUtils.findAnnotation(beanType, MessageMapping.class) != null)); } protected void detectHandlerMethods(Object handler) { Class handlerType = (handler instanceof String) ? this.applicationContext.getType((String) handler) : handler.getClass(); final Class userType = ClassUtils.getUserClass(handlerType); initHandlerMethods(handler, userType, MessageMapping.class, new MessageMappingInfoCreator(), this.messageMethods); initHandlerMethods(handler, userType, SubscribeEvent.class, new SubscribeMappingInfoCreator(), this.subscribeMethods); initHandlerMethods(handler, userType, UnsubscribeEvent.class, new UnsubscribeMappingInfoCreator(), this.unsubscribeMethods); } private void initHandlerMethods(Object handler, Class handlerType, final Class annotationType, MappingInfoCreator mappingInfoCreator, Map handlerMethods) { Set messageMethods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() { @Override public boolean matches(Method method) { return AnnotationUtils.findAnnotation(method, annotationType) != null; } }); for (Method method : messageMethods) { A annotation = AnnotationUtils.findAnnotation(method, annotationType); HandlerMethod hm = createHandlerMethod(handler, method); handlerMethods.put(mappingInfoCreator.create(annotation), hm); } } 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; } @Override public void handlePublish(Message message) { handleMessageInternal(message, this.messageMethods); } @Override public void handleSubscribe(Message message) { handleMessageInternal(message, this.subscribeMethods); } @Override public void handleUnsubscribe(Message message) { handleMessageInternal(message, this.unsubscribeMethods); } private void handleMessageInternal(final Message message, Map handlerMethods) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); String destination = headers.getDestination(); HandlerMethod match = getHandlerMethod(destination, handlerMethods); if (match == null) { return; } HandlerMethod handlerMethod = match.createWithResolvedBean(); // TODO: avoid re-creating invocableHandlerMethod InvocableMessageHandlerMethod invocableHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod); invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); try { MessageHolder.setMessage(message); Object value = invocableHandlerMethod.invoke(message); MethodParameter returnType = handlerMethod.getReturnType(); if (void.class.equals(returnType.getParameterType())) { return; } this.returnValueHandlers.handleReturnValue(value, returnType, message); } catch (Exception ex) { invokeExceptionHandler(message, handlerMethod, ex); } catch (Throwable ex) { // TODO ex.printStackTrace(); } finally { MessageHolder.reset(); } } private void invokeExceptionHandler(Message message, HandlerMethod handlerMethod, Exception ex) { InvocableMessageHandlerMethod invocableHandlerMethod; Class beanType = handlerMethod.getBeanType(); MessageExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType); if (resolver == null) { resolver = new MessageExceptionHandlerMethodResolver(beanType); this.exceptionHandlerCache.put(beanType, resolver); } Method method = resolver.resolveMethod(ex); if (method == null) { logger.error("Unhandled exception", ex); return; } invocableHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod.getBean(), method); invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); try { invocableHandlerMethod.invoke(message, ex); } catch (Throwable t) { logger.error("Error while handling exception", t); return; } } protected HandlerMethod getHandlerMethod(String destination, Map handlerMethods) { for (MappingInfo key : handlerMethods.keySet()) { for (String mappingDestination : key.getDestinations()) { if (destination.equals(mappingDestination)) { return handlerMethods.get(key); } } } return null; } private static class MappingInfo { private final List destinations; public MappingInfo(List destinations) { this.destinations = destinations; } public List getDestinations() { return this.destinations; } @Override public String toString() { return "MappingInfo [destinations=" + this.destinations + "]"; } } private interface MappingInfoCreator { MappingInfo create(A annotation); } private static class MessageMappingInfoCreator implements MappingInfoCreator { @Override public MappingInfo create(MessageMapping annotation) { return new MappingInfo(Arrays.asList(annotation.value())); } } private static class SubscribeMappingInfoCreator implements MappingInfoCreator { @Override public MappingInfo create(SubscribeEvent annotation) { return new MappingInfo(Arrays.asList(annotation.value())); } } private static class UnsubscribeMappingInfoCreator implements MappingInfoCreator { @Override public MappingInfo create(UnsubscribeEvent annotation) { return new MappingInfo(Arrays.asList(annotation.value())); } } }