From 41e437066ea635910d6972cb48e121a2d981227a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 18 Mar 2015 11:22:42 -0400 Subject: [PATCH] Support @MessageExceptionHandler w/ @ControllerAdvice This change adds support for global @MessageExceptionHandler methods with STOMP over WebSocket messages. Such methods can be added to @ControllerAdvice annotated components, much like @ExceptionHandler methods for Spring MVC. Issue: SPR-12696 --- .../handler/MessagingAdviceBean.java | 55 ++++++++ .../AbstractMethodMessageHandler.java | 78 +++++++++-- .../AbstractMessageBrokerConfiguration.java | 17 ++- .../MessageBrokerBeanDefinitionParser.java | 10 +- ...cketMessageBrokerConfigurationSupport.java | 10 +- ...bSocketAnnotationMethodMessageHandler.java | 107 ++++++++++++++++ .../socket/config/spring-websocket-4.1.xsd | 4 +- ...etAnnotationMethodMessageHandlerTests.java | 121 ++++++++++++++++++ 8 files changed, 377 insertions(+), 25 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandler.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandlerTests.java diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java new file mode 100644 index 0000000000..5042c64003 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 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; + +import org.springframework.core.Ordered; + +/** + * Represents a Spring-managed bean with cross-cutting functionality to be + * applied to one or more Spring beans with annotation-based message + * handling methods. + * + *

Component stereotypes such as + * {@link org.springframework.stereotype.Controller @Controller} with annotation + * handler methods often need cross-cutting functionality across all or a subset + * of such annotated components. A primary example of this is the need for "global" + * annotated exception handler methods but the concept applies more generally. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface MessagingAdviceBean extends Ordered { + + /** + * Return the type of the contained advice bean. + *

If the bean type is a CGLIB-generated class, the original user-defined + * class is returned. + */ + Class getBeanType(); + + /** + * Return the advice bean instance, if necessary resolving a bean specified + * by name through the BeanFactory. + */ + Object resolveBean(); + + /** + * Whether this {@link MessagingAdviceBean} applies to the given bean type. + * @param beanType the type of the bean to check + */ + boolean isApplicableToBeanType(Class beanType); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index c7bef5bf62..e14f0b07e7 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -41,6 +41,8 @@ import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.messaging.handler.HandlerMethodSelector; +import org.springframework.messaging.handler.MessagingAdviceBean; +import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver; import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.util.ClassUtils; @@ -87,6 +89,9 @@ public abstract class AbstractMethodMessageHandler private final Map, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap, AbstractExceptionHandlerMethodResolver>(64); + private final Map exceptionHandlerAdviceCache = + new LinkedHashMap(64); + /** * When this property is configured only messages to destinations matching @@ -327,6 +332,25 @@ public abstract class AbstractMethodMessageHandler */ protected abstract Set getDirectLookupDestinations(T mapping); + /** + * Sub-classes can invoke this method to populate the MessagingAdviceBean cache + * (e.g. to support "global" {@code @MessageExceptionHandler}). + * @since 4.2 + */ + protected void initMessagingAdviceCache(List beans) { + if (beans == null) { + return; + } + for (MessagingAdviceBean bean : beans) { + Class beanType = bean.getBeanType(); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(beanType); + if (resolver.hasExceptionMappings()) { + this.exceptionHandlerAdviceCache.put(bean, resolver); + logger.info("Detected @MessageExceptionHandler methods in " + bean); + } + } + } + @Override public void handleMessage(Message message) throws MessagingException { @@ -464,21 +488,11 @@ public abstract class AbstractMethodMessageHandler } protected void processHandlerMethodException(HandlerMethod handlerMethod, Exception ex, Message message) { - if (logger.isDebugEnabled()) { - logger.debug("Searching methods to handle " + ex.getClass().getSimpleName()); - } - 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) { + InvocableHandlerMethod invocable = getExceptionHandlerMethod(handlerMethod, ex); + if (invocable == null) { logger.error("Unhandled exception", ex); return; } - InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod.getBean(), method); invocable.setMessageMethodArgumentResolvers(this.argumentResolvers); if (logger.isDebugEnabled()) { logger.debug("Invoking " + invocable.getShortLogMessage()); @@ -499,6 +513,44 @@ public abstract class AbstractMethodMessageHandler protected abstract AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class beanType); + /** + * Find an {@code @MessageExceptionHandler} method for the given exception. + * The default implementation searches methods in the class hierarchy of the + * HandlerMethod first and if not found, it continues searching for additional + * {@code @MessageExceptionHandler} methods among the configured + * {@linkplain org.springframework.messaging.handler.MessagingAdviceBean + * MessagingAdviceBean}, if any. + * @param handlerMethod the method where the exception was raised + * @param exception the raised exception + * @return a method to handle the exception, or {@code null} + * @since 4.2 + */ + protected InvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { + if (logger.isDebugEnabled()) { + logger.debug("Searching methods to handle " + exception.getClass().getSimpleName()); + } + 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(exception); + if (method != null) { + return new InvocableHandlerMethod(handlerMethod.getBean(), method); + } + for (MessagingAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) { + if (advice.isApplicableToBeanType(beanType)) { + resolver = this.exceptionHandlerAdviceCache.get(advice); + method = resolver.resolveMethod(exception); + if (method != null) { + return new InvocableHandlerMethod(advice.resolveBean(), method); + } + } + } + return null; + } + protected void handleNoMatch(Set ts, String lookupDestination, Message message) { if (logger.isDebugEnabled()) { logger.debug("No matching methods."); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index 91632a2af0..21391ab957 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -225,9 +225,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC @Bean public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { - SimpAnnotationMethodMessageHandler handler = new SimpAnnotationMethodMessageHandler( - clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate()); - + SimpAnnotationMethodMessageHandler handler = createAnnotationMethodMessageHandler(); handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes()); handler.setMessageConverter(brokerMessageConverter()); handler.setValidator(simpValidator()); @@ -247,6 +245,17 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC return handler; } + /** + * Protected method for plugging in a custom sub-class of + * {@link org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler + * SimpAnnotationMethodMessageHandler}. + * @since 4.2 + */ + protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler() { + return new SimpAnnotationMethodMessageHandler(clientInboundChannel(), + clientOutboundChannel(), brokerMessagingTemplate()); + } + protected void addArgumentResolvers(List argumentResolvers) { } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java index cbb9353d98..db7d530083 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java @@ -21,9 +21,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean; -import org.springframework.messaging.support.ImmutableMessageChannelInterceptor; import org.w3c.dom.Element; import org.springframework.beans.MutablePropertyValues; @@ -34,11 +31,13 @@ import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean; import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.DefaultContentTypeResolver; @@ -46,13 +45,13 @@ import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpSessionScope; -import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; import org.springframework.messaging.simp.user.DefaultUserDestinationResolver; import org.springframework.messaging.simp.user.DefaultUserSessionRegistry; import org.springframework.messaging.simp.user.UserDestinationMessageHandler; import org.springframework.messaging.support.ExecutorSubscribableChannel; +import org.springframework.messaging.support.ImmutableMessageChannelInterceptor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -64,6 +63,7 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory; import org.springframework.web.socket.messaging.StompSubProtocolHandler; import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler; +import org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler; import org.springframework.web.socket.server.support.OriginHandshakeInterceptor; import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler; import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler; @@ -426,7 +426,7 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { values.add("destinationPrefixes", Arrays.asList(StringUtils.tokenizeToStringArray(prefixAttribute, ","))); values.add("messageConverter", converter); - RootBeanDefinition beanDef = new RootBeanDefinition(SimpAnnotationMethodMessageHandler.class, cavs, values); + RootBeanDefinition beanDef = new RootBeanDefinition(WebSocketAnnotationMethodMessageHandler.class, cavs, values); if (messageBrokerElement.hasAttribute("path-matcher")) { String pathMatcherRef = messageBrokerElement.getAttribute("path-matcher"); beanDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef)); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java index df93f746f7..0afefe9b50 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.SimpSessionScope; +import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler; import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; @@ -30,6 +31,7 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.WebSocketMessageBrokerStats; import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory; import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler; +import org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler; /** * Extends {@link AbstractMessageBrokerConfiguration} and adds configuration for @@ -48,6 +50,12 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac private WebSocketTransportRegistration transportRegistration; + @Override + protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler() { + return new WebSocketAnnotationMethodMessageHandler(clientInboundChannel(), + clientOutboundChannel(), brokerMessagingTemplate()); + } + @Bean public HandlerMapping stompWebSocketHandlerMapping() { WebSocketHandler handler = subProtocolWebSocketHandler(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandler.java new file mode 100644 index 0000000000..5660b4a094 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2015 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.web.socket.messaging; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.messaging.handler.MessagingAdviceBean; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; +import org.springframework.web.method.ControllerAdviceBean; + +/** + * A sub-class of {@link SimpAnnotationMethodMessageHandler} to provide support + * for {@link org.springframework.web.bind.annotation.ControllerAdvice + * ControllerAdvice} with global {@code @MessageExceptionHandler} methods. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public class WebSocketAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { + + + public WebSocketAnnotationMethodMessageHandler(SubscribableChannel clientInChannel, MessageChannel clientOutChannel, + SimpMessageSendingOperations brokerTemplate) { + + super(clientInChannel, clientOutChannel, brokerTemplate); + } + + + @Override + public void afterPropertiesSet() { + initControllerAdviceCache(); + super.afterPropertiesSet(); + } + + private void initControllerAdviceCache() { + if (getApplicationContext() == null) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for @MessageExceptionHandler mappings: " + getApplicationContext()); + } + List controllerAdvice = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); + AnnotationAwareOrderComparator.sort(controllerAdvice); + initMessagingAdviceCache(MessagingControllerAdviceBean.createFromList(controllerAdvice)); + } + + + /** + * Adapt ControllerAdviceBean to MessagingAdviceBean. + */ + private static class MessagingControllerAdviceBean implements MessagingAdviceBean { + + private final ControllerAdviceBean adviceBean; + + + private MessagingControllerAdviceBean(ControllerAdviceBean adviceBean) { + this.adviceBean = adviceBean; + } + + public static List createFromList(List controllerAdvice) { + List messagingAdvice = new ArrayList(controllerAdvice.size()); + for (ControllerAdviceBean bean : controllerAdvice) { + messagingAdvice.add(new MessagingControllerAdviceBean(bean)); + } + return messagingAdvice; + } + + @Override + public Class getBeanType() { + return this.adviceBean.getBeanType(); + } + + @Override + public Object resolveBean() { + return this.adviceBean.resolveBean(); + } + + @Override + public boolean isApplicableToBeanType(Class beanType) { + return this.adviceBean.isApplicableToBeanType(beanType); + } + + @Override + public int getOrder() { + return this.adviceBean.getOrder(); + } + } + +} diff --git a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd index 740c22b424..e020d467e6 100644 --- a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd +++ b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd @@ -696,7 +696,7 @@ @@ -728,7 +728,7 @@ diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandlerTests.java new file mode 100644 index 0000000000..e67f531304 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketAnnotationMethodMessageHandlerTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2015 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.web.socket.messaging; + +import static org.junit.Assert.*; + +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ControllerAdvice; + + +/** + * Unit tests for {@link WebSocketAnnotationMethodMessageHandler}. + * @author Rossen Stoyanchev + */ +public class WebSocketAnnotationMethodMessageHandlerTests { + + private TestWebSocketAnnotationMethodMessageHandler messageHandler; + + private StaticApplicationContext applicationContext; + + + @Before + public void setUp() throws Exception { + this.applicationContext = new StaticApplicationContext(); + this.applicationContext.registerSingleton("controller", TestController.class); + this.applicationContext.registerSingleton("controllerAdvice", TestControllerAdvice.class); + this.applicationContext.refresh(); + + SubscribableChannel channel = Mockito.mock(SubscribableChannel.class); + SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel); + + this.messageHandler = new TestWebSocketAnnotationMethodMessageHandler(brokerTemplate, channel, channel); + this.messageHandler.setApplicationContext(this.applicationContext); + this.messageHandler.afterPropertiesSet(); + } + + @Test + public void globalException() throws Exception { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); + headers.setSessionId("session1"); + headers.setSessionAttributes(new ConcurrentHashMap<>()); + headers.setDestination("/exception"); + Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + this.messageHandler.handleMessage(message); + + TestControllerAdvice controllerAdvice = this.applicationContext.getBean(TestControllerAdvice.class); + assertTrue(controllerAdvice.isExceptionHandled()); + } + + + @Controller + private static class TestController { + + @MessageMapping("/exception") + @SuppressWarnings("unused") + public void handleWithSimulatedException() { + throw new IllegalStateException("simulated exception"); + } + } + + @ControllerAdvice + private static class TestControllerAdvice { + + private boolean exceptionHandled; + + + public boolean isExceptionHandled() { + return this.exceptionHandled; + } + + @MessageExceptionHandler + public void handleException(IllegalStateException ex) { + this.exceptionHandled = true; + } + } + + + private static class TestWebSocketAnnotationMethodMessageHandler extends WebSocketAnnotationMethodMessageHandler { + + + public TestWebSocketAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, + SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel) { + + super(clientInboundChannel, clientOutboundChannel, brokerTemplate); + } + + public void registerHandler(Object handler) { + super.detectHandlerMethods(handler); + } + } + +}