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
This commit is contained in:
Rossen Stoyanchev
2015-03-18 11:22:42 -04:00
parent 192462902e
commit 41e437066e
8 changed files with 377 additions and 25 deletions

View File

@@ -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));

View File

@@ -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();

View File

@@ -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<ControllerAdviceBean> 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<MessagingAdviceBean> createFromList(List<ControllerAdviceBean> controllerAdvice) {
List<MessagingAdviceBean> messagingAdvice = new ArrayList<MessagingAdviceBean>(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();
}
}
}

View File

@@ -696,7 +696,7 @@
<xsd:documentation><![CDATA[
Configures HandlerMethodArgumentResolver types to support custom controller method argument types.
Using this option does not override the built-in support for resolving handler method arguments.
To customize the built-in support for argument resolution configure SimpAnnotationMethodMessageHandler directly.
To customize the built-in support for argument resolution configure WebSocketAnnotationMethodMessageHandler directly.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
@@ -728,7 +728,7 @@
<xsd:documentation><![CDATA[
Configures HandlerMethodReturnValueHandler types to support custom controller method return value handling.
Using this option does not override the built-in support for handling return values.
To customize the built-in support for handling return values configure SimpAnnotationMethodMessageHandler directly.
To customize the built-in support for handling return values configure WebSocketAnnotationMethodMessageHandler directly.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>

View File

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