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);
+ }
+ }
+
+}